NOt publised, some permissions issue

rename-package
vinay kumar 2025-10-17 12:25:27 +05:30
parent 39a65e00e3
commit 8a60842bec
17 changed files with 164 additions and 82 deletions

View File

@ -92,8 +92,8 @@ android {
applicationId 'com.vecmocon.driversaathi' applicationId 'com.vecmocon.driversaathi'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 5 versionCode 11
versionName "1.0.0" versionName "2.0.0"
} }
signingConfigs { signingConfigs {
debug { debug {

View File

@ -1,10 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.vecmocon.driversaathi" xmlns:tools="http://schemas.android.com/tools"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.vecmocon.driversaathi" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>

View File

@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Driver Saathi", "name": "Driver Saathi",
"slug": "BatteryAsAService", "slug": "BatteryAsAService",
"version": "1.0.0", "version": "2.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "batteryasaservice", "scheme": "batteryasaservice",
@ -23,7 +23,7 @@
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"package": "com.vecmocon.driversaathi", "package": "com.vecmocon.driversaathi",
"versionCode": 5 "versionCode": 11
}, },
"web": { "web": {
"bundler": "metro", "bundler": "metro",

View File

@ -8,7 +8,6 @@ import { useSnackbar } from "@/contexts/Snackbar";
import NetInfo from "@react-native-community/netinfo"; import NetInfo from "@react-native-community/netinfo";
import { getPaymentSummary, getUserDetails } from "@/store/userSlice"; import { getPaymentSummary, getUserDetails } from "@/store/userSlice";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { logout } from "@/store/authSlice";
import { getLanguage, setLanguage } from "@/services/i18n"; import { getLanguage, setLanguage } from "@/services/i18n";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@ -25,7 +25,7 @@ import { useSnackbar } from "@/contexts/Snackbar";
import CustomerCareIcon from "../../assets/icons/customer-care.svg"; import CustomerCareIcon from "../../assets/icons/customer-care.svg";
import api from "@/services/axiosClient"; import api from "@/services/axiosClient";
import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard"; 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 { useDispatch } from "react-redux";
import { import {
setAdvanceBalance, setAdvanceBalance,
@ -39,6 +39,7 @@ import RefreshIcon from "@/assets/icons/refresh.svg";
import CustomerSupport from "@/components/home/CustomerSupportModal"; import CustomerSupport from "@/components/home/CustomerSupportModal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Image } from "expo-image"; import { Image } from "expo-image";
import AsyncStorage from "@react-native-async-storage/async-storage";
export interface MyPlan { export interface MyPlan {
no_of_emi: number; no_of_emi: number;
@ -149,6 +150,12 @@ export default function PaymentsTabScreen() {
const fetchEmiDetails = async () => { const fetchEmiDetails = async () => {
try { try {
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) {
console.log("No auth token found, skipping API call");
return;
}
setIsLoading(true); setIsLoading(true);
setEmiDetails(null); setEmiDetails(null);
setAdvanceBalance(null); setAdvanceBalance(null);
@ -218,6 +225,8 @@ export default function PaymentsTabScreen() {
spinValue.setValue(0); spinValue.setValue(0);
}; };
const { isLoggedIn } = useSelector((state: RootState) => state.auth);
const spin = spinValue.interpolate({ const spin = spinValue.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: ["0deg", "360deg"], outputRange: ["0deg", "360deg"],
@ -225,10 +234,12 @@ export default function PaymentsTabScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setShowFullHistory(false); if (isLoggedIn) {
fetchEmiDetails(); setShowFullHistory(false);
fetchPaymentHistory(1, false); fetchEmiDetails();
}, []) fetchPaymentHistory(1, false);
}
}, [isLoggedIn])
); );
useLayoutEffect(() => { useLayoutEffect(() => {
@ -319,6 +330,12 @@ export default function PaymentsTabScreen() {
pageNumber: number = 1, pageNumber: number = 1,
isLoadMore: boolean = false 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 { try {
if (isLoadMore) { if (isLoadMore) {
setIsLoadingMore(true); setIsLoadingMore(true);

View File

@ -42,22 +42,28 @@ interface FormValues {
interface Issue { interface Issue {
id: number; 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 { 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<boolean>(false); const [isFocus, setIsFocus] = useState<boolean>(false);
const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false); const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false);
@ -196,26 +202,43 @@ export default function ServiceFormScreen(): JSX.Element {
}); });
}; };
const { t } = useTranslation();
const handleServiceTypeChange = async ( const handleServiceTypeChange = async (
item: { label: string; value: string }, item: { label: string; value: string },
setFieldValue: (field: string, value: any) => void, setFieldValue: (field: string, value: any) => void,
setFieldTouched: (field: string, touched: boolean) => void setFieldTouched: (field: string, touched: boolean) => void
) => { ) => {
setFieldValue("serviceType", item.value);
if (item.value === "Regular") { if (item.value === "Regular") {
// Regular service can be selected immediately
setFieldValue("serviceType", item.value);
setFieldValue("issues", []); setFieldValue("issues", []);
setFieldTouched("issues", false); setFieldTouched("issues", false);
setIssues([]); setIssues([]);
setIsFocus(false);
} else if (item.value === "On-demand") { } else if (item.value === "On-demand") {
await fetchIssues(); try {
setFieldValue("issues", []); setFieldValue("serviceType", item.value);
setFieldTouched("issues", false); 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[]) => { const getSelectedIssuesText = (selectedIssueIds: number[]) => {

View File

@ -40,8 +40,8 @@ export default function WelcomeScreen() {
const phoneValidationSchema = Yup.object().shape({ const phoneValidationSchema = Yup.object().shape({
phone: Yup.string() phone: Yup.string()
.required("Phone number is required") .required(t("onboarding.phone-number-is-required"))
.matches(/^\d{10}$/, "Phone number must be exactly 10 digits"), .matches(/^\d{10}$/, t("onboarding.must-be-10-digits")),
}); });
useEffect(() => { useEffect(() => {
if (status === AUTH_STATUSES.SUCCESS && otpId) { if (status === AUTH_STATUSES.SUCCESS && otpId) {

View File

@ -4,8 +4,6 @@ import {
Text, Text,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Alert,
Share,
Linking, Linking,
BackHandler, BackHandler,
ScrollView, ScrollView,
@ -192,7 +190,9 @@ const UpiPaymentScreen = () => {
}; };
function handlePaymentDone() { function handlePaymentDone() {
router.navigate("/(tabs)/payments"); offPaymentConfirmation();
disconnect();
router.replace("/(tabs)/payments");
} }
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -24,30 +24,30 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { displayValue, formatCurrency } from "@/utils/Common"; 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 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( const dueAmount = useSelector(
(state: RootState) => state.payments?.due_amount || 0 (state: RootState) => state.payments?.due_amount || 0
); );
@ -365,7 +365,8 @@ const SelectAmountScreen = () => {
</Text> </Text>
) : ( ) : (
<Text style={styles.payButtonText}> <Text style={styles.payButtonText}>
Pay {displayValue(getPaymentAmount(), formatCurrency)} {`${t("payment.pay")}`}{" "}
{displayValue(getPaymentAmount(), formatCurrency)}
</Text> </Text>
)} )}
</TouchableOpacity> </TouchableOpacity>

View File

@ -23,6 +23,7 @@ import { setUserData } from "@/store/userSlice";
import { Overlay } from "@/components/common/Overlay"; import { Overlay } from "@/components/common/Overlay";
import Header from "@/components/common/Header"; import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { clearStackAndRouteTo } from "@/utils/Common";
export default function ProfileScreen() { export default function ProfileScreen() {
const [isLangaugeModalVisible, setLanguageModalVisible] = const [isLangaugeModalVisible, setLanguageModalVisible] =
@ -43,7 +44,7 @@ export default function ProfileScreen() {
const handleLogout = () => { const handleLogout = () => {
dispatch(logout()); dispatch(logout());
router.replace("/auth/login"); clearStackAndRouteTo("/auth/login");
}; };
const handlePickImage = async () => { const handlePickImage = async () => {

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { import {
View, View,
Text, Text,
@ -11,10 +11,15 @@ import {
import Checkbox from "expo-checkbox"; import Checkbox from "expo-checkbox";
import { issueConfig } from "@/constants/config"; import { issueConfig } from "@/constants/config";
import CloseIcon from "@/assets/icons/close.svg"; import CloseIcon from "@/assets/icons/close.svg";
import { getLanguage } from "@/services/i18n";
import { useFocusEffect } from "expo-router";
interface Issue { interface Issue {
id: number; id: number;
name: string; name: {
en: String;
hi: String;
};
} }
interface IssueSelectorModalProps { interface IssueSelectorModalProps {
@ -37,6 +42,8 @@ export default function IssueSelectorModal({
); );
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [lang, setLang] = useState<"en" | "hi">("en");
const toggleValue = (id: number) => { const toggleValue = (id: number) => {
setSelectedValues((prev) => setSelectedValues((prev) =>
prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id]
@ -44,7 +51,7 @@ export default function IssueSelectorModal({
}; };
const filteredIssues = issues.filter((issue) => const filteredIssues = issues.filter((issue) =>
issue.name.toLowerCase().includes(search.toLowerCase().trim()) issue.name[lang].toLowerCase().includes(search.toLowerCase().trim())
); );
const clearSelection = () => setSelectedValues([]); const clearSelection = () => setSelectedValues([]);
@ -73,6 +80,17 @@ export default function IssueSelectorModal({
} }
}, [visible, initialSelectedValues]); }, [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 ( return (
<Modal visible={visible} animationType="slide"> <Modal visible={visible} animationType="slide">
<View style={styles.container}> <View style={styles.container}>
@ -126,7 +144,7 @@ export default function IssueSelectorModal({
selectedValues.includes(issue.id) ? "#009E71" : undefined selectedValues.includes(issue.id) ? "#009E71" : undefined
} }
/> />
<Text style={styles.itemLabel}>{issue.name}</Text> <Text style={styles.itemLabel}>{issue.name[lang]}</Text>
</TouchableOpacity> </TouchableOpacity>
)) ))
) : issues.length === 0 ? ( ) : issues.length === 0 ? (

View File

@ -12,9 +12,8 @@ import DangerIcon from "../assets/icons/danger.svg";
import type { BmsState } from "./types"; import type { BmsState } from "./types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export const BASE_URL = "https://dev-driver-saathi-api.vecmocon.com"; export const BASE_URL = "https://driver-saathi-api.vecmocon.com";
export const PAYMENT_SOCKET_BASE_URL = export const PAYMENT_SOCKET_BASE_URL = "wss://driver-saathi-api.vecmocon.com";
"wss://dev-driver-saathi-api.vecmocon.com";
// export const BASE_URL = "https://46fa2cacfc37.ngrok-free.app"; // export const BASE_URL = "https://46fa2cacfc37.ngrok-free.app";
// const SERVER_URL = // const SERVER_URL =
@ -181,7 +180,7 @@ export const issueConfig = [
]; ];
export const payments = { export const payments = {
MIN_AMOUNT: 1, MIN_AMOUNT: 200,
MAX_AMOUNT: 500000, MAX_AMOUNT: 500000,
LINK_EXPIRED: "Payment link expired", LINK_EXPIRED: "Payment link expired",
SOCKET_CONNECTION_TIMEOUT_IN_SECS: 5, SOCKET_CONNECTION_TIMEOUT_IN_SECS: 5,

View File

@ -1,7 +1,7 @@
{ {
"name": "batteryasaservice", "name": "batteryasaservice",
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.0.0", "version": "2.0.0",
"scripts": { "scripts": {
"start": "expo start --dev-client", "start": "expo start --dev-client",
"android": "expo run:android", "android": "expo run:android",

View File

@ -31,6 +31,7 @@ export const setLanguage = async (language: string) => {
export const getLanguage = async () => { export const getLanguage = async () => {
const lang = await AsyncStorage.getItem(STORAGE_KEYS.LANGUAGE); const lang = await AsyncStorage.getItem(STORAGE_KEYS.LANGUAGE);
if (!lang) return "en";
return lang; return lang;
}; };

View File

@ -9,9 +9,11 @@
"enter-otp": "Please enter OTP sent to your mobile number", "enter-otp": "Please enter OTP sent to your mobile number",
"verify-otp": "Verify OTP", "verify-otp": "Verify OTP",
"otp-incorrect": "OTP incorrect.", "otp-incorrect": "OTP incorrect.",
"resend-otp": "Resend OTP in", "resend-otp": "Resend OTP ",
"verification-limit-exceeded": "Verification limit exceeded, try again later", "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": { "navigation": {
"home": "Home", "home": "Home",
@ -116,7 +118,9 @@
"view-plan": "View Plan", "view-plan": "View Plan",
"cancel-payment": "Cancel Payment", "cancel-payment": "Cancel Payment",
"continue-payment": "Continue 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": { "service": {
"schedule-maintenance": "Schedule Maintenance", "schedule-maintenance": "Schedule Maintenance",
@ -135,7 +139,11 @@
"select-valid-time": "Select valid time", "select-valid-time": "Select valid time",
"words": "words", "words": "words",
"time-must-be-between-10-and-5": "Select a time between 10 AM 5 PM.", "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": {
"battery-and-warranty": "My Battery and Warranty", "battery-and-warranty": "My Battery and Warranty",

View File

@ -11,7 +11,9 @@
"otp-incorrect": "OTP गलत है।", "otp-incorrect": "OTP गलत है।",
"resend-otp": "OTP पुनः भेजें", "resend-otp": "OTP पुनः भेजें",
"verification-limit-exceeded": "वेरिफिकेशन लिमिट पार हो गई है, बाद में फिर से कोशिश करो", "verification-limit-exceeded": "वेरिफिकेशन लिमिट पार हो गई है, बाद में फिर से कोशिश करो",
"send-otp": "OTP भेजें" "send-otp": "OTP भेजें",
"phone-number-is-required": "फ़ोन नंबर अनिवार्य है",
"must-be-10-digits": "फ़ोन नंबर बिल्कुल 10 अंकों का होना चाहिए"
}, },
"navigation": { "navigation": {
"home": "होम", "home": "होम",
@ -116,7 +118,9 @@
"payment-done": "भुगतान हो गया", "payment-done": "भुगतान हो गया",
"cancel-payment": "भुगतान रद्द करें", "cancel-payment": "भुगतान रद्द करें",
"continue-payment": "भुगतान जारी रखें", "continue-payment": "भुगतान जारी रखें",
"do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?" "do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?",
"amount-is-required": "राशि दर्ज करें",
"pay": "भुगतान करें"
}, },
"service": { "service": {
"schedule-maintenance": "शेड्यूल मेंटेनेंस", "schedule-maintenance": "शेड्यूल मेंटेनेंस",
@ -135,7 +139,11 @@
"words": "शब्द", "words": "शब्द",
"select-valid-time": "सही समय चुनें", "select-valid-time": "सही समय चुनें",
"time-must-be-between-10-and-5": "समय सुबह 10:00 बजे से शाम 5:00 बजे के बीच चुनें।", "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": {
"battery-and-warranty": "मेरी बैटरी और वारंटी", "battery-and-warranty": "मेरी बैटरी और वारंटी",

View File

@ -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) => { export const displayValue = (value: any, formatter?: (val: any) => string) => {
if (value === null || value === undefined || value === "") { if (value === null || value === undefined || value === "") {
return "--"; return "--";
@ -8,3 +11,11 @@ export const displayValue = (value: any, formatter?: (val: any) => string) => {
export const formatCurrency = (amount: number) => { export const formatCurrency = (amount: number) => {
return `${amount.toLocaleString()}`; return `${amount.toLocaleString()}`;
}; };
export const clearStackAndRouteTo = (
link: Href,
options?: NavigationOptions
): void => {
router.dismissAll();
router.replace(link, options);
};