add client ability to apply an update
This commit is contained in:
@@ -190,7 +190,6 @@ module.exports = function (_config) {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
checkAutomatically: 'NEVER',
|
checkAutomatically: 'NEVER',
|
||||||
channel: UPDATES_CHANNEL,
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
'expo-video',
|
'expo-video',
|
||||||
|
|||||||
+137
-29
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Alert, AppState, AppStateStatus} from 'react-native'
|
import {Alert, AppState, type AppStateStatus} from 'react-native'
|
||||||
import {nativeBuildVersion} from 'expo-application'
|
import {nativeBuildVersion} from 'expo-application'
|
||||||
import {
|
import {
|
||||||
checkForUpdateAsync,
|
checkForUpdateAsync,
|
||||||
@@ -29,6 +29,128 @@ async function setExtraParams() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setExtraParamsPullRequest(channel: string) {
|
||||||
|
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
|
||||||
|
`${nativeBuildVersion}`,
|
||||||
|
)
|
||||||
|
await setExtraParamAsync('channel', channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTestflight() {
|
||||||
|
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()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplyPullRequestOTAUpdate() {
|
||||||
|
const {currentlyRunning} = useUpdates()
|
||||||
|
const [pending, setPending] = React.useState(false)
|
||||||
|
const currentChannel = currentlyRunning?.channel
|
||||||
|
const isCurrentlyRunningPullRequestDeployment =
|
||||||
|
currentChannel?.startsWith('pull-request')
|
||||||
|
|
||||||
|
const onTryApplyUpdate = async (channel: string) => {
|
||||||
|
setPending(true)
|
||||||
|
if (currentChannel === channel) {
|
||||||
|
const res = await checkForUpdateAsync()
|
||||||
|
if (res.isAvailable) {
|
||||||
|
logger.debug('Attempting to fetch update...')
|
||||||
|
await fetchUpdateAsync()
|
||||||
|
Alert.alert(
|
||||||
|
'Deployment Availalbe',
|
||||||
|
`A new deployment of ${channel} is availalble. Relaunch now?`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'No',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Relaunch',
|
||||||
|
style: 'default',
|
||||||
|
onPress: async () => {
|
||||||
|
await reloadAsync()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'No Deployment Availalbe',
|
||||||
|
`No new deployments of ${channel} are currently available for your current native build.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setExtraParamsPullRequest(channel)
|
||||||
|
const res = await checkForUpdateAsync()
|
||||||
|
if (res.isAvailable) {
|
||||||
|
Alert.alert(
|
||||||
|
'Deployment Availalbe',
|
||||||
|
`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?`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'No',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Relaunch',
|
||||||
|
style: 'default',
|
||||||
|
onPress: async () => {
|
||||||
|
await reloadAsync()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'No Deployment Availalbe',
|
||||||
|
`No new deployments of ${channel} are currently available for your current native build.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPending(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRevertToEmbedded = async () => {
|
||||||
|
try {
|
||||||
|
await updateTestflight()
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error('Internal OTA Update Error', {error: `${e}`})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onTryApplyUpdate,
|
||||||
|
onRevertToEmbedded,
|
||||||
|
currentChannel,
|
||||||
|
isCurrentlyRunningPullRequestDeployment,
|
||||||
|
pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useOTAUpdates() {
|
export function useOTAUpdates() {
|
||||||
const shouldReceiveUpdates = isEnabled && !__DEV__
|
const shouldReceiveUpdates = isEnabled && !__DEV__
|
||||||
|
|
||||||
@@ -36,7 +158,8 @@ export function useOTAUpdates() {
|
|||||||
const lastMinimize = React.useRef(0)
|
const lastMinimize = React.useRef(0)
|
||||||
const ranInitialCheck = React.useRef(false)
|
const ranInitialCheck = React.useRef(false)
|
||||||
const timeout = React.useRef<NodeJS.Timeout>()
|
const timeout = React.useRef<NodeJS.Timeout>()
|
||||||
const {isUpdatePending} = useUpdates()
|
const {currentlyRunning, isUpdatePending} = useUpdates()
|
||||||
|
const currentChannel = currentlyRunning?.channel
|
||||||
|
|
||||||
const setCheckTimeout = React.useCallback(() => {
|
const setCheckTimeout = React.useCallback(() => {
|
||||||
timeout.current = setTimeout(async () => {
|
timeout.current = setTimeout(async () => {
|
||||||
@@ -60,36 +183,18 @@ export function useOTAUpdates() {
|
|||||||
|
|
||||||
const onIsTestFlight = React.useCallback(async () => {
|
const onIsTestFlight = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await setExtraParams()
|
await updateTestflight()
|
||||||
|
|
||||||
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) {
|
} catch (e: any) {
|
||||||
logger.error('Internal OTA Update Error', {error: `${e}`})
|
logger.error('Internal OTA Update Error', {error: `${e}`})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// We don't need to check anything if the curernt update is a PR update
|
||||||
|
if (currentChannel?.startsWith('pull-request')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// We use this setTimeout to allow Statsig to initialize before we check for an update
|
// We use this setTimeout to allow Statsig to initialize before we check for an update
|
||||||
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
|
// 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
|
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
|
||||||
@@ -103,12 +208,15 @@ export function useOTAUpdates() {
|
|||||||
|
|
||||||
setCheckTimeout()
|
setCheckTimeout()
|
||||||
ranInitialCheck.current = true
|
ranInitialCheck.current = true
|
||||||
}, [onIsTestFlight, setCheckTimeout, shouldReceiveUpdates])
|
}, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates])
|
||||||
|
|
||||||
// After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available
|
// After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available
|
||||||
// or B check for an update again.
|
// or B check for an update again.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isEnabled) return
|
// We also don't start this timeout if the user is on a pull request update
|
||||||
|
if (!isEnabled || currentChannel?.startsWith('pull-request')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const subscription = AppState.addEventListener(
|
const subscription = AppState.addEventListener(
|
||||||
'change',
|
'change',
|
||||||
@@ -138,5 +246,5 @@ export function useOTAUpdates() {
|
|||||||
clearTimeout(timeout.current)
|
clearTimeout(timeout.current)
|
||||||
subscription.remove()
|
subscription.remove()
|
||||||
}
|
}
|
||||||
}, [isUpdatePending, setCheckTimeout])
|
}, [isUpdatePending, currentChannel, setCheckTimeout])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {LayoutAnimation, Pressable, View} from 'react-native'
|
import {Alert, LayoutAnimation, Pressable, View} from 'react-native'
|
||||||
import {Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
import {useReducedMotion} from 'react-native-reanimated'
|
import {useReducedMotion} from 'react-native-reanimated'
|
||||||
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
@@ -12,12 +12,14 @@ import {useActorStatus} from '#/lib/actor-status'
|
|||||||
import {IS_INTERNAL} from '#/lib/app-info'
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {HELP_DESK_URL} from '#/lib/constants'
|
import {HELP_DESK_URL} from '#/lib/constants'
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
|
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NavigationProp,
|
type NavigationProp,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {clearStorage} from '#/state/persisted'
|
import {clearStorage} from '#/state/persisted'
|
||||||
@@ -392,6 +394,41 @@ function DevOptions() {
|
|||||||
setActyNotifNudged(false)
|
setActyNotifNudged(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
onTryApplyUpdate,
|
||||||
|
onRevertToEmbedded,
|
||||||
|
isCurrentlyRunningPullRequestDeployment,
|
||||||
|
currentChannel,
|
||||||
|
} = useApplyPullRequestOTAUpdate()
|
||||||
|
|
||||||
|
const onPressApplyPullRequest = async () => {
|
||||||
|
Alert.prompt(
|
||||||
|
'Apply Pull Request',
|
||||||
|
"Enter a channel name below to apply a pull request. Channel names usually look like 'pull-request-<PULL REQUEST ID>'",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Apply',
|
||||||
|
style: 'default',
|
||||||
|
onPress: async channel => {
|
||||||
|
if (!channel) {
|
||||||
|
Alert.alert('Error', 'No channel provided to look for.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await onTryApplyUpdate(channel ?? '')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plain-text',
|
||||||
|
isCurrentlyRunningPullRequestDeployment
|
||||||
|
? currentChannel
|
||||||
|
: 'pull-request-',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
@@ -452,6 +489,24 @@ function DevOptions() {
|
|||||||
<Trans>Clear all storage data (restart after this)</Trans>
|
<Trans>Clear all storage data (restart after this)</Trans>
|
||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
|
{isNative ? (
|
||||||
|
<SettingsList.PressableItem
|
||||||
|
onPress={onPressApplyPullRequest}
|
||||||
|
label={_(msg`Apply Pull Request`)}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Apply Pull Request</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
</SettingsList.PressableItem>
|
||||||
|
) : null}
|
||||||
|
{isNative && isCurrentlyRunningPullRequestDeployment ? (
|
||||||
|
<SettingsList.PressableItem
|
||||||
|
onPress={onRevertToEmbedded}
|
||||||
|
label={_(msg`Unapply Pull Request`)}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Unapply Pull Request {currentChannel}</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
</SettingsList.PressableItem>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user