Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/components/scenes/GiftCardAccountInfoScene.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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<string, unknown>)
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/locales/strings/enUS.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
62 changes: 55 additions & 7 deletions src/plugins/gift-cards/phazeGiftCardProvider.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -56,7 +64,8 @@ const asStoredIdentity = asObject({
userApiKey: asOptional(asString),
balance: asString,
balanceCurrency: asString,
uniqueId: asString
uniqueId: asString,
createdDate: asOptional(asDate)
})

/**
Expand Down Expand Up @@ -84,6 +93,12 @@ export interface PhazeGiftCardProvider {
*/
listIdentities: (account: EdgeAccount) => Promise<PhazeUser[]>

/**
* 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<void>

/** Get underlying API instance (for direct API calls) */
getApi: () => PhazeApi

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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,

Expand Down
Loading