From 157e2eea0355f52f80db74ebdf93b402767c8f35 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 24 Feb 2026 12:08:53 -0800 Subject: [PATCH] Auto-rotate Phaze identity when account info is revealed When a user reveals their Phaze email via the Gift Card Account Info scene, automatically create a new identity for future purchases. This prevents a malicious actor who sees the revealed email from intercepting new gift card orders. Old identities are preserved for order history. --- .../scenes/GiftCardAccountInfoScene.tsx | 21 ++++++- src/locales/en_US.ts | 2 + src/locales/strings/enUS.json | 1 + .../gift-cards/phazeGiftCardProvider.ts | 62 ++++++++++++++++--- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/components/scenes/GiftCardAccountInfoScene.tsx b/src/components/scenes/GiftCardAccountInfoScene.tsx index 85e7976bb8e..439598f84f7 100644 --- a/src/components/scenes/GiftCardAccountInfoScene.tsx +++ b/src/components/scenes/GiftCardAccountInfoScene.tsx @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { View } from 'react-native' @@ -35,6 +35,7 @@ export const GiftCardAccountInfoScene: React.FC< const styles = getStyles(theme) const account = useSelector(state => state.core.account) + const queryClient = useQueryClient() // Provider for identity lookup const phazeConfig = (ENV.PLUGIN_API_KEYS as Record) @@ -70,9 +71,23 @@ export const GiftCardAccountInfoScene: React.FC< onPress={async () => true} /> )) - if (confirmed) { - setIsRevealed(true) + if (!confirmed) return + if (provider == null) return + + // Rotation must succeed before revealing the old email so the exposed + // identity is never re-used for future purchases. + try { + await provider.rotateIdentity(account) + await queryClient.invalidateQueries({ + queryKey: ['phazeProvider'] + }) + } catch (err: unknown) { + showError(err) + return } + + setIsRevealed(true) + showToast(lstrings.gift_card_account_info_rotated) }) const handleCopyAll = useHandler(async () => { diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index a077ddaacb1..5f6b1d5a827 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1974,6 +1974,8 @@ const strings = { gift_card_account_info_warning: 'Anyone with access to this information may be able to redeem your unredeemed gift cards. Do not share this publicly.', gift_card_account_info_email: 'Account Email', + gift_card_account_info_rotated: + 'A new identity has been created for future purchases.', gift_card_account_info_user_id: 'Phaze User ID', gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 31c7722d9d1..2ce36adcf86 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1534,6 +1534,7 @@ "gift_card_account_info_reveal_button": "View Phaze Account Info", "gift_card_account_info_warning": "Anyone with access to this information may be able to redeem your unredeemed gift cards. Do not share this publicly.", "gift_card_account_info_email": "Account Email", + "gift_card_account_info_rotated": "A new identity has been created for future purchases.", "gift_card_account_info_user_id": "Phaze User ID", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", diff --git a/src/plugins/gift-cards/phazeGiftCardProvider.ts b/src/plugins/gift-cards/phazeGiftCardProvider.ts index e7410b7c378..9bb607381e6 100644 --- a/src/plugins/gift-cards/phazeGiftCardProvider.ts +++ b/src/plugins/gift-cards/phazeGiftCardProvider.ts @@ -1,4 +1,11 @@ -import { asMaybe, asNumber, asObject, asOptional, asString } from 'cleaners' +import { + asDate, + asMaybe, + asNumber, + asObject, + asOptional, + asString +} from 'cleaners' import type { EdgeAccount } from 'edge-core-js' import { debugLog } from '../../util/logger' @@ -47,6 +54,7 @@ export const hasStoredPhazeIdentity = async ( // Cleaner for individual identity storage (PhazeUser fields + uniqueId) interface StoredIdentity extends PhazeUser { uniqueId: string + createdDate?: Date } const asStoredIdentity = asObject({ id: asNumber, @@ -56,7 +64,8 @@ const asStoredIdentity = asObject({ userApiKey: asOptional(asString), balance: asString, balanceCurrency: asString, - uniqueId: asString + uniqueId: asString, + createdDate: asOptional(asDate) }) /** @@ -84,6 +93,12 @@ export interface PhazeGiftCardProvider { */ listIdentities: (account: EdgeAccount) => Promise + /** + * Create a new Phaze identity and set it as the active identity for + * future purchases. Old identities are kept for order history. + */ + rotateIdentity: (account: EdgeAccount) => Promise + /** Get underlying API instance (for direct API calls) */ getApi: () => PhazeApi @@ -277,13 +292,18 @@ export const makePhazeGiftCardProvider = ( debugLog('phaze', 'Failed to pre-fetch FX rates:', err) }) - // Check for existing identities. Uses the first identity found for purchases/orders. - // Multiple identities is an edge case (multi-device before sync completes) - - // new orders simply go to whichever identity is active. + // Check for existing identities. Prefer the newest identity (by + // createdDate) so that rotated identities take priority. Identities + // without createdDate are treated as oldest for backward compat. // Order VIEWING aggregates all identities via getAllOrdersFromAllIdentities(). const identities = await loadIdentities(account) + const sorted = [...identities].sort((a, b) => { + const timeA = a.createdDate?.valueOf() ?? 0 + const timeB = b.createdDate?.valueOf() ?? 0 + return timeB - timeA + }) - for (const identity of identities) { + for (const identity of sorted) { if (identity.userApiKey != null) { api.setUserApiKey(identity.userApiKey) debugLog('phaze', 'Using existing identity:', identity.uniqueId) @@ -320,7 +340,8 @@ export const makePhazeGiftCardProvider = ( // Save identity to its own unique key in encrypted dataStore const newIdentity: StoredIdentity = { ...response.data, - uniqueId + uniqueId, + createdDate: new Date() } await saveIdentity(account, newIdentity) @@ -338,6 +359,33 @@ export const makePhazeGiftCardProvider = ( return await loadIdentities(account) }, + async rotateIdentity(account) { + const uniqueId = await makeUuid() + const email = `${uniqueId}@edge.app` + const firstName = 'Edgeuser' + const lastName = account.username ?? 'User' + + const response = await api.registerUser({ + email, + firstName, + lastName + }) + + const userApiKey = response.data.userApiKey + if (userApiKey == null) { + throw new Error('Identity rotation failed: no userApiKey returned') + } + + const newIdentity: StoredIdentity = { + ...response.data, + uniqueId, + createdDate: new Date() + } + await saveIdentity(account, newIdentity) + api.setUserApiKey(userApiKey) + debugLog('phaze', 'Rotated identity, new uniqueId:', uniqueId) + }, + getApi: () => api, getCache: () => cache,