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
48 changes: 39 additions & 9 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
| --------------------------------------- | -------------------------------------------------------------------------- |
| Phase 1: Infrastructure & Configuration | ✅ Complete |
| Phase 2: WebView Bridge (2.1-2.4) | ✅ Complete |
| Phase 2.5: Storm-Side Changes | [ ] Not started (external dependency — requires changes to storm codebase) |
| Phase 2.5: Storm-Side Changes | ⏸️ Deferred — only if injected bridge fails on target devices |
| Phase 3: Credit Card Component | ✅ Complete (core messages + field events; `setPort` deferred — not needed for tokenization) |
| Phase 4: 3D Secure Component | ✅ Complete |
| Phase 5: Digital Wallets | ✅ Code written — needs physical device testing |
| Phase 6: Integration & QA | Partial — example app built, E2E/device testing pending |
| Phase 7: End-to-End Flows | 🔄 In progress7.1 message flow tests complete |
| Phase 7: End-to-End Flows | ✅ Code completeremaining items need backend E2E / device testing |
| File structure | ✅ All 24 planned files created |
| TypeScript | ✅ Compiles cleanly (strict mode) |
| Unit tests | ✅ 89 tests passing |

**Remaining work:**

- Storm-side changes (3 files in `libs/base/`) — external dependency, not in this repo
- Storm-side changes — ⏸️ deferred (only needed if injected bridge fails on target devices)
- Credit Card `setPort` RPC channel — **deferred** (not needed for tokenization; only used for `loadMerchantDetails()` + analytics)
- E2E verification (bridge smoke test, tokenization, 3DS challenge)
- E2E verification (bridge smoke test, tokenization, 3DS challenge) — requires staging environment
- Physical device testing for Apple Pay and Google Pay
- Token format verification against Bolt's add-card API (backend E2E)
- App store compliance review
- **Phase 7 work** — wallet management, 3DS bootstrap, Tokenizer Proxy compatibility, add-card-from-wallet flows

---

Expand Down Expand Up @@ -465,6 +465,15 @@ Storm's web elements (`add-card-from-apple-wallet`, `add-card-from-google-wallet
- **`onComplete` result shape** should include: `{ token, billingContact: { email, name, phone, postalAddress } }` (Apple Pay) and equivalent for Google Pay.
- **Bolt account creation:** When a shopper pays with Apple Pay for the first time, Bolt generates an account using the email from the Apple Pay response.

**Status:**
- ✅ Billing contact fields collected (email, phone, name, postal address) — both Apple Pay and Google Pay
- ✅ `boltReference` field returned from both Apple Pay (`ApplePayResult.boltReference`) and Google Pay (`GooglePayResult.boltReference`) — extracted from Bolt tokenize API response
- ✅ `onComplete` result shape matches requirements for both platforms
- ✅ Example app displays billing contact + bolt reference in alerts
- ✅ Type tests updated for `boltReference` field
- [ ] Token format verification against Bolt's add-card API (requires backend E2E test)
- [ ] Physical device testing (Apple Pay sandbox, Google Pay test account)

**Deliverable targets from requirements doc:**

- Apple Pay card addition: 9/22 target
Expand All @@ -479,6 +488,10 @@ The Tokenizer Proxy (`POST /v1/tokenizer/proxy`) allows merchants to use Bolt to

**No SDK changes needed**, but documentation/example should show the expected backend integration.

**Status:**
- ✅ Example app shows both V3 Payments and Tokenizer Proxy paths in commented backend integration steps
- [ ] E2E verification that `tokenize()` output works with Tokenizer Proxy (requires backend test)

#### 7.4 — Wallet Management Documentation

Per requirements, the merchant must maintain shopper wallets in their own app UI. The data comes from Bolt's APIs but the merchant renders it (they don't want our webview/payment-selector element).
Expand All @@ -498,6 +511,11 @@ Per requirements, the merchant must maintain shopper wallets in their own app UI
5. To add a new card: SDK `CreditCard.Component` → `tokenize()` → merchant backend adds card via API
6. To add via Apple Pay: SDK `ApplePay` component → `onComplete` → merchant backend adds card via API

**Status:**
- ✅ Example app "Wallet" tab shows saved cards list (mock data matching `GET /v3/account` response shape)
- ✅ "Pay with Saved Card" demonstrates using `credit_card_id` with V3 Payments or Tokenizer Proxy
- ✅ "Add Card" tab demonstrates full card addition + 3DS bootstrap flow

#### 7.5 — Shopper Identity Flows Documentation

