Skip to content

Commit d1dbf3c

Browse files
authored
Merge pull request #42 from ansari-project/app-version-checks
Integrate with the app-check API and implement the app update popup
2 parents 94b0fb2 + e68318b commit d1dbf3c

File tree

15 files changed

+268
-36
lines changed

15 files changed

+268
-36
lines changed

src/app/_layout.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EnhancedStore } from '@reduxjs/toolkit'
77
import React, { useEffect, useState } from 'react'
88
import { I18nextProvider } from 'react-i18next'
99
import { Provider } from 'react-redux'
10-
import { MaintenanceScreen, LoadingScreen } from '@/components'
10+
import { MaintenanceScreen, LoadingScreen, AppUpdatePopup } from '@/components'
1111
// eslint-disable-next-line camelcase
1212
import { useFonts, Inter_400Regular } from '@expo-google-fonts/inter'
1313
import RootContainer from '@/components/RootContainer'
@@ -18,6 +18,9 @@ import { isRunningInExpoGo } from 'expo'
1818
import { Platform, StatusBar, useColorScheme } from 'react-native'
1919
import { getThemeStyle } from '@/utils'
2020
import { KeyboardProvider } from 'react-native-keyboard-controller'
21+
import ApiService from '@/services/ApiService'
22+
import { AppVersionCheckResponse } from '@/types'
23+
import * as Application from 'expo-application'
2124

