diff --git a/android/app/build.gradle b/android/app/build.gradle index 8a73332..fce61fe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -92,8 +92,8 @@ android { applicationId 'com.vecmocon.driversaathi' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 5 - versionName "1.0.0" + versionCode 11 + versionName "2.0.0" } signingConfigs { debug { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 731f3e1..849e9f6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,6 @@ - - - - diff --git a/app.json b/app.json index 24c1166..8abeba6 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Driver Saathi", "slug": "BatteryAsAService", - "version": "1.0.0", + "version": "2.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "batteryasaservice", @@ -23,7 +23,7 @@ }, "edgeToEdgeEnabled": true, "package": "com.vecmocon.driversaathi", - "versionCode": 5 + "versionCode": 11 }, "web": { "bundler": "metro", diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index aa9b3ac..72ef08f 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -8,7 +8,6 @@ import { useSnackbar } from "@/contexts/Snackbar"; import NetInfo from "@react-native-community/netinfo"; import { getPaymentSummary, getUserDetails } from "@/store/userSlice"; import { useDispatch } from "react-redux"; -import { logout } from "@/store/authSlice"; import { getLanguage, setLanguage } from "@/services/i18n"; import { useTranslation } from "react-i18next"; diff --git a/app/(tabs)/payments.tsx b/app/(tabs)/payments.tsx index a30eaf6..76f7f1d 100644 --- a/app/(tabs)/payments.tsx +++ b/app/(tabs)/payments.tsx @@ -25,7 +25,7 @@ import { useSnackbar } from "@/contexts/Snackbar"; import CustomerCareIcon from "../../assets/icons/customer-care.svg"; import api from "@/services/axiosClient"; import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard"; -import { BASE_URL } from "@/constants/config"; +import { BASE_URL, STORAGE_KEYS } from "@/constants/config"; import { useDispatch } from "react-redux"; import { setAdvanceBalance, @@ -39,6 +39,7 @@ import RefreshIcon from "@/assets/icons/refresh.svg"; import CustomerSupport from "@/components/home/CustomerSupportModal"; import { useTranslation } from "react-i18next"; import { Image } from "expo-image"; +import AsyncStorage from "@react-native-async-storage/async-storage"; export interface MyPlan { no_of_emi: number; @@ -149,6 +150,12 @@ export default function PaymentsTabScreen() { const fetchEmiDetails = async () => { try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + console.log("No auth token found, skipping API call"); + return; + } + setIsLoading(true); setEmiDetails(null); setAdvanceBalance(null); @@ -218,6 +225,8 @@ export default function PaymentsTabScreen() { spinValue.setValue(0); }; + const { isLoggedIn } = useSelector((state: RootState) => state.auth); + const spin = spinValue.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"], @@ -225,10 +234,12 @@ export default function PaymentsTabScreen() { useFocusEffect( useCallback(() => { - setShowFullHistory(false); - fetchEmiDetails(); - fetchPaymentHistory(1, false); - }, []) + if (isLoggedIn) { + setShowFullHistory(false); + fetchEmiDetails(); + fetchPaymentHistory(1, false); + } + }, [isLoggedIn]) ); useLayoutEffect(() => { @@ -319,6 +330,12 @@ export default function PaymentsTabScreen() { pageNumber: number = 1, isLoadMore: boolean = false ) => { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + console.log("No auth token found, skipping API call"); + return; + } + try { if (isLoadMore) { setIsLoadingMore(true); diff --git a/app/(tabs)/service.tsx b/app/(tabs)/service.tsx index 0d919d9..0b41077 100644 --- a/app/(tabs)/service.tsx +++ b/app/(tabs)/service.tsx @@ -42,22 +42,28 @@ interface FormValues { interface Issue { id: number; - name: string; + name: { + en: String; + hi: String; + }; } -const validationSchema = Yup.object().shape({ - serviceType: Yup.string().required("Service Type is required"), - issues: Yup.array().when("serviceType", { - is: (val: string) => val !== "Regular", - then: (schema) => schema.min(1, "At least one issue is required"), - otherwise: (schema) => schema.notRequired(), - }), - date: Yup.date().required("Date and Time is required"), - photos: Yup.array().min(1, "At least one photo is required"), - comments: Yup.string(), -}); - export default function ServiceFormScreen(): JSX.Element { + const { t } = useTranslation(); + + const validationSchema = Yup.object().shape({ + serviceType: Yup.string().required(t("service.service-type-is-required")), + issues: Yup.array().when("serviceType", { + is: (val: string) => val !== "Regular", + then: (schema) => + schema.min(1, t("service.atleast-one-issue-is-required")), + otherwise: (schema) => schema.notRequired(), + }), + date: Yup.date().required(t("service.date-and-time-is-required")), + photos: Yup.array().min(1, t("service.atleast-one-photo-is-required")), + comments: Yup.string(), + }); + const [isFocus, setIsFocus] = useState(false); const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false); @@ -196,26 +202,43 @@ export default function ServiceFormScreen(): JSX.Element { }); }; - const { t } = useTranslation(); - const handleServiceTypeChange = async ( item: { label: string; value: string }, setFieldValue: (field: string, value: any) => void, setFieldTouched: (field: string, touched: boolean) => void ) => { - setFieldValue("serviceType", item.value); - if (item.value === "Regular") { + // Regular service can be selected immediately + setFieldValue("serviceType", item.value); setFieldValue("issues", []); setFieldTouched("issues", false); setIssues([]); + setIsFocus(false); } else if (item.value === "On-demand") { - await fetchIssues(); - setFieldValue("issues", []); - setFieldTouched("issues", false); - } + try { + setFieldValue("serviceType", item.value); + setIsLoadingIssues(true); + const response = await api.get(`${BASE_URL}/api/v1/service-issue-list`); - setIsFocus(false); + if (response.data.success) { + setIssues(response.data.data); + setFieldValue("serviceType", item.value); + setFieldValue("issues", []); + setFieldTouched("issues", false); + setIsFocus(false); + } else { + setFieldValue("serviceType", null); + throw new Error(response.data?.message || "Failed to fetch issues"); + } + } catch (error: any) { + console.error("Error fetching issues:", error); + showSnackbar(`${t("common.something-went-wrong")}`, "error"); + setIssues([]); + setFieldValue("serviceType", null); + } finally { + setIsLoadingIssues(false); + } + } }; const getSelectedIssuesText = (selectedIssueIds: number[]) => { diff --git a/app/auth/login.tsx b/app/auth/login.tsx index cd36c21..cd50ae3 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -40,8 +40,8 @@ export default function WelcomeScreen() { const phoneValidationSchema = Yup.object().shape({ phone: Yup.string() - .required("Phone number is required") - .matches(/^\d{10}$/, "Phone number must be exactly 10 digits"), + .required(t("onboarding.phone-number-is-required")) + .matches(/^\d{10}$/, t("onboarding.must-be-10-digits")), }); useEffect(() => { if (status === AUTH_STATUSES.SUCCESS && otpId) { diff --git a/app/payments/payEmi.tsx b/app/payments/payEmi.tsx index f55e0fe..b1e78e3 100644 --- a/app/payments/payEmi.tsx +++ b/app/payments/payEmi.tsx @@ -4,8 +4,6 @@ import { Text, TouchableOpacity, StyleSheet, - Alert, - Share, Linking, BackHandler, ScrollView, @@ -192,7 +190,9 @@ const UpiPaymentScreen = () => { }; function handlePaymentDone() { - router.navigate("/(tabs)/payments"); + offPaymentConfirmation(); + disconnect(); + router.replace("/(tabs)/payments"); } const { t } = useTranslation(); diff --git a/app/payments/selectAmount.tsx b/app/payments/selectAmount.tsx index a0c72d4..35ffae2 100644 --- a/app/payments/selectAmount.tsx +++ b/app/payments/selectAmount.tsx @@ -24,30 +24,30 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; import { displayValue, formatCurrency } from "@/utils/Common"; -const validationSchema = Yup.object().shape({ - paymentType: Yup.string().required("Please select a payment option"), - customAmount: Yup.string().when("paymentType", { - is: "custom", - then: (schema) => - schema - .required("Amount is required") - .test("valid-number", "Please enter a valid amount", (value) => { - const numValue = parseFloat(value); - return !isNaN(numValue) && numValue > 0; - }) - .test( - "min-amount", - `Minimum amount is ₹${payments.MIN_AMOUNT}`, - (value) => { - const numValue = parseFloat(value); - return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT; - } - ), - otherwise: (schema) => schema.notRequired(), - }), -}); - const SelectAmountScreen = () => { + const validationSchema = Yup.object().shape({ + paymentType: Yup.string().required("Please select a payment option"), + customAmount: Yup.string().when("paymentType", { + is: "custom", + then: (schema) => + schema + .required(t("payment.amount-is-required")) + .test("valid-number", "Please enter a valid amount", (value) => { + const numValue = parseFloat(value); + return !isNaN(numValue) && numValue > 0; + }) + .test( + "min-amount", + `${t("payment.minimum")}: ₹${payments.MIN_AMOUNT}`, + (value) => { + const numValue = parseFloat(value); + return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT; + } + ), + otherwise: (schema) => schema.notRequired(), + }), + }); + const dueAmount = useSelector( (state: RootState) => state.payments?.due_amount || 0 ); @@ -365,7 +365,8 @@ const SelectAmountScreen = () => { ) : ( - Pay {displayValue(getPaymentAmount(), formatCurrency)} + {`${t("payment.pay")}`}{" "} + {displayValue(getPaymentAmount(), formatCurrency)} )} diff --git a/app/user/profile.tsx b/app/user/profile.tsx index d073288..50eaeb5 100644 --- a/app/user/profile.tsx +++ b/app/user/profile.tsx @@ -23,6 +23,7 @@ import { setUserData } from "@/store/userSlice"; import { Overlay } from "@/components/common/Overlay"; import Header from "@/components/common/Header"; import { useTranslation } from "react-i18next"; +import { clearStackAndRouteTo } from "@/utils/Common"; export default function ProfileScreen() { const [isLangaugeModalVisible, setLanguageModalVisible] = @@ -43,7 +44,7 @@ export default function ProfileScreen() { const handleLogout = () => { dispatch(logout()); - router.replace("/auth/login"); + clearStackAndRouteTo("/auth/login"); }; const handlePickImage = async () => { diff --git a/components/service/IssueSelectorModal.tsx b/components/service/IssueSelectorModal.tsx index 91304bc..3a6118d 100644 --- a/components/service/IssueSelectorModal.tsx +++ b/components/service/IssueSelectorModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { View, Text, @@ -11,10 +11,15 @@ import { import Checkbox from "expo-checkbox"; import { issueConfig } from "@/constants/config"; import CloseIcon from "@/assets/icons/close.svg"; +import { getLanguage } from "@/services/i18n"; +import { useFocusEffect } from "expo-router"; interface Issue { id: number; - name: string; + name: { + en: String; + hi: String; + }; } interface IssueSelectorModalProps { @@ -37,6 +42,8 @@ export default function IssueSelectorModal({ ); const [search, setSearch] = useState(""); + const [lang, setLang] = useState<"en" | "hi">("en"); + const toggleValue = (id: number) => { setSelectedValues((prev) => prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] @@ -44,7 +51,7 @@ export default function IssueSelectorModal({ }; const filteredIssues = issues.filter((issue) => - issue.name.toLowerCase().includes(search.toLowerCase().trim()) + issue.name[lang].toLowerCase().includes(search.toLowerCase().trim()) ); const clearSelection = () => setSelectedValues([]); @@ -73,6 +80,17 @@ export default function IssueSelectorModal({ } }, [visible, initialSelectedValues]); + useFocusEffect( + useCallback(() => { + const fetchLang = async () => { + const selectedLang = await getLanguage(); + setLang((selectedLang as "en") || "hi"); + }; + + fetchLang(); + }, []) // dependencies, can include anything that triggers refetch + ); + return ( @@ -126,7 +144,7 @@ export default function IssueSelectorModal({ selectedValues.includes(issue.id) ? "#009E71" : undefined } /> - {issue.name} + {issue.name[lang]} )) ) : issues.length === 0 ? ( diff --git a/constants/config.ts b/constants/config.ts index 8acf168..4df3280 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -12,9 +12,8 @@ import DangerIcon from "../assets/icons/danger.svg"; import type { BmsState } from "./types"; import { useTranslation } from "react-i18next"; -export const BASE_URL = "https://dev-driver-saathi-api.vecmocon.com"; -export const PAYMENT_SOCKET_BASE_URL = - "wss://dev-driver-saathi-api.vecmocon.com"; +export const BASE_URL = "https://driver-saathi-api.vecmocon.com"; +export const PAYMENT_SOCKET_BASE_URL = "wss://driver-saathi-api.vecmocon.com"; // export const BASE_URL = "https://46fa2cacfc37.ngrok-free.app"; // const SERVER_URL = @@ -181,7 +180,7 @@ export const issueConfig = [ ]; export const payments = { - MIN_AMOUNT: 1, + MIN_AMOUNT: 200, MAX_AMOUNT: 500000, LINK_EXPIRED: "Payment link expired", SOCKET_CONNECTION_TIMEOUT_IN_SECS: 5, diff --git a/package.json b/package.json index e3830f4..ad6b126 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "batteryasaservice", "main": "expo-router/entry", - "version": "1.0.0", + "version": "2.0.0", "scripts": { "start": "expo start --dev-client", "android": "expo run:android", diff --git a/services/i18n/index.ts b/services/i18n/index.ts index 005107e..9fa02ff 100644 --- a/services/i18n/index.ts +++ b/services/i18n/index.ts @@ -31,6 +31,7 @@ export const setLanguage = async (language: string) => { export const getLanguage = async () => { const lang = await AsyncStorage.getItem(STORAGE_KEYS.LANGUAGE); + if (!lang) return "en"; return lang; }; diff --git a/services/i18n/locals/en.json b/services/i18n/locals/en.json index 88d5343..59f5fdd 100644 --- a/services/i18n/locals/en.json +++ b/services/i18n/locals/en.json @@ -9,9 +9,11 @@ "enter-otp": "Please enter OTP sent to your mobile number", "verify-otp": "Verify OTP", "otp-incorrect": "OTP incorrect.", - "resend-otp": "Resend OTP in", + "resend-otp": "Resend OTP ", "verification-limit-exceeded": "Verification limit exceeded, try again later", - "send-otp": "Send OTP" + "send-otp": "Send OTP", + "phone-number-is-required": "Phone number is required", + "must-be-10-digits": "Phone number must be exactly 10 digits" }, "navigation": { "home": "Home", @@ -116,7 +118,9 @@ "view-plan": "View Plan", "cancel-payment": "Cancel Payment", "continue-payment": "Continue Payment", - "do-you-want-to-cancel-payment": "Are you sure you want to cancel the payment?" + "do-you-want-to-cancel-payment": "Are you sure you want to cancel the payment?", + "amount-is-required": "Amount is required", + "pay": "Pay" }, "service": { "schedule-maintenance": "Schedule Maintenance", @@ -135,7 +139,11 @@ "select-valid-time": "Select valid time", "words": "words", "time-must-be-between-10-and-5": "Select a time between 10 AM – 5 PM.", - "regular-available-after-6-months": "Regular service available after 6 months of purchase" + "regular-available-after-6-months": "Regular service available after 6 months of purchase", + "service-type-is-required": "Service Type is required", + "atleast-one-issue-is-required": "At least one issue is required", + "date-and-time-is-required": "Date and time is required", + "atleast-one-photo-is-required": "At least one photo is required" }, "battery": { "battery-and-warranty": "My Battery and Warranty", diff --git a/services/i18n/locals/hi.json b/services/i18n/locals/hi.json index 67ab3e6..78b169c 100644 --- a/services/i18n/locals/hi.json +++ b/services/i18n/locals/hi.json @@ -11,7 +11,9 @@ "otp-incorrect": "OTP गलत है।", "resend-otp": "OTP पुनः भेजें", "verification-limit-exceeded": "वेरिफिकेशन लिमिट पार हो गई है, बाद में फिर से कोशिश करो", - "send-otp": "OTP भेजें" + "send-otp": "OTP भेजें", + "phone-number-is-required": "फ़ोन नंबर अनिवार्य है", + "must-be-10-digits": "फ़ोन नंबर बिल्कुल 10 अंकों का होना चाहिए" }, "navigation": { "home": "होम", @@ -116,7 +118,9 @@ "payment-done": "भुगतान हो गया", "cancel-payment": "भुगतान रद्द करें", "continue-payment": "भुगतान जारी रखें", - "do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?" + "do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?", + "amount-is-required": "राशि दर्ज करें", + "pay": "भुगतान करें" }, "service": { "schedule-maintenance": "शेड्यूल मेंटेनेंस", @@ -135,7 +139,11 @@ "words": "शब्द", "select-valid-time": "सही समय चुनें", "time-must-be-between-10-and-5": "समय सुबह 10:00 बजे से शाम 5:00 बजे के बीच चुनें।", - "regular-available-after-6-months": "रेगुलर सेवा खरीद के 6 महीने बाद उपलब्ध होगी" + "regular-available-after-6-months": "रेगुलर सेवा खरीद के 6 महीने बाद उपलब्ध होगी", + "service-type-is-required": "सेवा का प्रकार चुनें", + "atleast-one-issue-is-required": "कम से कम एक समस्या चुनें", + "date-and-time-is-required": "तारीख और समय चुनें", + "atleast-one-photo-is-required": "कम से कम एक फोटो चुनें" }, "battery": { "battery-and-warranty": "मेरी बैटरी और वारंटी", diff --git a/utils/Common.ts b/utils/Common.ts index 54a2bfd..74e6916 100644 --- a/utils/Common.ts +++ b/utils/Common.ts @@ -1,3 +1,6 @@ +import { router, type Href } from "expo-router"; +import type { NavigationOptions } from "expo-router/build/global-state/routing"; + export const displayValue = (value: any, formatter?: (val: any) => string) => { if (value === null || value === undefined || value === "") { return "--"; @@ -8,3 +11,11 @@ export const displayValue = (value: any, formatter?: (val: any) => string) => { export const formatCurrency = (amount: number) => { return `₹${amount.toLocaleString()}`; }; + +export const clearStackAndRouteTo = ( + link: Href, + options?: NavigationOptions +): void => { + router.dismissAll(); + router.replace(link, options); +};