Skip to content

Commit 059ab2d

Browse files
committed
Updated ViewController with CDP support for WKWebView
1 parent 9f6904c commit 059ab2d

2 files changed

Lines changed: 133 additions & 122 deletions

File tree

standalone-sample/ios-native-wkwebview/README.md

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
# Coinbase Apple Pay iOS Demo
22

3-
Minimal working implementation of Coinbase Apple Pay Onramp for native iOS apps using WKWebView.
3+
Minimal working example of Coinbase Apple Pay integration for native iOS apps using WKWebView.
44

55
## Video Demo
66

7-
87
https://github.com/user-attachments/assets/ee734f06-3292-4874-8113-b8a360370b6e
98

10-
11-
129
## Project Structure
1310

1411
```
1512
iOS/ # Native iOS Swift app
16-
├── ViewController.swift # Main: WKWebView + postMessage bridge
13+
├── ViewController.swift # WKWebView implementation
1714
├── AppDelegate.swift
1815
├── SceneDelegate.swift
1916
├── Info.plist # Network security config
2017
└── project.yml # XcodeGen config
2118
22-
web/ # Backend + Web interface
23-
├── server.js # Express API
19+
web/ # Backend server + Web testing
20+
├── server.js # Express API to create payment orders
2421
├── public/
25-
│ ├── index.html
22+
│ ├── index.html # Web testing interface
2623
│ └── app.js
27-
└── .env.example # API credentials template
24+
└── .env.example # API credentials template
2825
```
2926

3027
## Quick Start
@@ -37,7 +34,7 @@ npm install
3734
cp .env.example .env
3835
```
3936

40-
Edit `.env` and add your Coinbase CDP API credentials:
37+
Edit `.env` and add your [Coinbase CDP API credentials](https://portal.cdp.coinbase.com):
4138
```
4239
CDP_API_KEY_ID=your_api_key_id
4340
CDP_API_KEY_SECRET=your_api_key_secret
@@ -48,33 +45,30 @@ Start server:
4845
npm start
4946
```
5047

51-
**Web Testing:** Open `http://localhost:3000` in your browser to test the Apple Pay flow using the iframe method (displays QR code fallback for web).
48+
**Optional:** Test in browser at `http://localhost:3000` (shows QR code for web)
5249

5350
### 2. iOS App Setup
5451

5552
**Option A: Same WiFi Network**
5653

57-
1. Find your computer's IP address:
58-
```bash
59-
ifconfig | grep "inet " | grep -v 127.0.0.1
60-
```
54+
Find your computer's IP address:
55+
```bash
56+
ifconfig | grep "inet " | grep -v 127.0.0.1
57+
```
6158

62-
2. Update `ViewController.swift` line 153:
63-
```swift
64-
let backendURL = "http://YOUR_IP_ADDRESS:3000/api/create-order"
65-
```
59+
Update `ViewController.swift` line 153:
60+
```swift
61+
let backendURL = "http://YOUR_IP_ADDRESS:3000/api/create-order"
62+
```
6663

6764
**Option B: Using ngrok (Recommended)**
6865

6966
```bash
70-
# Install ngrok
7167
brew install ngrok
72-
73-
# Create tunnel
7468
ngrok http 3000
7569
```
7670

77-
Update `ViewController.swift` line 153 with the ngrok URL:
71+
Update `ViewController.swift` line 153:
7872
```swift
7973
let backendURL = "https://your-ngrok-url.ngrok-free.dev/api/create-order"
8074
```
@@ -86,62 +80,117 @@ let backendURL = "https://your-ngrok-url.ngrok-free.dev/api/create-order"
8680
3. Select your device in Xcode
8781
4. Build & Run (⌘R)
8882

89-
**Note:** Apple Pay requires a physical iOS device with Apple Pay support and setup It will not work in the simulator.
83+
**Note:** Requires a physical iOS device with Apple Pay configured. Won't work in simulator.
9084

9185
## Requirements
9286

