Bluesky app fork with some witchin' additions 馃挮
at main 232 lines 7.1 kB view raw
1import React from 'react' 2import {Alert, AppState, type 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 {isNetworkError} from '#/lib/strings/errors' 14import {logger} from '#/logger' 15import {isAndroid, isIOS} from '#/platform/detection' 16import {IS_TESTFLIGHT} from '#/env' 17 18const MINIMUM_MINIMIZE_TIME = 15 * 60e3 19 20async function setExtraParams() { 21 await setExtraParamAsync( 22 isIOS ? 'ios-build-number' : 'android-build-number', 23 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. 24 // This just ensures it gets passed as a string 25 `${nativeBuildVersion}`, 26 ) 27 await setExtraParamAsync( 28 'channel', 29 IS_TESTFLIGHT ? 'testflight' : 'production', 30 ) 31} 32 33async function setExtraParamsPullRequest(channel: string) { 34 await setExtraParamAsync( 35 isIOS ? 'ios-build-number' : 'android-build-number', 36 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. 37 // This just ensures it gets passed as a string 38 `${nativeBuildVersion}`, 39 ) 40 await setExtraParamAsync('channel', channel) 41} 42 43async function updateTestflight() { 44 await setExtraParams() 45 46 const res = await checkForUpdateAsync() 47 if (res.isAvailable) { 48 await fetchUpdateAsync() 49 Alert.alert( 50 'Update Available', 51 'A new version of the app is available. Relaunch now?', 52 [ 53 { 54 text: 'No', 55 style: 'cancel', 56 }, 57 { 58 text: 'Relaunch', 59 style: 'default', 60 onPress: async () => { 61 await reloadAsync() 62 }, 63 }, 64 ], 65 ) 66 } 67} 68 69export function useApplyPullRequestOTAUpdate() { 70 const {currentlyRunning} = useUpdates() 71 const [pending, setPending] = React.useState(false) 72 const currentChannel = currentlyRunning?.channel 73 const isCurrentlyRunningPullRequestDeployment = 74 currentChannel?.startsWith('pull-request') 75 76 const tryApplyUpdate = async (channel: string) => { 77 setPending(true) 78 await setExtraParamsPullRequest(channel) 79 const res = await checkForUpdateAsync() 80 if (res.isAvailable) { 81 Alert.alert( 82 'Deployment Available', 83 `A deployment of ${channel} is availalble. Applying this deployment may result in a bricked installation, in which case you will need to reinstall the app and may lose local data. Are you sure you want to proceed?`, 84 [ 85 { 86 text: 'No', 87 style: 'cancel', 88 }, 89 { 90 text: 'Relaunch', 91 style: 'default', 92 onPress: async () => { 93 await fetchUpdateAsync() 94 await reloadAsync() 95 }, 96 }, 97 ], 98 ) 99 } else { 100 Alert.alert( 101 'No Deployment Available', 102 `No new deployments of ${channel} are currently available for your current native build.`, 103 ) 104 } 105 setPending(false) 106 } 107 108 const revertToEmbedded = async () => { 109 try { 110 await updateTestflight() 111 } catch (e: any) { 112 logger.error('Internal OTA Update Error', {error: `${e}`}) 113 } 114 } 115 116 return { 117 tryApplyUpdate, 118 revertToEmbedded, 119 isCurrentlyRunningPullRequestDeployment, 120 currentChannel, 121 pending, 122 } 123} 124 125export function useOTAUpdates() { 126 const shouldReceiveUpdates = isEnabled && !__DEV__ 127 128 const appState = React.useRef<AppStateStatus>('active') 129 const lastMinimize = React.useRef(0) 130 const ranInitialCheck = React.useRef(false) 131 const timeout = React.useRef<NodeJS.Timeout>(undefined) 132 const {currentlyRunning, isUpdatePending} = useUpdates() 133 const currentChannel = currentlyRunning?.channel 134 135 const setCheckTimeout = React.useCallback(() => { 136 timeout.current = setTimeout(async () => { 137 try { 138 await setExtraParams() 139 140 logger.debug('Checking for update...') 141 const res = await checkForUpdateAsync() 142 143 if (res.isAvailable) { 144 logger.debug('Attempting to fetch update...') 145 await fetchUpdateAsync() 146 } else { 147 logger.debug('No update available.') 148 } 149 } catch (err) { 150 if (!isNetworkError(err)) { 151 logger.error('OTA Update Error', {safeMessage: err}) 152 } 153 } 154 }, 10e3) 155 }, []) 156 157 const onIsTestFlight = React.useCallback(async () => { 158 try { 159 await updateTestflight() 160 } catch (err: any) { 161 if (!isNetworkError(err)) { 162 logger.error('Internal OTA Update Error', {safeMessage: err}) 163 } 164 } 165 }, []) 166 167 React.useEffect(() => { 168 // We don't need to check anything if the current update is a PR update 169 if (currentChannel?.startsWith('pull-request')) { 170 return 171 } 172 173 // We use this setTimeout to allow Statsig to initialize before we check for an update 174 // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This 175 // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update 176 // immediately. 177 if (IS_TESTFLIGHT) { 178 onIsTestFlight() 179 return 180 } else if (!shouldReceiveUpdates || ranInitialCheck.current) { 181 return 182 } 183 184 setCheckTimeout() 185 ranInitialCheck.current = true 186 }, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates]) 187 188 // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available 189 // or B check for an update again. 190 React.useEffect(() => { 191 // We also don't start this timeout if the user is on a pull request update 192 if (!isEnabled || currentChannel?.startsWith('pull-request')) { 193 return 194 } 195 196 // TEMP: disable wake-from-background OTA loading on Android. 197 // This is causing a crash when the thread view is open due to 198 // `maintainVisibleContentPosition`. See repro repo for more details: 199 // https://github.com/mozzius/ota-crash-repro 200 // Old Arch only - re-enable once we're on the New Archictecture! -sfn 201 if (isAndroid) return 202 203 const subscription = AppState.addEventListener( 204 'change', 205 async nextAppState => { 206 if ( 207 appState.current.match(/inactive|background/) && 208 nextAppState === 'active' 209 ) { 210 // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since 211 // chances are that there isn't anything important going on in the current session. 212 if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { 213 if (isUpdatePending) { 214 await reloadAsync() 215 } else { 216 setCheckTimeout() 217 } 218 } 219 } else { 220 lastMinimize.current = Date.now() 221 } 222 223 appState.current = nextAppState 224 }, 225 ) 226 227 return () => { 228 clearTimeout(timeout.current) 229 subscription.remove() 230 } 231 }, [isUpdatePending, currentChannel, setCheckTimeout]) 232}