Skip to content

Commit ee4191d

Browse files
author
Jon Tzeng
committed
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.
1 parent 55d511c commit ee4191d

4 files changed

Lines changed: 76 additions & 10 deletions

File tree

src/components/scenes/GiftCardAccountInfoScene.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Clipboard from '@react-native-clipboard/clipboard'
2-
import { useQuery } from '@tanstack/react-query'
2+
import { useQuery, useQueryClient } from '@tanstack/react-query'
33
import * as React from 'react'
44
import { View } from 'react-native'
55

@@ -35,6 +35,7 @@ export const GiftCardAccountInfoScene: React.FC<
3535
const styles = getStyles(theme)
3636

3737
const account = useSelector(state => state.core.account)
38+
const queryClient = useQueryClient()
3839

3940
// Provider for identity lookup
4041
const phazeConfig = (ENV.PLUGIN_API_KEYS as Record<string, unknown>)
@@ -70,9 +71,23 @@ export const GiftCardAccountInfoScene: React.FC<
7071
onPress={async () => true}
7172
/>
7273
))
73-
if (confirmed) {
74-
setIsRevealed(true)
74+
if (!confirmed) return
75+
if (provider == null) return
76+
77+
// Rotation must succeed before revealing the old email so the exposed
78+
// identity is never re-used for future purchases.
79+
try {
80+
await provider.rotateIdentity(account)
81+
await queryClient.invalidateQueries({
82+
queryKey: ['phazeProvider']
83+
})
84+
} catch (err: unknown) {
85+
showError(err)
86+
return
7587
}
88+
89+
setIsRevealed(true)
90+
showToast(lstrings.gift_card_account_info_rotated)
7691
})
7792

7893
const handleCopyAll = useHandler(async () => {

src/locales/en_US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,6 +1974,8 @@ const strings = {
19741974
gift_card_account_info_warning:
19751975
'Anyone with access to this information may be able to redeem your unredeemed gift cards. Do not share this publicly.',
19761976
gift_card_account_info_email: 'Account Email',
1977+
gift_card_account_info_rotated:
1978+
'A new identity has been created for future purchases.',
19771979
gift_card_account_info_user_id: 'Phaze User ID',
19781980
gift_card_pending: 'Pending Delivery, Please Wait...',
19791981
gift_card_pending_toast:

src/locales/strings/enUS.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,7 @@
15341534
"gift_card_account_info_reveal_button": "View Phaze Account Info",
15351535
"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.",
15361536
"gift_card_account_info_email": "Account Email",
1537+
"gift_card_account_info_rotated": "A new identity has been created for future purchases.",
15371538
"gift_card_account_info_user_id": "Phaze User ID",
15381539
"gift_card_pending": "Pending Delivery, Please Wait...",
15391540
"gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.",

src/plugins/gift-cards/phazeGiftCardProvider.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { asMaybe, asNumber, asObject, asOptional, asString } from 'cleaners'
1+
import {
2+
asDate,
3+
asMaybe,
4+
asNumber,
5+
asObject,
6+
asOptional,
7+
asString
8+
} from 'cleaners'
29
import type { EdgeAccount } from 'edge-core-js'
310

411
import { debugLog } from '../../util/logger'
@@ -47,6 +54,7 @@ export const hasStoredPhazeIdentity = async (
4754
// Cleaner for individual identity storage (PhazeUser fields + uniqueId)
4855
interface StoredIdentity extends PhazeUser {
4956
uniqueId: string
57+
createdDate?: Date
5058
}
5159
const asStoredIdentity = asObject({
5260
id: asNumber,
@@ -56,7 +64,8 @@ const asStoredIdentity = asObject({
5664
userApiKey: asOptional(asString),
5765
balance: asString,
5866
balanceCurrency: asString,
59-
uniqueId: asString
67+
uniqueId: asString,
68+
createdDate: asOptional(asDate)
6069
})
6170

6271
/**
@@ -84,6 +93,12 @@ export interface PhazeGiftCardProvider {
8493
*/
8594
listIdentities: (account: EdgeAccount) => Promise<PhazeUser[]>
8695

96+
/**
97+
* Create a new Phaze identity and set it as the active identity for
98+
* future purchases. Old identities are kept for order history.
99+
*/
100+
rotateIdentity: (account: EdgeAccount) => Promise<void>
101+
87102
/** Get underlying API instance (for direct API calls) */
88103
getApi: () => PhazeApi
89104

@@ -277,13 +292,18 @@ export const makePhazeGiftCardProvider = (
277292
debugLog('phaze', 'Failed to pre-fetch FX rates:', err)
278293
})
279294

280-
// Check for existing identities. Uses the first identity found for purchases/orders.
281-
// Multiple identities is an edge case (multi-device before sync completes) -
282-
// new orders simply go to whichever identity is active.
295+
// Check for existing identities. Prefer the newest identity (by
296+
// createdDate) so that rotated identities take priority. Identities
297+
// without createdDate are treated as oldest for backward compat.
283298
// Order VIEWING aggregates all identities via getAllOrdersFromAllIdentities().
284299
const identities = await loadIdentities(account)
300+
const sorted = [...identities].sort((a, b) => {
301+
const timeA = a.createdDate?.valueOf() ?? 0
302+
const timeB = b.createdDate?.valueOf() ?? 0
303+
return timeB - timeA
304+
})
285305

286-
for (const identity of identities) {
306+
for (const identity of sorted) {
287307
if (identity.userApiKey != null) {
288308
api.setUserApiKey(identity.userApiKey)
289309
debugLog('phaze', 'Using existing identity:', identity.uniqueId)
@@ -320,7 +340,8 @@ export const makePhazeGiftCardProvider = (
320340
// Save identity to its own unique key in encrypted dataStore
321341
const newIdentity: StoredIdentity = {
322342
...response.data,
323-
uniqueId
343+
uniqueId,
344+
createdDate: new Date()
324345
}
325346
await saveIdentity(account, newIdentity)
326347

@@ -338,6 +359,33 @@ export const makePhazeGiftCardProvider = (
338359
return await loadIdentities(account)
339360
},
340361

362+
async rotateIdentity(account) {
363+
const uniqueId = await makeUuid()
364+
const email = `${uniqueId}@edge.app`
365+
const firstName = 'Edgeuser'
366+
const lastName = account.username ?? 'User'
367+
368+
const response = await api.registerUser({
369+
email,
370+
firstName,
371+
lastName
372+
})
373+
374+
const userApiKey = response.data.userApiKey
375+
if (userApiKey == null) {
376+
throw new Error('Identity rotation failed: no userApiKey returned')
377+
}
378+
379+
const newIdentity: StoredIdentity = {
380+
...response.data,
381+
uniqueId,
382+
createdDate: new Date()
383+
}
384+
await saveIdentity(account, newIdentity)
385+
api.setUserApiKey(userApiKey)
386+
debugLog('phaze', 'Rotated identity, new uniqueId:', uniqueId)
387+
},
388+
341389
getApi: () => api,
342390
getCache: () => cache,
343391

0 commit comments

Comments
 (0)