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