Skip to content

Conversation

@Jon-edge
Copy link
Collaborator

@Jon-edge Jon-edge commented Dec 18, 2025

Phaze Gift Card Integration Design

image image image image image image

Overview

The Phaze gift card feature allows Edge users to purchase gift cards using cryptocurrency. The integration consists of:

  1. Gift Card Market - Browse available brands
  2. Gift Card Purchase - Select denomination and pay with crypto
  3. Gift Card List - View purchased cards with voucher codes
  4. Order Polling - Background service to detect completed orders

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                              UI Layer                                   │
├─────────────────┬─────────────────┬─────────────────────────────────────┤
│ GiftCardMarket  │ GiftCardPurchase│ GiftCardListScene                   │
│ Scene           │ Scene           │ (+ GiftCardDisplayCard)             │
└────────┬────────┴────────┬────────┴──────────────┬──────────────────────┘
         │                 │                       │
         └─────────────────┼───────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        PhazeGiftCardProvider                            │
│  - Brand fetching (market, full details)                                │
│  - Order management (create, status)                                    │
│  - Identity management (user registration)                              │
│  - Cache access via getCache()                                          │
└─────────────────────────┬───────────────────────────────────────────────┘
                          │
         ┌────────────────┼────────────────┐
         │                │                │
         ▼                ▼                ▼
┌─────────────────┐ ┌───────────────┐ ┌────────────────────────────────────┐
│   PhazeApi      │ │ PhazeGiftCard │ │ PhazeGiftCardOrderStore            │
│  (HTTP calls)   │ │   Cache       │ │ (Order augments: txid, brand info) │
└─────────────────┘ └───────────────┘ └────────────────────────────────────┘
                          │
         ┌────────────────┼────────────────┐
         │ In-Memory      │ Disk Persistence│
         │ (Map<country,  │ (localDisklet)  │
         │  brands>)      │                 │
         └────────────────┴─────────────────┘

Data Storage

Brand Cache (PhazeGiftCardCache)

Purpose: Cache gift card brand data to minimize API calls and provide offline access.

Storage Location: account.localDisklet (device-local, not synced)

  • File: phazeGiftCards/brands-{countryCode}.json

Why localDisklet?

  • No need to sync brand cache across devices, this is just mirroring the API data locally.

Cache Structure:

// In-memory (module-level singleton)
Map<countryCode, {
  timestamp: number
  brandsByProductId: Map<productId, PhazeGiftCardBrand>
  fullDetailProductIds: Set<productId>  // Brands with full data
}>

// Disk format
{
  version: 2,
  timestamp: number,
  countryCode: string,
  brands: PhazeGiftCardBrand[],
  fullDetailProductIds: number[]
}

TTL Policy:

  • In-memory: 1 hour (triggers API refresh)
  • Disk: 24 hours (offline fallback)

Cache Flow:

Startup:
  loadFromDisk() → populate memory cache

UI reads:
  getBrands(countryCode) → returns from memory (fast)
  getBrand(countryCode, productId) → single brand lookup

API refresh:
  getMarketBrands() → setBrands() + saveToDisk()

Order "Augments" (PhazeGiftCardOrderStore)

Purpose: Store local metadata that associates blockchain transactions with gift card orders, and Edge-specific metadata like redeemed date. Also includes brand information like image uri for quick access without API dependencies.

Storage Location: account.disklet (synced across devices)

  • File: phazeGiftCardAugments/{orderId}.json

Why synced disklet?

  • User may purchase on one device, view on another
  • Transaction metadata must persist

Augment Data:

{
  walletId: string      // Wallet that sent payment
  tokenId?: string      // Token used (null for native)
  txid: string          // Blockchain transaction ID
  brandName: string     // For display before API responds
  brandImage: string    // For display before API responds
  fiatAmount: number    // Purchase amount
  fiatCurrency: string  // Currency code
  redeemedDate?: string // ISO date when marked redeemed
}

User Identities

Storage Location: account.disklet (synced)

  • File: phaze-identity-{uuid}.json