Three flows need to be documented per requirements:
Expand Down Expand Up @@ -529,6 +547,10 @@ App auth → Bolt Merchant Shopper Login (existing account)
→ Shopper completes action → payment
```

**Status:**
- ✅ All three shopper flows documented as inline comments in example app
- ✅ Example app structure demonstrates the flows (Add Card tab = Flow 2, Wallet tab = Flow 1/3)

**Open question:** How to capture email for unrecognized shoppers?

- Option 1: Don't require email (phone-only Bolt account) — under discussion
Expand All @@ -545,6 +567,10 @@ For shoppers without a Bolt account who decline to create one, the Bolt API supp

**SDK impact:** No additional SDK components needed. The SDK provides the token via `tokenize()`, the merchant backend handles the guest payment API call.

**Status:**
- ✅ "Guest Payment Flow" button in example app Wallet tab shows the complete guest payment sequence
- ✅ No SDK changes needed — `tokenize()` output is compatible with guest payments

---

## Open Questions & Dependencies
Expand All @@ -562,13 +588,17 @@ For shoppers without a Bolt account who decline to create one, the Bolt API supp

---

## Storm Codebase Changes Required — NOT STARTED (external dependency)
## Storm Codebase Changes — ⏸️ DEFERRED

Only needed if the injected bridge (`injectedBridge.ts`) fails on target devices — specifically if `Object.defineProperty(window, 'parent')` doesn't work or origin spoofing breaks.

| File | Change | Risk | Status |
| ------------------------------------ | --------------------------------------------------------- | ---- | ------ |
| `libs/base/utils/Parent.ts` | Add `isReactNativeWebView()`, update `getParent()` | Low | [ ] |
| `libs/base/messaging/Listener.ts` | Skip origin validation in RN WebView | Low | [ ] |
| `libs/base/messaging/PostMessage.ts` | Route `safePost` through `ReactNativeWebView.postMessage` | Low | [ ] |
| `libs/base/utils/Parent.ts` | Add `isReactNativeWebView()`, update `getParent()` | Low | ⏸️ |
| `libs/base/messaging/Listener.ts` | Skip origin validation in RN WebView | Low | ⏸️ |
| `libs/base/messaging/PostMessage.ts` | Route `safePost` through `ReactNativeWebView.postMessage` | Low | ⏸️ |

**Decision:** Test injected bridge on physical devices first. If it works reliably, these changes are unnecessary.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ class GooglePayModule(reactContext: ReactApplicationContext) :
result.put("email", email)
}

// Bolt reference from tokenize response (used for add-card API)
val boltReference = tokenResult.optString("bolt_reference", "")
if (boltReference.isNotEmpty()) {
result.put("boltReference", boltReference)
}

promise.resolve(result.toString())
} else {
promise.reject("TOKENIZE_FAILED", "Failed to tokenize Google Pay payment")
Expand Down
224 changes: 222 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ const AddCardScreen = () => {
);

// Step 3 (merchant backend, not SDK):
//
// Option A: Bolt V3 Payments (direct)
// POST /v3/payments with token + 3DS reference for $1 auth
// If 3DS challenge required:
// const challengeResult = await threeDSecure.challengeWithConfig(
Expand All @@ -103,6 +105,12 @@ const AddCardScreen = () => {
// );
// POST void transaction API to void the $1 auth
// POST add card API to store the card → receive creditCardID
//
// Option B: Tokenizer Proxy (existing processor)
// POST /v1/tokenizer/proxy with Bolt token
// Bolt exchanges token for raw PAN in PCI-compliant environment
// Forwards to merchant's existing processor (e.g., Stripe)
// Merchant receives processor token for payment
} catch (error) {
Alert.alert(
'Error',
Expand Down Expand Up @@ -133,7 +141,8 @@ const AddCardScreen = () => {
`Token: ${result.token.slice(0, 20)}...\n` +
`Email: ${result.email ?? 'N/A'}\n` +
`Name: ${result.billingAddress?.name ?? 'N/A'}\n` +
`Phone: ${result.billingAddress?.phoneNumber ?? 'N/A'}\n\n` +
`Phone: ${result.billingAddress?.phoneNumber ?? 'N/A'}\n` +
`Bolt Ref: ${result.boltReference ?? 'N/A'}\n\n` +
'Next: merchant backend calls Bolt add-card API with this token.'
);
}, []);
Expand Down Expand Up @@ -232,10 +241,165 @@ const AddCardScreen = () => {
);
};

// ── Wallet Management (Shopper Identity Flows) ─────────────
// Demonstrates how the merchant app manages saved cards using
// Bolt APIs alongside the SDK for card input/tokenization.
//
// This is a UI-only demo — the merchant backend calls are shown
// as commented pseudocode since they happen server-side.
//
// Three shopper flows:
//
// Flow 1: Recognized shopper (new to app)
// App auth → phone number → Bolt Merchant Shopper Login (finds existing Bolt account)
// → OAuth token exchange → GET /v3/account → saved cards populate in app UI
// → Shopper pays → POST /v3/payments with credit_card_id
//
// Flow 2: Unrecognized shopper (new)
// App auth → Bolt Merchant Shopper Login (creates new Bolt account)
// → OAuth token exchange → no saved cards
// → SDK: CreditCard.Component → tokenize() → add card to Bolt account
// → 3DS bootstrap ($1 auth + void)
// → Shopper pays → POST /v3/payments or Tokenizer Proxy
//
// Flow 3: Returning shopper
// App auth → Bolt Merchant Shopper Login (existing account)
// → OAuth token exchange → GET /v3/account → stored cards shown
// → Use stored card or add new via SDK
// → Shopper pays → payment

// Mock data representing what GET /v3/account returns
const MOCK_SAVED_CARDS = [
{
credit_card_id: 'cc_abc123',
last4: '1111',
network: 'visa',
expiration: '2028-12',
},
{
credit_card_id: 'cc_def456',
last4: '4242',
network: 'mastercard',
expiration: '2027-06',
},
];

const WalletScreen = () => {
const [savedCards] = useState(MOCK_SAVED_CARDS);
const [selectedCard, setSelectedCard] = useState<string | null>(null);

const handlePayWithSavedCard = useCallback((creditCardId: string) => {
Alert.alert(
'Pay with Saved Card',
`Using credit_card_id: ${creditCardId}\n\n` +
'Merchant backend would call:\n' +
'POST /v3/payments with credit_card_id\n' +
' or\n' +
'POST /v1/tokenizer/proxy for existing processor'
);
}, []);

// Guest payment flow (7.6) — for shoppers without a Bolt account
const handleGuestPayment = useCallback(() => {
Alert.alert(
'Guest Payment',
'For shoppers without a Bolt account:\n\n' +
'1. SDK: CreditCard.Component → tokenize()\n' +
'2. Merchant backend: POST /v3/guest/payments with:\n' +
' - profile (name, email, phone)\n' +
' - cart details\n' +
' - tokenized credit card + billing address\n' +
' - optional: create_bolt_account flag'
);
}, []);

return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Wallet Management</Text>
<Text style={styles.subtitle}>Saved Cards from GET /v3/account</Text>

{/* Saved Cards List */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Saved Cards</Text>
<Text style={styles.description}>
Cards returned by GET /v3/account after Merchant Shopper Login + OAuth
token exchange. The merchant renders these in their own UI.
</Text>
{savedCards.map((card) => (
<TouchableOpacity
key={card.credit_card_id}
style={[
styles.cardRow,
selectedCard === card.credit_card_id && styles.cardRowSelected,
]}
onPress={() => setSelectedCard(card.credit_card_id)}
>
<Text style={styles.cardNetwork}>{card.network.toUpperCase()}</Text>
<Text style={styles.cardDetails}>
****{card.last4} — exp {card.expiration}
</Text>
</TouchableOpacity>
))}
</View>

{/* Pay with Saved Card */}
<TouchableOpacity
style={[styles.primaryButton, !selectedCard && styles.buttonDisabled]}
onPress={() => selectedCard && handlePayWithSavedCard(selectedCard)}
disabled={!selectedCard}
>
<Text style={styles.primaryButtonText}>Pay with Saved Card</Text>
</TouchableOpacity>

{/* Guest Payment */}
<TouchableOpacity
style={[styles.primaryButton, styles.secondaryButton]}
onPress={handleGuestPayment}
>
<Text style={[styles.primaryButtonText, styles.secondaryButtonText]}>
Guest Payment Flow
</Text>
</TouchableOpacity>
</ScrollView>
);
};

// ── App with Tab Navigation ─────────────────────────────────

export default function App() {
const [activeTab, setActiveTab] = useState<'addCard' | 'wallet'>('addCard');

return (
<BoltProvider client={bolt}>
<AddCardScreen />
{activeTab === 'addCard' ? <AddCardScreen /> : <WalletScreen />}
<View style={styles.tabBar}>
<TouchableOpacity
style={[styles.tab, activeTab === 'addCard' && styles.tabActive]}
onPress={() => setActiveTab('addCard')}
>
<Text
style={[
styles.tabText,
activeTab === 'addCard' && styles.tabTextActive,
]}
>
Add Card
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === 'wallet' && styles.tabActive]}
onPress={() => setActiveTab('wallet')}
>
<Text
style={[
styles.tabText,
activeTab === 'wallet' && styles.tabTextActive,
]}
>
Wallet
</Text>
</TouchableOpacity>
</View>
</BoltProvider>
);
}
Expand Down Expand Up @@ -325,4 +489,60 @@ const styles = StyleSheet.create({
fontSize: 11,
color: '#666',
},
cardRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 14,
borderRadius: 8,
backgroundColor: '#f9fafb',
marginBottom: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
},
cardRowSelected: {
borderColor: '#5A31F4',
backgroundColor: '#f5f3ff',
},
cardNetwork: {
fontSize: 12,
fontWeight: '700',
color: '#374151',
width: 90,
},
cardDetails: {
fontSize: 14,
color: '#6b7280',
},
secondaryButton: {
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#5A31F4',
},
secondaryButtonText: {
color: '#5A31F4',
},
tabBar: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
backgroundColor: '#ffffff',
},
tab: {
flex: 1,
paddingVertical: 14,
alignItems: 'center',
},
tabActive: {
borderTopWidth: 2,
borderTopColor: '#5A31F4',
},
tabText: {
fontSize: 14,
color: '#9ca3af',
fontWeight: '500',
},
tabTextActive: {
color: '#5A31F4',
fontWeight: '600',
},
});
Loading
Loading