9387
- macOS with Xcode 14+
9488
- Node.js 16+
95-
- iOS 14.0+ device with Apple Pay configured
89+
- Physical iOS device (iOS 14.0+) with Apple Pay set up
9690
- [Coinbase CDP API credentials](https://portal.cdp.coinbase.com)
9791

98-
## Key Implementation Details
92+
## How It Works
93+
94+
### 1. Create Payment Order
9995

100-
### WKWebView Configuration
96+
Your backend calls the Coinbase API to create an order and receive a payment URL:
97+
98+
```javascript
99+
// Backend calls Coinbase
100+
const response = await fetch('https://api.cdp.coinbase.com/platform/v2/onramp/orders', {
101+
method: 'POST',
102+
body: JSON.stringify({
103+
paymentAmount: "20",
104+
paymentCurrency: "USD",
105+
purchaseCurrency: "USDC",
106+
paymentMethod: "GUEST_CHECKOUT_APPLE_PAY",
107+
// ... other fields
108+
})
109+
});
110+
111+
// Returns: { paymentLink: { url: "https://pay.coinbase.com/..." } }
112+
```
113+
114+
### 2. Configure WKWebView
115+
116+
Set up WKWebView with the message handler:
101117

102118
```swift
119+
// Configure WKWebView
103120
let configuration = WKWebViewConfiguration()
104-
configuration.allowsInlineMediaPlayback = true
105-
configuration.mediaTypesRequiringUserActionForPlayback = []
106121

122+
// Register message handler to receive payment events
107123
let contentController = WKUserContentController()
108-
contentController.add(self, name: "onramp")
124+
contentController.add(self, name: "cbOnramp") // Must be "cbOnramp"
109125
configuration.userContentController = contentController
126+
127+
// Create WKWebView
128+
webView = WKWebView(frame: .zero, configuration: configuration)
110129
```
111130

112-
### Coinbase Event Handling
131+
### 3. Load Payment URL
132+
133+
```swift
134+
// Load the payment URL from Coinbase
135+
let url = URL(string: paymentLink)
136+
webView.load(URLRequest(url: url))
137+
```
138+
139+
### 4. Receive Payment Events
140+
141+
Coinbase detects `window.webkit.messageHandlers.cbOnramp` and sends events directly:
142+
143+
```swift
144+
extension ViewController: WKScriptMessageHandler {
145+
func userContentController(_ userContentController: WKUserContentController,
146+
didReceive message: WKScriptMessage) {
147+
guard message.name == "cbOnramp" else { return }
148+
149+
// Parse event
150+
if let messageBody = message.body as? String,
151+
let data = messageBody.data(using: .utf8),
152+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
153+
let eventName = json["eventName"] as? String {
154+
print("Payment event: \(eventName)")
155+
handlePaymentEvent(eventName, data: json)
156+
}
157+
}
158+
}
159+
```
113160

114-
The app listens for these events via `WKScriptMessageHandler`:
161+
## Payment Events
115162

116-
- See all events [here](https://docs.cdp.coinbase.com/onramp-&-offramp/onramp-apis/apple-pay-onramp-api#post-message-events)
163+
The payment flow sends various events like `onramp_api.load_success`, `onramp_api.commit_success`, `onramp_api.polling_success`, etc.
117164

118-
See `ViewController.swift` lines 357-410 for full implementation.
165+
See the [full list of events](https://docs.cdp.coinbase.com/onramp-&-offramp/onramp-apis/apple-pay-onramp-api#post-message-events) in the Coinbase documentation.
119166

120167
## Sandbox Testing
121168

122-
The demo uses sandbox mode by default. Transactions use test credentials and won't charge real money.
169+
The demo uses sandbox mode (no real money):
170+
- `partnerUserRef` must start with `"sandbox-"`
171+
- Transactions auto-complete
123172

124173
## Troubleshooting
125174

126175
**"Internet connection appears to be offline"**
127-
- Ensure iPhone and computer are on the same WiFi
176+
- Ensure iPhone and Mac are on same WiFi
177+
- Try ngrok instead of local IP
128178
- Check firewall settings
129-
- Use ngrok to bypass network issues
130179

131-
**Apple Pay not appearing**
132-
- Must use physical iOS device (not simulator)
180+
**Apple Pay sheet doesn't appear**
181+
- Must use physical device (not simulator)
133182
- Device must have Apple Pay configured
134-
- App must be served over HTTPS (use ngrok for testing)
183+
- Ensure message handler name is exactly `"cbOnramp"`
135184

136-
**Events not being received**
137-
- Check message handler name is `"onramp"`
138-
- Verify JavaScript bridge injection in `didFinish` navigation
139-
- Enable Safari Web Inspector to debug
185+
**No events received**
186+
- Handler name must be exactly `"cbOnramp"` (case-sensitive)
187+
- Check Safari Web Inspector for JavaScript errors
140188

141189
## Documentation
142190

143-
- [Coinbase Onramp Documentation](https://docs.cdp.coinbase.com/onramp/docs)
191+
- [Coinbase Onramp API Docs](https://docs.cdp.coinbase.com/onramp/docs)
144192
- [Apple Pay Onramp API](https://docs.cdp.coinbase.com/onramp-&-offramp/onramp-apis/apple-pay-onramp-api)
193+
- [CDP Portal (API Keys)](https://portal.cdp.coinbase.com)
145194

146195
## License
147196

standalone-sample/ios-native-wkwebview/iOS/ViewController.swift

Lines changed: 39 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,16 @@ class ViewController: UIViewController {
8989
let configuration = WKWebViewConfiguration()
9090
configuration.preferences.javaScriptEnabled = true
9191

92-
// IMPORTANT: Set up message handler for postMessage events
92+
// IMPORTANT: Set up message handler for Coinbase events
93+
// Coinbase SDK natively supports WKWebView via window.webkit.messageHandlers.cbOnramp
94+
// No JavaScript injection needed - events are sent directly to this handler!
9395
let contentController = WKUserContentController()
94-
contentController.add(self, name: "onramp")
96+
contentController.add(self, name: "cbOnramp") // Handler name must be "cbOnramp"
9597
configuration.userContentController = contentController
9698

97-
// CRITICAL: These settings are required for Apple Pay to work
98-
configuration.allowsInlineMediaPlayback = true
99-
configuration.mediaTypesRequiringUserActionForPlayback = []
99+
// These settings may be required for Apple Pay to work
100+
// configuration.allowsInlineMediaPlayback = true
101+
// configuration.mediaTypesRequiringUserActionForPlayback = []
100102

101103
// Create hidden webview (will only show Apple Pay sheet, not the web page)
102104
webView = WKWebView(frame: .zero, configuration: configuration)
@@ -320,33 +322,17 @@ class ViewController: UIViewController {
320322

321323
extension ViewController: WKScriptMessageHandler {
322324
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
323-
// This is the CRITICAL bridge for receiving postMessage events from the Coinbase web page
324-
325-
guard message.name == "onramp" else { return }
326-
327-
// Log the type we're receiving
328-
logEvent("📦 Message type: \(type(of: message.body))")
329-
330-
// Parse the message body
331-
// With postMessage override, messages arrive as JSON strings
332-
if let messageBody = message.body as? String {
333-
// Parse JSON string (standard format from postMessage override)
334-
logEvent("🔤 Received as String, parsing JSON...")
335-
if let data = messageBody.data(using: .utf8),
336-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
337-
handleMessageData(json)
338-
}
339-
} else if let messageBody = message.body as? [String: Any] {
340-
// Fallback for Dictionary format (used by event listener approach if enabled)
341-
logEvent("📘 Received as Dictionary directly")
342-
handleMessageData(messageBody)
325+
// Receive events directly from Coinbase SDK via window.webkit.messageHandlers.cbOnramp
326+
guard message.name == "cbOnramp" else { return }
327+
328+
// Parse the message body (Coinbase sends as JSON string)
329+
if let messageBody = message.body as? String,
330+
let data = messageBody.data(using: .utf8),
331+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
332+
let eventName = json["eventName"] as? String {
333+
handleCoinbaseEvent(eventName, data: json)
343334
}
344335
}
345-
346-
private func handleMessageData(_ data: [String: Any]) {
347-
guard let eventName = data["eventName"] as? String else { return }
348-
handleCoinbaseEvent(eventName, data: data)
349-
}
350336
}
351337

352338
// MARK: - WKNavigationDelegate
@@ -355,61 +341,28 @@ extension ViewController: WKNavigationDelegate {
355341
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
356342
logEvent("📄 WebView page loaded")
357343

358-
// JavaScript Bridge for Coinbase postMessage Events
359-
// Coinbase uses the standard web postMessage API, which doesn't automatically reach native iOS.
360-
// This bridge forwards those events to iOS via window.webkit.messageHandlers.
361-
344+
// ✅ NO JAVASCRIPT INJECTION NEEDED!
345+
// Coinbase SDK natively detects window.webkit.messageHandlers.cbOnramp
346+
// and sends events directly - no manual bridging required!
347+
348+
/*
349+
// ============================================================================
350+
// LEGACY APPROACH - Kept for reference
351+
// ============================================================================
352+
// Before native WKWebView support, iOS developers had to use JavaScript
353+
// to bridge postMessage events to native code. This is no longer needed!
354+
//
355+
// Old method: Intercept window.postMessage and forward to native
356+
//
362357
let bridgeScript = """
363358
(function() {
364-
// Override window.postMessage to forward events to native iOS
365-
// This intercepts postMessage calls at the source (single capture point)
366359
const originalPostMessage = window.postMessage;
367360
window.postMessage = function(message, targetOrigin) {
368-
// Forward to native iOS via WKWebView message handler
369361
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.onramp) {
370362
window.webkit.messageHandlers.onramp.postMessage(message);
371363
}
372-
// Preserve original web behavior
373364
originalPostMessage.apply(window, arguments);
374365
};
375-
376-
/*
377-
// ALTERNATIVE APPROACH (WILL NOT WORK): Message Event Listener
378-
//
379-
// This approach listens for postMessage events as they cross iframe boundaries.
380-
// However, due to Coinbase's nested iframe architecture, the same event may be
381-
// captured multiple times as it propagates through iframe levels, causing:
382-
// - Duplicate event processing
383-
// - Race conditions in payment flow
384-
// - Potential spurious cancel events
385-
//
386-
// The postMessage override above is preferred as it captures each event exactly
387-
// once at its source, avoiding duplicates.
388-
//
389-
// Only consider this approach if:
390-
// - The override doesn't work in your environment
391-
// - You need to capture cross-origin iframe messages specifically
392-
// - You implement deduplication logic on the native side
393-
//
394-
window.addEventListener('message', function(event) {
395-
// Verify message origin
396-
try {
397-
const originUrl = new URL(event.origin);
398-
const allowedHosts = ['pay.coinbase.com', 'coinbase.com'];
399-
const isAllowed = allowedHosts.some(host =>
400-
originUrl.hostname === host || originUrl.hostname.endsWith('.' + host)
401-
);
402-
if (!isAllowed) return;
403-
} catch (e) {
404-
return;
405-
}
406-
407-
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
408-
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.onramp) {
409-
window.webkit.messageHandlers.onramp.postMessage(data);
410-
}
411-
});
412-
*/
413366
})();
414367
"""
415368

@@ -420,6 +373,15 @@ extension ViewController: WKNavigationDelegate {
420373
self?.logEvent("✅ postMessage bridge injected")
421374
}
422375
}
376+
//
377+
// Issues with this approach:
378+
// - Extra code complexity (50+ lines of JavaScript)
379+
// - Potential for race conditions if not implemented carefully
380+
// - Maintenance burden for developers
381+
//
382+
// With native support, simply register the "cbOnramp" handler and you're done!
383+
// ============================================================================
384+
*/
423385
}
424386

425387
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
@@ -432,4 +394,4 @@ extension ViewController: WKNavigationDelegate {
432394

433395
extension ViewController: WKUIDelegate {
434396
// This is important for handling alerts and prompts from the web page
435-
}
397+
}

0 commit comments

Comments
 (0)