Contents: Phaze user registration data including userApiKey

Multiple Identities: There should typically only be one identity in most cases. An edge case that can occur from multi-device usage before sync completes. Handled as follows:

  • Ordering: Uses first identity found (new orders go to active identity)
  • Viewing orders: Aggregates all identities via getAllOrdersFromAllIdentities()
  • Polling: Iterates all identities to ensure voucher updates for all orders

API Integration

Brand Fetching Strategy

  1. Market Display (getMarketBrands):

    • Fetches minimal fields for fast loading
    • Stores in cache, persists to disk
  2. Full Details (getBrandDetails):

    • Fetches complete data when user taps a brand
    • Stores with fullDetailProductIds flag
    • Prevents re-fetching on subsequent visits
  3. Background Prefetch:

    • Market scene fetches full details in background
    • Purchase scene has data ready immediately

Order Status Flow

  1. Order Creation:

    • Provider creates order with Phaze API
    • Returns quote with payment address
  2. Payment Broadcast:

    • Edge wallet broadcasts transaction
    • saveOrderAugment() persists txid + brand info
  3. Voucher Polling:

    • PhazeOrderPollingService polls for vouchers on open orders (redemption link + code)
    • Updates tx.savedAction with redemption info, when available
  4. List Scene Display:

    • Merges API orders with local augments
    • Shows pending cards (with txid, no voucher)
    • Shows active/redeemable cards (with voucher containing redemption link + code)
    • Shows redeemed cards (marked by user)

Scene Responsibilities

GiftCardListScene

  • Displays user's purchased cards
  • Initial load from module-level cache
  • Polls API for updates when focused
  • Separates active vs redeemed cards

GiftCardMarketScene

  • Displays available brands
  • Initial load from disk/memory cache
  • API refresh on mount (parallel with cache load)
  • Background fetch of full brand details

GiftCardPurchaseScene

  • Shows brand details and denomination picker
  • Uses useBrand hook for lazy detail loading
  • Creates order and handles payment flow

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Note

Introduces a full gift card experience powered by Phaze, including browsing, purchasing, and managing cards.

  • New scenes: GiftCardMarketScene, GiftCardPurchaseScene, GiftCardListScene; wired into navigation and Home “Spend” entry
  • Transaction integration: persists giftCard savedAction on send; displays details via GiftCardDetailsCard in TransactionDetailsScene
  • New UI building blocks: brand tiles/cards, region buttons, dropdown/pill buttons, icons; improved SceneContainer and SceneWrapper dock handling
  • Modals for amount selection, brand search, menus; enhanced WebViewModal (supports HTML and external links)
  • Country/state selection flow extended (skipStateProvince), ramp scenes updated to use unified region UI
  • Added react-native-render-html dependency (Podfile + package.json); multiple snapshot updates
  • Misc cleanup: logging tweaks, layout/padding fixes, KYC/Swap/Stake scenes adjusted to new container patterns

Written by Cursor Bugbot for commit 4c8d85e. This will update automatically on new commits. Configure here.


@Jon-edge Jon-edge marked this pull request as draft December 18, 2025 22:59
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Jon-edge Jon-edge force-pushed the jon/gift-cards branch 4 times, most recently from 7a499d1 to 32f616e Compare December 19, 2025 05:59
@Jon-edge Jon-edge marked this pull request as ready for review December 19, 2025 06:22
@Jon-edge Jon-edge force-pushed the jon/gift-cards branch 4 times, most recently from 572e99f to 6d1303c Compare December 19, 2025 19:34
@Jon-edge Jon-edge force-pushed the jon/gift-cards branch 2 times, most recently from 5c97e0f to 6731b1b Compare December 19, 2025 21:21
Copy link
Contributor

@swansontec swansontec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First half.

