Add OTA updates support for testflight
channel (#3291)
* some progress another adjustment, testing another adjustment, testing fix again fix again set default runtime version fix test this script test this script test this script add build numbers to the deployment url clean give script access to build number add `useBuildNumberEnv` without a bump new line fix missing async add channel name to deployment url add updates check on launch for testflight users ver bump init updates on launch for native add `testflight` as default in build submit add is_testflight check * disable inline predictions to prevent ios composer jank * temp bump * Revert "temp bump" This reverts commit 44c51134a35d817c73edb1e635495597c95117b3. * adjustments version bump adjust fixes test * cleanup and finalize drop check down to every 15 minutes adjustments change to 15 mins use jq to get version if necessary rm test on push figured it out remove nightly testflight releases test again again again again again AGAIN ONCE MORE test again again again again again AGAIN test again again again again again AGAIN test again again again again again test again again again again test again again again test again again test again test test test run deploy if necessary run deploy if necessary run deploy if necessary run deploy if necessary run deploy if necessary remove test message fix environment oops cleanup merge in changes * remove unnecessary `workflow_call` * remove changes that have been merged into main now * finalize android update git ignore rm test stuff from the bundle action remove test message test message fix test message test message few android fixes few android fixes fix jq add a test message fix slack webhook create android deployments test 2 create android deployments add `testflight-android` profile to eas.json more cleanup some more cleanup simplify some logic remove unnecessary channel rename to `useOTAUpdates` * rm test portion
This commit is contained in:
parent
02b2ab4f1f
commit
73df7e53b3
21 changed files with 589 additions and 156 deletions
142
src/lib/hooks/useOTAUpdates.ts
Normal file
142
src/lib/hooks/useOTAUpdates.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import React from 'react'
|
||||
import {Alert, AppState, AppStateStatus} from 'react-native'
|
||||
import app from 'react-native-version-number'
|
||||
import {
|
||||
checkForUpdateAsync,
|
||||
fetchUpdateAsync,
|
||||
isEnabled,
|
||||
reloadAsync,
|
||||
setExtraParamAsync,
|
||||
useUpdates,
|
||||
} from 'expo-updates'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {IS_TESTFLIGHT} from 'lib/app-info'
|
||||
import {isIOS} from 'platform/detection'
|
||||
|
||||
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
|
||||
|
||||
async function setExtraParams() {
|
||||
await setExtraParamAsync(
|
||||
isIOS ? 'ios-build-number' : 'android-build-number',
|
||||
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
|
||||
// This just ensures it gets passed as a string
|
||||
`${app.buildVersion}`,
|
||||
)
|
||||
await setExtraParamAsync(
|
||||
'channel',
|
||||
IS_TESTFLIGHT ? 'testflight' : 'production',
|
||||
)
|
||||
}
|
||||
|
||||
export function useOTAUpdates() {
|
||||
const appState = React.useRef<AppStateStatus>('active')
|
||||
const lastMinimize = React.useRef(0)
|
||||
const ranInitialCheck = React.useRef(false)
|
||||
const timeout = React.useRef<NodeJS.Timeout>()
|
||||
const {isUpdatePending} = useUpdates()
|
||||
|
||||
const setCheckTimeout = React.useCallback(() => {
|
||||
timeout.current = setTimeout(async () => {
|
||||
try {
|
||||
await setExtraParams()
|
||||
|
||||
logger.debug('Checking for update...')
|
||||
const res = await checkForUpdateAsync()
|
||||
|
||||
if (res.isAvailable) {
|
||||
logger.debug('Attempting to fetch update...')
|
||||
await fetchUpdateAsync()
|
||||
} else {
|
||||
logger.debug('No update available.')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('OTA Update Error', {error: `${e}`})
|
||||
}
|
||||
}, 10e3)
|
||||
}, [])
|
||||
|
||||
const onIsTestFlight = React.useCallback(() => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await setExtraParams()
|
||||
|
||||
const res = await checkForUpdateAsync()
|
||||
if (res.isAvailable) {
|
||||
await fetchUpdateAsync()
|
||||
|
||||
Alert.alert(
|
||||
'Update Available',
|
||||
'A new version of the app is available. Relaunch now?',
|
||||
[
|
||||
{
|
||||
text: 'No',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Relaunch',
|
||||
style: 'default',
|
||||
onPress: async () => {
|
||||
await reloadAsync()
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
// No need to handle
|
||||
}
|
||||
}, 3e3)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
|
||||
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
|
||||
// immediately.
|
||||
if (IS_TESTFLIGHT) {
|
||||
onIsTestFlight()
|
||||
return
|
||||
} else if (!isEnabled || __DEV__ || ranInitialCheck.current) {
|
||||
// Development client shouldn't check for updates at all, so we skip that here.
|
||||
return
|
||||
}
|
||||
|
||||
setCheckTimeout()
|
||||
ranInitialCheck.current = true
|
||||
}, [onIsTestFlight, setCheckTimeout])
|
||||
|
||||
// After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available
|
||||
// or B check for an update again.
|
||||
React.useEffect(() => {
|
||||
if (!isEnabled) return
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
'change',
|
||||
async nextAppState => {
|
||||
if (
|
||||
appState.current.match(/inactive|background/) &&
|
||||
nextAppState === 'active'
|
||||
) {
|
||||
// If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
|
||||
// chances are that there isn't anything important going on in the current session.
|
||||
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
|
||||
if (isUpdatePending) {
|
||||
await reloadAsync()
|
||||
} else {
|
||||
setCheckTimeout()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastMinimize.current = Date.now()
|
||||
}
|
||||
|
||||
appState.current = nextAppState
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout.current)
|
||||
subscription.remove()
|
||||
}
|
||||
}, [isUpdatePending, setCheckTimeout])
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue