mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {Alert, AppState, AppStateStatus} from 'react-native'
3import {nativeBuildVersion} from 'expo-application'
4import {
5 checkForUpdateAsync,
6 fetchUpdateAsync,
7 isEnabled,
8 reloadAsync,
9 setExtraParamAsync,
10 useUpdates,
11} from 'expo-updates'
12
13import {IS_TESTFLIGHT} from '#/lib/app-info'
14import {logger} from '#/logger'
15import {isIOS} from '#/platform/detection'
16
17const MINIMUM_MINIMIZE_TIME = 15 * 60e3
18
19async function setExtraParams() {
20 await setExtraParamAsync(
21 isIOS ? 'ios-build-number' : 'android-build-number',
22 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
23 // This just ensures it gets passed as a string
24 `${nativeBuildVersion}`,
25 )
26 await setExtraParamAsync(
27 'channel',
28 IS_TESTFLIGHT ? 'testflight' : 'production',
29 )
30}
31
32export function useOTAUpdates() {
33 const shouldReceiveUpdates = isEnabled && !__DEV__
34
35 const appState = React.useRef<AppStateStatus>('active')
36 const lastMinimize = React.useRef(0)
37 const ranInitialCheck = React.useRef(false)
38 const timeout = React.useRef<NodeJS.Timeout>()
39 const {isUpdatePending} = useUpdates()
40
41 const setCheckTimeout = React.useCallback(() => {
42 timeout.current = setTimeout(async () => {
43 try {
44 await setExtraParams()
45
46 logger.debug('Checking for update...')
47 const res = await checkForUpdateAsync()
48
49 if (res.isAvailable) {
50 logger.debug('Attempting to fetch update...')
51 await fetchUpdateAsync()
52 } else {
53 logger.debug('No update available.')
54 }
55 } catch (e) {
56 logger.error('OTA Update Error', {error: `${e}`})
57 }
58 }, 10e3)
59 }, [])
60
61 const onIsTestFlight = React.useCallback(async () => {
62 try {
63 await setExtraParams()
64
65 const res = await checkForUpdateAsync()
66 if (res.isAvailable) {
67 await fetchUpdateAsync()
68
69 Alert.alert(
70 'Update Available',
71 'A new version of the app is available. Relaunch now?',
72 [
73 {
74 text: 'No',
75 style: 'cancel',
76 },
77 {
78 text: 'Relaunch',
79 style: 'default',
80 onPress: async () => {
81 await reloadAsync()
82 },
83 },
84 ],
85 )
86 }
87 } catch (e: any) {
88 logger.error('Internal OTA Update Error', {error: `${e}`})
89 }
90 }, [])
91
92 React.useEffect(() => {
93 // We use this setTimeout to allow Statsig to initialize before we check for an update
94 // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
95 // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
96 // immediately.
97 if (IS_TESTFLIGHT) {
98 onIsTestFlight()
99 return
100 } else if (!shouldReceiveUpdates || ranInitialCheck.current) {
101 return
102 }
103
104 setCheckTimeout()
105 ranInitialCheck.current = true
106 }, [onIsTestFlight, setCheckTimeout, shouldReceiveUpdates])
107
108 // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available
109 // or B check for an update again.
110 React.useEffect(() => {
111 if (!isEnabled) return
112
113 const subscription = AppState.addEventListener(
114 'change',
115 async nextAppState => {
116 if (
117 appState.current.match(/inactive|background/) &&
118 nextAppState === 'active'
119 ) {
120 // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
121 // chances are that there isn't anything important going on in the current session.
122 if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
123 if (isUpdatePending) {
124 await reloadAsync()
125 } else {
126 setCheckTimeout()
127 }
128 }
129 } else {
130 lastMinimize.current = Date.now()
131 }
132
133 appState.current = nextAppState
134 },
135 )
136
137 return () => {
138 clearTimeout(timeout.current)
139 subscription.remove()
140 }
141 }, [isUpdatePending, setCheckTimeout])
142}