Comment on lines +163 to +168
),
phaze: asOptional(
asObject({
apiKey: asString,
baseUrl: asString
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this goes in a future commit? Not a big deal, just a tiny optional cleanup.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can reorg after approval

} | null>(null)

// Fetch allowed tokens from Phaze API
React.useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use Tan Stack or useAsyncValue

@Jon-edge Jon-edge force-pushed the jon/gift-cards branch 4 times, most recently from ddff5a2 to 3f9f177 Compare December 23, 2025 03:24
Copy link
Contributor

@swansontec swansontec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one question.

* Convert a JS number's `toString()` exponential form (e.g. "1e-7") into a
* plain decimal string (e.g. "0.0000001").
*/
const expandExponential = (num: number): string => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, doesn't biggystring know how to handle 1e1 formatted numbers now? If not, could something simpler like .toFixed be used?

@Jon-edge Jon-edge force-pushed the jon/gift-cards branch 2 times, most recently from 00a472f to 418fe81 Compare December 24, 2025 00:26
- Replace bit-flag logging with string categories (e.g. 'phaze', 'coinrank')
- Add LOG_CONFIG in env.json for enabling categories
- Add header masking for sensitive API keys in logs
- Improve type safety (unknown instead of any)
- Add Phaze plugin API key configuration
- caip19Utils: Convert between EdgeAsset and CAIP-19 identifiers
- parseLinkedText: Render HTML links as tappable React elements
- phazeApi: HTTP client with header masking
- phazeGiftCardTypes: TypeScript types and cleaners
- phazeGiftCardCache: Two-layer cache (memory + disk) for brands
- phazeGiftCardOrderStore: Order augments persistence
- phazeGiftCardProvider: High-level API for UI consumption
- phazeOrderPollingService: Background voucher polling
- useBrand: Lazy-load full brand details on demand
- useGiftCardProvider: Provider instance management
- PhazeActions: Start/stop order polling service
- LoginActions: Hook polling to login/logout lifecycle
UI text for market, purchase, and list scenes
- ThemedIcons: Add GiftCardIcon and GiftIcon
- WebViewModal: Support HTML content with external link handling
- SideMenu: Add gift card navigation entry
- Button/layout component styling updates
- Fix lint errors in files removed from exceptions list
Mimics raised/embossed text styling on physical credit cards.
Used for gift card display components.
When onPress is undefined, row renders dimmed and non-interactive.
Used for conditionally disabled gift card menu options.
- GiftCardDisplayCard: Full card view with PIN, status, shimmer states
- GiftCardDetailsCard: Compact card details
- GiftCardTile: Grid/list item for market browsing
- CircularBrandIcon: Rounded brand logo
- GiftCardAmountModal: Denomination picker
- GiftCardMenuModal: Card options (redeem, view tx)
- GiftCardSearchModal: Brand search with filtering
Required for gift card purchase scene HTML content rendering
- GiftCardListScene: View purchased cards (active/redeemed)
- GiftCardMarketScene: Browse available brands with categories
- GiftCardPurchaseScene: Select denomination and pay with crypto
- Register routes in navigation stack
- Display gift cards in TransactionDetailsScene
- Add giftCard action type handling in CategoriesActions
- Add sliderTopNode prop to SendScene2 for purchase flow
- Add Phaze merchant contact info
cachedActiveOrders = []
cachedRedeemedOrders = []
hasLoadedOnce = false
hasFetchedBrands = false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brand cache flag not reset when country changes

The module-level hasFetchedBrands variable is only reset in clearOrderCache() when the account changes, but not when countryCode changes. Since brands are country-specific and loadOrdersFromApi fetches brands based on countryCode, changing countries without resetting this flag causes brands for the new country to not be fetched. The useFocusEffect callback reads !hasFetchedBrands to determine includeBrands, so if brands were previously fetched for another country, they won't be refetched for the new one. This results in brandLookup returning undefined for the new country's product images.

Additional Locations (1)

Fix in Cursor Fix in Web

@Jon-edge Jon-edge merged commit 165de9a into develop Dec 24, 2025
4 checks passed
@Jon-edge Jon-edge deleted the jon/gift-cards branch December 24, 2025 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants