Skip to content

Commit 1777ede

Browse files
authored
Merge pull request #254 from reown-com/feat/ic_webview
feat(pay): add WebView-based Information Capture for Pay SDK
2 parents 4b671bf + 1977d7d commit 1777ede

File tree

10 files changed

+477
-68
lines changed

10 files changed

+477
-68
lines changed

Example/ExampleApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@
311311
PAY0000012EE100000000000A /* PayDateOfBirthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000A /* PayDateOfBirthView.swift */; };
312312
PAY0000012EE100000000000B /* PaySuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000B /* PaySuccessView.swift */; };
313313
PAY0000012EE100000000000C /* PayConfirmingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000C /* PayConfirmingView.swift */; };
314+
PAY0000012EE100000000000D /* PayDataCollectionWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */; };
314315
SIGNTD0001000000000001 /* SignTypedDataSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = SIGNTD0002000000000001 /* SignTypedDataSigner.swift */; };
315316
/* End PBXBuildFile section */
316317

@@ -618,6 +619,7 @@
618619
PAY0000022EE100000000000A /* PayDateOfBirthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDateOfBirthView.swift; sourceTree = "<group>"; };
619620
PAY0000022EE100000000000B /* PaySuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaySuccessView.swift; sourceTree = "<group>"; };
620621
PAY0000022EE100000000000C /* PayConfirmingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayConfirmingView.swift; sourceTree = "<group>"; };
622+
PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDataCollectionWebView.swift; sourceTree = "<group>"; };
621623
SIGNTD0002000000000001 /* SignTypedDataSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignTypedDataSigner.swift; sourceTree = "<group>"; };
622624
/* End PBXFileReference section */
623625