2225
const navigationIntegration = Sentry.reactNavigationIntegration({
2326
enableTimeToInitialDisplay: !isRunningInExpoGo(),
@@ -58,6 +61,8 @@ const RootLayout = () => {
5861
// Capture the NavigationContainer ref and register it with the integration
5962
const ref = useNavigationContainerRef()
6063
const colorScheme = useColorScheme()
64+
const [appVersionStatus, setAppVersionStatus] = useState<AppVersionCheckResponse | null>(null)
65+
const [appUpdatePopupVisible, setAppUpdatePopupVisible] = useState(false)
6166

6267
useEffect(() => {
6368
if (ref?.current) {
@@ -78,11 +83,45 @@ const RootLayout = () => {
7883
})
7984
}, [])
8085

86+
// Check app version
87+
useEffect(() => {
88+
async function checkAppVersion() {
89+
try {
90+
const apiService = new ApiService()
91+
92+
// Get native app version and build version
93+
let appVersion = '1.0.0'
94+
let buildVersion = '1'
95+
96+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
97+
appVersion = Application.nativeApplicationVersion!
98+
buildVersion = Application.nativeBuildVersion!
99+
}
100+
101+
const appVersionCheckResults = await apiService.checkAppVersion(
102+
Platform.OS, // Pass Platform.OS directly (web, ios, android)
103+
appVersion,
104+
buildVersion,
105+
)
106+
107+
setAppVersionStatus(appVersionCheckResults)
108+
setAppUpdatePopupVisible(
109+
appVersionCheckResults.force_update_required || appVersionCheckResults.update_available,
110+
)
111+
} catch (error) {
112+
console.error('Failed to check app version:', error)
113+
}
114+
}
115+
116+
checkAppVersion()
117+
}, [])
118+
81119
if (!reduxStore || !fontsLoaded) {
82120
return <LoadingScreen />
83121
}
84122

85-
if (process.env.EXPO_PUBLIC_MAINTENANCE_MODE === 'true') {
123+
// Maintenance mode check from API response
124+
if (appVersionStatus && appVersionStatus.maintenance_mode) {
86125
return <MaintenanceScreen />
87126
}
88127

@@ -94,6 +133,14 @@ const RootLayout = () => {
94133
<I18nextProvider i18n={i18n}>
95134
<Provider store={reduxStore}>
96135
<RootContainer>
136+
{appVersionStatus && (
137+
<AppUpdatePopup
138+
isForced={appVersionStatus.force_update_required}
139+
visible={appUpdatePopupVisible}
140+
onDismiss={appVersionStatus.force_update_required ? undefined : () => setAppUpdatePopupVisible(false)}
141+
/>
142+
)}
143+
97144
<Stack
98145
screenOptions={{
99146
headerShown: false,

src/components/AppUpdatePopup.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useCallback } from 'react'
2+
import { Text, View, Linking, Platform } from 'react-native'
3+
import { useTranslation } from 'react-i18next'
4+
import { useSelector } from 'react-redux'
5+
import { RootState } from '@/store'
6+
import { useDirection } from '@/hooks/useDirection'
7+
import { useScreenInfo } from '@/hooks/useScreenInfo'
8+
import ConfirmationDialog from './ConfirmationDialog'
9+
10+
interface AppUpdatePopupProps {
11+
isForced: boolean
12+
visible: boolean
13+
onDismiss?: () => void
14+
}
15+
16+
const AppUpdatePopup: React.FC<AppUpdatePopupProps> = ({ isForced, visible, onDismiss }) => {
17+
const { t } = useTranslation()
18+
const { isRTL } = useDirection()
19+
const { isSmallScreen } = useScreenInfo()
20+
const theme = useSelector((state: RootState) => state.theme.theme)
21+
22+
const openPlayStore = useCallback(async (packageName: string) => {
23+
try {
24+
await Linking.openURL(`market://details?id=${packageName}`)
25+
} catch (error) {
26+
console.error('Error opening Play Store:', error)
27+
await Linking.openURL(`https://play.google.com/store/apps/details?id=${packageName}`)
28+
}
29+
}, [])
30+
31+
const openAppStore = useCallback(async (appId: string) => {
32+
try {
33+
await Linking.openURL(`itms://itunes.apple.com/app/id${appId}`)
34+
} catch (error) {
35+
console.error('Error opening App Store:', error)
36+
await Linking.openURL(`https://apps.apple.com/app/id${appId}`)
37+
}
38+
}, [])
39+
40+
const handleUpdate = useCallback(() => {
41+
if (Platform.OS === 'android') {
42+
openPlayStore('chat.ansari.app')
43+
}
44+
if (Platform.OS === 'ios') {
45+
openAppStore('6743072108')
46+
}
47+
}, [openAppStore, openPlayStore])
48+
49+
const UpdateMessage = (
50+
<View>
51+
<Text
52+
className={`text-lg mb-2 font-bold ${isRTL ? 'text-right' : 'text-left'}`}
53+
style={{ color: theme.textColor }}
54+
>
55+
{isForced ? t('common:updateRequired') : t('common:updateAvailable')}
56+
</Text>
57+
<Text className={`${isRTL ? 'text-right' : 'text-left'}`} style={{ color: theme.textColor }}>
58+
{isForced ? t('common:updateRequiredDescription') : t('common:updateAvailableDescription')}
59+
</Text>
60+
</View>
61+
)
62+
63+
return (
64+
<ConfirmationDialog
65+
isRTL={isRTL}
66+
isSmallScreen={isSmallScreen}
67+
visible={visible}
68+
onConfirm={handleUpdate}
69+
onCancel={onDismiss || (() => {})}
70+
message={UpdateMessage}
71+
confirmButtonText={t('common:updateNow')}
72+
cancelButtonText={t('common:updateLater')}
73+
canCancel={!isForced}
74+
confirmIsPrimary={true}
75+
stacked={false}
76+
/>
77+
)
78+
}
79+
80+
export default AppUpdatePopup

src/components/ConfirmationDialog.tsx

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RootState } from '@/store'
22
import { createGeneralThemedStyles } from '@/utils'
3-
import React, { useState } from 'react'
3+
import React from 'react'
44
import { useTranslation } from 'react-i18next'
55
import { Modal, Pressable, Text, View } from 'react-native'
66
import { useSelector } from 'react-redux'
@@ -15,6 +15,9 @@ type ConfirmationDialogProps = {
1515
title?: string // Optional title text
1616
message: React.ReactNode // Message content, can be a string or a component for custom styling
1717
confirmButtonText?: string // Optional custom text for the confirm button
18+
cancelButtonText?: string // Optional custom text for the cancel button
19+
canCancel?: boolean // Controls whether the cancel button is shown, defaults to true
20+
confirmIsPrimary?: boolean // Controls whether the confirm button is styled as primary, defaults to false
1821
}
1922

2023
/**
@@ -30,16 +33,18 @@ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
3033
title,
3134
message,
3235
confirmButtonText,
36+
cancelButtonText,
37+
canCancel = true,
38+
confirmIsPrimary = false,
3339
}) => {
3440
const { t } = useTranslation()
3541
const theme = useSelector((state: RootState) => state.theme.theme)
36-
const [isHover, setIsHover] = useState<number>(0)
3742

3843
// Add your styles here
3944
const generalStyle = createGeneralThemedStyles(theme, isRTL, isSmallScreen)
4045

4146
return (
42-
<Modal visible={visible} transparent={true} animationType='fade' onRequestClose={onCancel}>
47+
<Modal visible={visible} transparent={true} animationType='fade' onRequestClose={canCancel ? onCancel : undefined}>
4348
<View className='flex-1 justify-center items-center' style={{ backgroundColor: theme.splashBackgroundColor }}>
4449
<View
4550
className={`m-5 rounded p-6 items-center ${isSmallScreen ? 'w-[90%]' : 'w-full max-w-[448px]'}`}
@@ -62,31 +67,27 @@ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
6267
{message}
6368
</Text>
6469
<View className={`mt-[15px] w-full justify-end gap-3 ${stacked ? 'flex-col' : 'flex-row'}`}>
70+
{canCancel && (
71+
<Pressable
72+
style={[
73+
!confirmIsPrimary ? generalStyle.buttonPrimary : generalStyle.buttonSecondary,
74+
generalStyle.smallButton,
75+
]}
76+
onPress={onCancel}
77+
>
78+
<Text style={!confirmIsPrimary ? generalStyle.buttonPrimaryText : generalStyle.buttonSecondaryText}>
79+
{cancelButtonText || t('cancel')}
80+
</Text>
81+
</Pressable>
82+
)}
6583
<Pressable
6684
style={[
67-
generalStyle.buttonSecondary,
68-
isHover === 0 && generalStyle.buttonPrimary,
69-
generalStyle.smallButton,
70-
]}
71-
onPress={onCancel}
72-
onMouseEnter={() => setIsHover(0)}
73-
onMouseLeave={() => setIsHover(-1)}
74-
>
75-
<Text style={[generalStyle.buttonSecondaryText, isHover === 0 && generalStyle.buttonPrimaryText]}>
76-
{t('cancel')}
77-
</Text>
78-
</Pressable>
79-
<Pressable
80-
style={[
81-
generalStyle.buttonSecondary,
82-
isHover === 1 && generalStyle.buttonPrimary,
85+
confirmIsPrimary ? generalStyle.buttonPrimary : generalStyle.buttonSecondary,
8386
generalStyle.smallButton,
8487
]}
8588
onPress={onConfirm}
86-
onMouseEnter={() => setIsHover(1)}
87-
onMouseLeave={() => setIsHover(-1)}
8889
>
89-
<Text style={[generalStyle.buttonSecondaryText, isHover === 1 && generalStyle.buttonPrimaryText]}>
90+
<Text style={confirmIsPrimary ? generalStyle.buttonPrimaryText : generalStyle.buttonSecondaryText}>
9091
{confirmButtonText || t('delete')}
9192
</Text>
9293
</Pressable>

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as LoadingScreen } from './LoadingScreen'
55
export { default as MaintenanceScreen } from './MaintenanceScreen'
66
export { default as Subscription } from './Subscription'
77
export { default as Toast } from './Toast'
8+
export { default as AppUpdatePopup } from './AppUpdatePopup'
89
export { default as ChatContainer } from './chat/ChatContainer'
910
export { default as ShareContainer } from './chat/ShareContainer'
1011
export { default as ChatInput } from './chat/ChatInput'

src/i18n/locales/ar/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@
6565
"successCopy": "تم نسخ رابط المحادثة المشتركة إلى الحافظة!"
6666
},
6767
"pageNotFound": "الصفحة غير موجودة",
68-
"notfoundMessage": "تعذر العثور على الصفحة."
68+
"notfoundMessage": "تعذر العثور على الصفحة.",
69+
"updateRequired": "تحديث مطلوب",
70+
"updateAvailable": "تحديث متاح",
71+
"updateRequiredDescription": "مطلوب إصدار جديد لمواصلة استخدام أنصاري. يرجى التحديث الآن.",
72+
"updateAvailableDescription": "يتوفر إصدار جديد من أنصاري مع ميزات وتحسينات جديدة.",
73+
"updateNow": "التحديث الآن",
74+
"updateLater": "لاحقاً"
6975
}

src/i18n/locales/bs/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@
6565
"successCopy": "Kopiran URL dijaloga u međuspremnik!"
6666
},
6767
"pageNotFound": "Stranica nije pronađena",
68-
"notfoundMessage": "Stranica nije pronađena."
68+
"notfoundMessage": "Stranica nije pronađena.",
69+
"updateRequired": "Ažuriranje obavezno",
70+
"updateAvailable": "Ažuriranje dostupno",
71+
"updateRequiredDescription": "Nova verzija je potrebna za nastavak korištenja Ansari Chata. Molimo vas da ažurirate odmah.",
72+
"updateAvailableDescription": "Nova verzija Ansari Chata je dostupna s novim funkcijama i poboljšanjima.",
73+
"updateNow": "Ažuriraj sada",
74+
"updateLater": "Kasnije"
6975
}

src/i18n/locales/en/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@
6565
"successCopy": "Copied shared conversation URL to clipboard!"
6666
},
6767
"pageNotFound": "Page Not Found",
68-
"notfoundMessage": "The page could not be found."
68+
"notfoundMessage": "The page could not be found.",
69+
"updateRequired": "Update Required",
70+
"updateAvailable": "Update Available",
71+
"updateRequiredDescription": "A new version is required to continue using Ansari Chat. Please update now.",
72+
"updateAvailableDescription": "A new version of Ansari Chat is available with new features and improvements.",
73+
"updateNow": "Update Now",
74+
"updateLater": "Later"
6975
}

src/i18n/locales/fr/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@
6565
"successCopy": "URL de la conversation partagée copié dans le presse-papiers !"
6666
},
6767
"pageNotFound": "Page non trouvée",
68-
"notfoundMessage": "La page demandée est introuvable."
68+
"notfoundMessage": "La page demandée est introuvable.",
69+
"updateRequired": "Mise à jour requise",
70+
"updateAvailable": "Mise à jour disponible",
71+
"updateRequiredDescription": "Une nouvelle version est nécessaire pour continuer à utiliser Ansari Chat. Veuillez mettre à jour maintenant.",
72+
"updateAvailableDescription": "Une nouvelle version d'Ansari Chat est disponible avec de nouvelles fonctionnalités et améliorations.",
73+
"updateNow": "Mettre à jour maintenant",
74+
"updateLater": "Plus tard"
6975
}

src/i18n/locales/id/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,11 @@
6767
"successCopy": "Tautan percakapan yang dibagikan telah disalin ke papan klip!"
6868
},
6969
"pageNotFound": "Halaman Tidak Ditemukan",
70-
"notfoundMessage": "Halaman tidak dapat ditemukan."
70+
"notfoundMessage": "Halaman tidak dapat ditemukan.",
71+
"updateRequired": "Pembaruan Diperlukan",
72+
"updateAvailable": "Pembaruan Tersedia",
73+
"updateRequiredDescription": "Versi baru diperlukan untuk melanjutkan menggunakan Ansari Chat. Silakan perbarui sekarang.",
74+
"updateAvailableDescription": "Versi baru Ansari Chat tersedia dengan fitur dan peningkatan baru.",
75+
"updateNow": "Perbarui Sekarang",
76+
"updateLater": "Nanti"
7177
}

src/i18n/locales/tml/common.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@
6565
"successCopy": "பகிரப்பட்ட உரையாடல் URL கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது!"
6666
},
6767
"pageNotFound": "இந்த இணையதள பக்கம் கிடைக்கவில்லை.",
68-
"notfoundMessage": "இந்த இணையதள பக்கம் கிடைக்கவில்லை."
68+
"notfoundMessage": "இந்த இணையதள பக்கம் கிடைக்கவில்லை.",
69+
"updateRequired": "புதுப்பித்தல் தேவை",
70+
"updateAvailable": "புதுப்பிப்பு கிடைக்கிறது",
71+
"updateRequiredDescription": "அன்சாரி சாட்டைத் தொடர்ந்து பயன்படுத்த புதிய பதிப்பு தேவை. இப்போது புதுப்பிக்கவும்.",
72+
"updateAvailableDescription": "புதிய அம்சங்கள் மற்றும் மேம்பாடுகளுடன் அன்சாரி சாட்டின் புதிய பதிப்பு கிடைக்கிறது.",
73+
"updateNow": "இப்போது புதுப்பி",
74+
"updateLater": "பிறகு"
6975
}

0 commit comments

Comments
 (0)