Skip to content

Commit

Permalink
feat(cab): Add error banner for CAB delete failure (#5000)
Browse files Browse the repository at this point in the history
### Description

For
[ACT-767](https://linear.app/valora/issue/ACT-767/backup-deletion-loading-and-error-states).

### Test plan

Manual and unit tested. See video below. You can manually trigger this
to appear without code changes by just backing out of PIN entry after
pressing the delete button.



https://github.com/valora-inc/wallet/assets/569401/e70db41a-d4a4-49be-8bf4-ef61f0656bfa


### Related issues

- Fixes
[ACT-767](https://linear.app/valora/issue/ACT-767/backup-deletion-loading-and-error-states).

### Backwards compatibility

Yes.

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
jophish committed Mar 1, 2024
1 parent 6865c2b commit b2b7890
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 8 deletions.
4 changes: 3 additions & 1 deletion locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"enableDataSaver": "Enable Data Saver",
"dataSaverDetail": "Data Saver mode allows you to communicate with the the network through a trusted node. You can always change this mode in app settings.",
"keylessBackupSettingsTitle": "Email & Phone Recovery",
"keylessBackupSettingsDeleteError": "Your backup was not able to be deleted, please wait & try again later.",
"setup": "Set Up",
"restartModalSwitchOff": {
"header": "Restart To Switch Off Data Saver",
Expand Down Expand Up @@ -2132,5 +2133,6 @@
"description": "{{rewardName}} has successfully been added to your {{appName}} wallet."
}
},
"pleaseWait": "Please wait"
"pleaseWait": "Please wait",
"error": "Error"
}
22 changes: 21 additions & 1 deletion src/account/Settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from 'src/app/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { PRIVACY_LINK, TOS_LINK } from 'src/brandingConfig'
import { deleteKeylessBackupStarted } from 'src/keylessBackup/slice'
import { deleteKeylessBackupStarted, hideDeleteKeylessBackupError } from 'src/keylessBackup/slice'
import { KeylessBackupDeleteStatus } from 'src/keylessBackup/types'
import { ensurePincode, navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
Expand Down Expand Up @@ -351,6 +351,26 @@ describe('Account', () => {
expect(ValoraAnalytics.track).not.toHaveBeenCalled()
})

it('shows error banner when keyless backup delete fails', async () => {
jest.mocked(getFeatureGate).mockReturnValue(true)
const store = createMockStore({
account: { cloudBackupCompleted: true },
keylessBackup: { showDeleteBackupError: true },
})
const { getByTestId } = render(
<Provider store={store}>
<Settings {...getMockStackScreenProps(Screens.Settings)} />
</Provider>
)
expect(getByTestId('KeylessBackupDeleteError')).toBeTruthy()

await act(() => {
fireEvent.press(getByTestId('KeylessBackupDeleteError/dismiss'))
})

expect(store.getActions()).toContainEqual(hideDeleteKeylessBackupError())
})

it('can revoke the phone number successfully', async () => {
mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), {
status: 200,
Expand Down
25 changes: 23 additions & 2 deletions src/account/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ import { PRIVACY_LINK, TOS_LINK } from 'src/config'
import { currentLanguageSelector } from 'src/i18n/selectors'
import ForwardChevron from 'src/icons/ForwardChevron'
import LoadingSpinner from 'src/icons/LoadingSpinner'
import { deleteKeylessBackupStatusSelector } from 'src/keylessBackup/selectors'
import { deleteKeylessBackupStarted } from 'src/keylessBackup/slice'
import {
deleteKeylessBackupStatusSelector,
showDeleteKeylessBackupErrorSelector,
} from 'src/keylessBackup/selectors'
import { deleteKeylessBackupStarted, hideDeleteKeylessBackupError } from 'src/keylessBackup/slice'
import { KeylessBackupDeleteStatus } from 'src/keylessBackup/types'
import { getLocalCurrencyCode } from 'src/localCurrency/selectors'
import DrawerTopBar from 'src/navigator/DrawerTopBar'
Expand All @@ -80,6 +83,8 @@ import Logger from 'src/utils/Logger'
import { useRevokeCurrentPhoneNumber } from 'src/verify/hooks'
import { selectSessions } from 'src/walletConnect/selectors'
import { walletAddressSelector } from 'src/web3/selectors'
import InLineNotificationModal from 'src/components/InLineNotificationModal'
import { Severity } from 'src/components/InLineNotification'

type Props = NativeStackScreenProps<StackParamList, Screens.Settings>

Expand Down Expand Up @@ -109,6 +114,7 @@ export const Account = ({ navigation, route }: Props) => {
const currentLanguage = useSelector(currentLanguageSelector)
const cloudBackupCompleted = useSelector(cloudBackupCompletedSelector)
const deleteKeylessBackupStatus = useSelector(deleteKeylessBackupStatusSelector)
const showDeleteKeylessBackupError = useSelector(showDeleteKeylessBackupErrorSelector)
const walletConnectEnabled = v2
const connectedApplications = sessions.length

Expand All @@ -118,6 +124,10 @@ export const Account = ({ navigation, route }: Props) => {
}
}, [])

const onDismissKeylessBackupError = () => {
dispatch(hideDeleteKeylessBackupError())
}

const goToProfile = () => {
ValoraAnalytics.track(SettingsEvents.settings_profile_edit)
navigate(Screens.Profile)
Expand Down Expand Up @@ -542,6 +552,17 @@ export const Account = ({ navigation, route }: Props) => {
>
{t('promptConfirmRemovalModal.body')}
</Dialog>

<InLineNotificationModal
severity={Severity.Warning}
description={t('keylessBackupSettingsDeleteError')}
isVisible={showDeleteKeylessBackupError}
onDismiss={onDismissKeylessBackupError}
onPressCta={onDismissKeylessBackupError}
ctaLabel={t('dismiss')}
title={t('error')}
testID="KeylessBackupDeleteError"
/>
</ScrollView>

<RevokePhoneNumber forwardedRef={revokeBottomSheetRef} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/InLineNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function InLineNotification({
) =>
label &&
onPress && (
<Text style={[styles.ctaLabel, { color }]} onPress={onPress}>
<Text testID={`${testID}/${label}`} style={[styles.ctaLabel, { color }]} onPress={onPress}>
{label}
</Text>
)
Expand Down
57 changes: 57 additions & 0 deletions src/components/InLineNotificationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react'
import { StyleSheet } from 'react-native'
import Modal from 'src/components/Modal'
import { Spacing } from 'src/styles/styles'
import InLineNotification, { InLineNotificationProps } from 'src/components/InLineNotification'

type InLineNotificationModalProps = InLineNotificationProps & {
onDismiss: () => void
isVisible: boolean
}

export default function InLineNotificationModal({
severity,
title,
description,
style,
ctaLabel,
onPressCta,
ctaLabel2,
onPressCta2,
testID,
onDismiss,
isVisible,
}: InLineNotificationModalProps) {
return (
<Modal
modalStyle={styles.modalStyle}
style={styles.modalContentStyle}
isVisible={isVisible}
onModalHide={onDismiss}
onBackgroundPress={onDismiss}
>
<InLineNotification
severity={severity}
title={title}
description={description}
style={style}
ctaLabel={ctaLabel}
onPressCta={onPressCta}
ctaLabel2={ctaLabel2}
onPressCta2={onPressCta2}
testID={testID}
/>
</Modal>
)
}

const styles = StyleSheet.create({
modalStyle: {
justifyContent: 'flex-end',
marginHorizontal: Spacing.Regular16,
marginBottom: Spacing.XLarge48,
},
modalContentStyle: {
padding: 0,
},
})
3 changes: 3 additions & 0 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props {
children: React.ReactNode
isVisible: boolean
style?: StyleProp<ViewStyle>
modalStyle?: StyleProp<ViewStyle>
testID?: string
onBackgroundPress?: () => void
onModalHide?: () => void
Expand All @@ -18,6 +19,7 @@ export default function Modal({
children,
isVisible,
style,
modalStyle,
testID,
onBackgroundPress,
onModalHide,
Expand All @@ -27,6 +29,7 @@ export default function Modal({
return (
<ReactNativeModal
testID={testID}
style={modalStyle}
isVisible={isVisible}
backdropOpacity={0.1}
onBackdropPress={onBackgroundPress}
Expand Down
2 changes: 2 additions & 0 deletions src/keylessBackup/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export const keylessBackupStatusSelector = (state: RootState) => state.keylessBa
export const torusKeyshareSelector = (state: RootState) => state.keylessBackup.torusKeyshare
export const deleteKeylessBackupStatusSelector = (state: RootState) =>
state.keylessBackup.deleteBackupStatus
export const showDeleteKeylessBackupErrorSelector = (state: RootState) =>
state.keylessBackup.showDeleteBackupError
7 changes: 7 additions & 0 deletions src/keylessBackup/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface State {
torusKeyshare: string | null
backupStatus: KeylessBackupStatus
deleteBackupStatus: KeylessBackupDeleteStatus
showDeleteBackupError: boolean
}

export const initialState: State = {
Expand All @@ -19,6 +20,7 @@ export const initialState: State = {
torusKeyshare: null,
backupStatus: KeylessBackupStatus.NotStarted,
deleteBackupStatus: KeylessBackupDeleteStatus.NotStarted,
showDeleteBackupError: false,
}

export const slice = createSlice({
Expand Down Expand Up @@ -72,6 +74,10 @@ export const slice = createSlice({
},
deleteKeylessBackupFailed: (state) => {
state.deleteBackupStatus = KeylessBackupDeleteStatus.Failed
state.showDeleteBackupError = true
},
hideDeleteKeylessBackupError: (state) => {
state.showDeleteBackupError = false
},
},
})
Expand All @@ -90,6 +96,7 @@ export const {
deleteKeylessBackupStarted,
deleteKeylessBackupCompleted,
deleteKeylessBackupFailed,
hideDeleteKeylessBackupError,
} = slice.actions

export default slice.reducer
1 change: 1 addition & 0 deletions src/redux/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,4 +1594,5 @@ export const migrations = {
isEncryptingComment: false,
},
}),
195: (state: any) => state,
}
3 changes: 2 additions & 1 deletion src/redux/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('store state', () => {
{
"_persist": {
"rehydrated": true,
"version": 194,
"version": 195,
},
"account": {
"acceptedTerms": false,
Expand Down Expand Up @@ -264,6 +264,7 @@ describe('store state', () => {
"backupStatus": "NotStarted",
"deleteBackupStatus": "NotStarted",
"googleIdToken": null,
"showDeleteBackupError": false,
"torusKeyshare": null,
"valoraKeyshare": null,
},
Expand Down
2 changes: 1 addition & 1 deletion src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const persistConfig: PersistConfig<RootState> = {
key: 'root',
// default is -1, increment as we make migrations
// See https://github.com/valora-inc/wallet/tree/main/WALLET.md#redux-state-migration
version: 194,
version: 195,
keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
storage: FSStorage(),
blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup'],
Expand Down
4 changes: 4 additions & 0 deletions test/RootStateSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4008,6 +4008,9 @@
"string"
]
},
"showDeleteBackupError": {
"type": "boolean"
},
"torusKeyshare": {
"type": [
"null",
Expand All @@ -4025,6 +4028,7 @@
"backupStatus",
"deleteBackupStatus",
"googleIdToken",
"showDeleteBackupError",
"torusKeyshare",
"valoraKeyshare"
],
Expand Down
14 changes: 13 additions & 1 deletion test/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3098,6 +3098,18 @@ export const v194Schema = {
},
}

export const v195Schema = {
...v194Schema,
_persist: {
...v194Schema._persist,
version: 195,
},
keylessBackup: {
...v194Schema.keylessBackup,
showDeleteBackupError: false,
},
}

export function getLatestSchema(): Partial<RootState> {
return v194Schema as Partial<RootState>
return v195Schema as Partial<RootState>
}

0 comments on commit b2b7890

Please sign in to comment.