@@ -1627,6 +1629,7 @@
16271629
PAY0000022EE1000000000009 /* PayConfirmView.swift */,
16281630
PAY0000022EE100000000000B /* PaySuccessView.swift */,
16291631
PAY0000022EE100000000000C /* PayConfirmingView.swift */,
1632+
PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */,
16301633
);
16311634
path = Pay;
16321635
sourceTree = "<group>";
@@ -2205,6 +2208,7 @@
22052208
PAY0000012EE1000000000009 /* PayConfirmView.swift in Sources */,
22062209
PAY0000012EE100000000000B /* PaySuccessView.swift in Sources */,
22072210
PAY0000012EE100000000000C /* PayConfirmingView.swift in Sources */,
2211+
PAY0000012EE100000000000D /* PayDataCollectionWebView.swift in Sources */,
22082212
44650A3F2E951D71004F2144 /* TonSigner.swift in Sources */,
22092213
44650A432F1E0002004F2144 /* TronSigner.swift in Sources */,
22102214
C55D3494295DFA750004314A /* WelcomePresenter.swift in Sources */,

Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Example/WalletApp/PresentationLayer/Wallet/Pay/PayConfirmView.swift

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,37 +46,39 @@ struct PayConfirmView: View {
4646
.padding(.top, 16)
4747

4848
if let info = presenter.paymentInfo {
49-
// Merchant icon
50-
if let iconUrl = info.merchant.iconUrl {
51-
AsyncImage(url: URL(string: iconUrl)) { phase in
52-
if let image = phase.image {
53-
image
54-
.resizable()
55-
.aspectRatio(contentMode: .fit)
56-
} else {
57-
merchantPlaceholder(name: info.merchant.name)
49+
// Merchant icon with verified badge
50+
ZStack(alignment: .bottomTrailing) {
51+
if let iconUrl = info.merchant.iconUrl {
52+
AsyncImage(url: URL(string: iconUrl)) { phase in
53+
if let image = phase.image {
54+
image
55+
.resizable()
56+
.aspectRatio(contentMode: .fit)
57+
} else {
58+
merchantPlaceholder(name: info.merchant.name)
59+
}
5860
}
59-
}
60-
.frame(width: 64, height: 64)
61-
.cornerRadius(12)
62-
.padding(.top, 16)
63-
} else {
64-
merchantPlaceholder(name: info.merchant.name)
6561
.frame(width: 64, height: 64)
66-
.padding(.top, 16)
67-
}
68-
69-
// Payment title
70-
HStack(spacing: 6) {
71-
Text("Pay \(info.formattedAmount) to \(info.merchant.name)")
72-
.font(.system(size: 18, weight: .semibold, design: .rounded))
73-
.foregroundColor(.grey8)
74-
62+
.cornerRadius(12)
63+
} else {
64+
merchantPlaceholder(name: info.merchant.name)
65+
.frame(width: 64, height: 64)
66+
}
67+
68+
// Verified badge
7569
Image(systemName: "checkmark.seal.fill")
76-
.font(.system(size: 14))
70+
.font(.system(size: 20))
7771
.foregroundColor(.blue)
72+
.background(Circle().fill(Color.white).frame(width: 16, height: 16))
73+
.offset(x: 4, y: 4)
7874
}
7975
.padding(.top, 16)
76+
77+
// Payment title
78+
Text("Pay \(info.formattedAmount) to \(info.merchant.name)")
79+
.font(.system(size: 18, weight: .semibold, design: .rounded))
80+
.foregroundColor(.grey8)
81+
.padding(.top, 16)
8082

8183
// Payment details
8284
VStack(spacing: 0) {

Example/WalletApp/PresentationLayer/Wallet/Pay/PayContainerView.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ struct PayContainerView: View {
2121
case .intro:
2222
PayIntroView()
2323
.environmentObject(presenter)
24+
case .webviewDataCollection:
25+
if let url = presenter.buildICWebViewURL() {
26+
PayDataCollectionWebView(
27+
url: url,
28+
onClose: { presenter.goBack() },
29+
onComplete: { presenter.onICWebViewComplete() },
30+
onError: { error in presenter.onICWebViewError(error) },
31+
onFormDataChanged: { fullName, dob, pobAddress in presenter.onICFormDataChanged(fullName: fullName, dob: dob, pobAddress: pobAddress) }
32+
)
33+
.frame(maxWidth: .infinity, maxHeight: .infinity)
34+
.padding(.top, 50)
35+
.background(Color.whiteBackground)
36+
.ignoresSafeArea(edges: .bottom)
37+
}
2438
case .nameInput:
2539
PayNameInputView()
2640
.environmentObject(presenter)
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import SwiftUI
2+
import WebKit
3+
4+
struct PayDataCollectionWebView: View {
5+
let url: URL
6+
let onClose: () -> Void
7+
let onComplete: () -> Void
8+
let onError: (String) -> Void
9+
let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void
10+
11+
@State private var isLoading = true
12+
13+
var body: some View {
14+
ZStack(alignment: .topTrailing) {
15+
PayWebViewRepresentable(
16+
url: url,
17+
isLoading: $isLoading,
18+
onComplete: onComplete,
19+
onError: onError,
20+
onFormDataChanged: onFormDataChanged
21+
)
22+
23+
// Close button
24+
Button(action: onClose) {
25+
Image(systemName: "xmark")
26+
.font(.system(size: 14, weight: .semibold))
27+
.foregroundColor(.gray)
28+
.frame(width: 28, height: 28)
29+
.background(Color.white.opacity(0.9))
30+
.clipShape(Circle())
31+
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
32+
}
33+
.padding(.top, 56)
34+
.padding(.trailing, 20)
35+
36+
if isLoading {
37+
VStack(spacing: 16) {
38+
ProgressView()
39+
.scaleEffect(1.2)
40+
Text("Loading...")
41+
.font(.system(size: 14, design: .rounded))
42+
.foregroundColor(.secondary)
43+
}
44+
.frame(maxWidth: .infinity, maxHeight: .infinity)
45+
.background(Color.white)
46+
}
47+
}
48+
}
49+
}
50+
51+
private struct PayWebViewRepresentable: UIViewRepresentable {
52+
let url: URL
53+
@Binding var isLoading: Bool
54+
let onComplete: () -> Void
55+
let onError: (String) -> Void
56+
let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void
57+
58+
func makeCoordinator() -> Coordinator {
59+
Coordinator(isLoading: $isLoading, onComplete: onComplete, onError: onError, onFormDataChanged: onFormDataChanged)
60+
}
61+
62+
func makeUIView(context: Context) -> WKWebView {
63+
let config = WKWebViewConfiguration()
64+
65+
// Register message handler matching the IC page's expected name
66+
config.userContentController.add(context.coordinator, name: "payDataCollectionComplete")
67+
68+
let webView = WKWebView(frame: .zero, configuration: config)
69+
webView.navigationDelegate = context.coordinator
70+
webView.uiDelegate = context.coordinator
71+
webView.backgroundColor = .white
72+
webView.scrollView.backgroundColor = .white
73+
webView.isOpaque = false
74+
75+
print("💳 [PayWebView] Loading URL: \(url)")
76+
webView.load(URLRequest(url: url))
77+
return webView
78+
}
79+
80+
func updateUIView(_ uiView: WKWebView, context: Context) {}
81+
82+
class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate {
83+
@Binding var isLoading: Bool
84+
let onComplete: () -> Void
85+
let onError: (String) -> Void
86+
let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void
87+
88+
init(isLoading: Binding<Bool>, onComplete: @escaping () -> Void, onError: @escaping (String) -> Void, onFormDataChanged: @escaping (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void) {
89+
self._isLoading = isLoading
90+
self.onComplete = onComplete
91+
self.onError = onError
92+
self.onFormDataChanged = onFormDataChanged
93+
}
94+
95+
// JavaScript calls: window.webkit.messageHandlers.payDataCollectionComplete.postMessage({...})
96+
func userContentController(_ userContentController: WKUserContentController,
97+
didReceive message: WKScriptMessage) {
98+
print("💳 [PayWebView] Received message: \(message.body)")
99+
100+
guard let body = message.body as? [String: Any],
101+
let type = body["type"] as? String else {
102+
print("💳 [PayWebView] Invalid message format: \(message.body)")
103+
onError("Invalid message format")
104+
return
105+
}
106+
107+
print("💳 [PayWebView] Message type: \(type)")
108+
109+
switch type {
110+
case "IC_COMPLETE":
111+
let success = body["success"] as? Bool ?? false
112+
// Extract form data from completion message (same fields as prefill)
113+
let fullName = body["fullName"] as? String
114+
let dob = body["dob"] as? String
115+
let pobAddress = body["pobAddress"] as? String
116+
print("💳 [PayWebView] IC_COMPLETE received, success: \(success), fullName: \(fullName ?? "nil"), dob: \(dob ?? "nil"), pobAddress: \(pobAddress ?? "nil")")
117+
if success {
118+
DispatchQueue.main.async { [weak self] in
119+
self?.onFormDataChanged(fullName, dob, pobAddress)
120+
self?.onComplete()
121+
}
122+
}
123+
case "IC_ERROR":
124+
let error = body["error"] as? String ?? "Unknown error"
125+
print("💳 [PayWebView] IC_ERROR received: \(error)")
126+
DispatchQueue.main.async { [weak self] in
127+
self?.onError(error)
128+
}
129+
case "IC_FORM_DATA":
130+
let fullName = body["fullName"] as? String
131+
let dob = body["dob"] as? String
132+
let pobAddress = body["pobAddress"] as? String
133+
print("💳 [PayWebView] IC_FORM_DATA received - fullName: \(fullName ?? "nil"), dob: \(dob ?? "nil"), pobAddress: \(pobAddress ?? "nil")")
134+
DispatchQueue.main.async { [weak self] in
135+
self?.onFormDataChanged(fullName, dob, pobAddress)
136+
}
137+
default:
138+
// Ignore unknown message types silently (don't treat as error)
139+
print("💳 [PayWebView] Unknown message type: \(type)")
140+
}
141+
}
142+
143+
// Capture console.log from JavaScript
144+
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String,
145+
initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
146+
print("💳 [PayWebView] JS Alert: \(message)")
147+
completionHandler()
148+
}
149+
150+
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
151+
DispatchQueue.main.async { [weak self] in
152+
self?.isLoading = true
153+
}
154+
}
155+
156+
// Handle navigation errors
157+
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
158+
print("💳 [PayWebView] Navigation failed: \(error)")
159+
DispatchQueue.main.async { [weak self] in
160+
self?.isLoading = false
161+
self?.onError("Navigation failed: \(error.localizedDescription)")
162+
}
163+
}
164+
165+
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
166+
print("💳 [PayWebView] Failed to load: \(error)")
167+
DispatchQueue.main.async { [weak self] in
168+
self?.isLoading = false
169+
self?.onError("Failed to load page: \(error.localizedDescription)")
170+
}
171+
}
172+
173+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
174+
print("💳 [PayWebView] Page loaded successfully")
175+
DispatchQueue.main.async { [weak self] in
176+
self?.isLoading = false
177+
}
178+
}
179+
180+
// Handle link clicks - open external links in Safari
181+
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
182+
guard let url = navigationAction.request.url else {
183+
decisionHandler(.allow)
184+
return
185+
}
186+
187+
// Allow initial page load and same-origin navigations
188+
if navigationAction.navigationType == .other {
189+
decisionHandler(.allow)
190+
return
191+
}
192+
193+
// For link clicks, open in Safari
194+
if navigationAction.navigationType == .linkActivated {
195+
print("💳 [PayWebView] Opening external link in Safari: \(url)")
196+
UIApplication.shared.open(url)
197+
decisionHandler(.cancel)
198+
return
199+
}
200+
201+
decisionHandler(.allow)
202+
}
203+
}
204+
}
205+
206+
#if DEBUG
207+
struct PayDataCollectionWebView_Previews: PreviewProvider {
208+
static var previews: some View {
209+
PayDataCollectionWebView(
210+
url: URL(string: "https://example.com")!,
211+
onClose: {},
212+
onComplete: {},
213+
onError: { _ in },
214+
onFormDataChanged: { _, _, _ in }
215+
)
216+
}
217+
}
218+
#endif

0 commit comments

Comments
 (0)