Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions Example/ExampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
PAY0000012EE100000000000A /* PayDateOfBirthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000A /* PayDateOfBirthView.swift */; };
PAY0000012EE100000000000B /* PaySuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000B /* PaySuccessView.swift */; };
PAY0000012EE100000000000C /* PayConfirmingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000C /* PayConfirmingView.swift */; };
PAY0000012EE100000000000D /* PayDataCollectionWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */; };
SIGNTD0001000000000001 /* SignTypedDataSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = SIGNTD0002000000000001 /* SignTypedDataSigner.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -618,6 +619,7 @@
PAY0000022EE100000000000A /* PayDateOfBirthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDateOfBirthView.swift; sourceTree = "<group>"; };
PAY0000022EE100000000000B /* PaySuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaySuccessView.swift; sourceTree = "<group>"; };
PAY0000022EE100000000000C /* PayConfirmingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayConfirmingView.swift; sourceTree = "<group>"; };
PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDataCollectionWebView.swift; sourceTree = "<group>"; };
SIGNTD0002000000000001 /* SignTypedDataSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignTypedDataSigner.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -1627,6 +1629,7 @@
PAY0000022EE1000000000009 /* PayConfirmView.swift */,
PAY0000022EE100000000000B /* PaySuccessView.swift */,
PAY0000022EE100000000000C /* PayConfirmingView.swift */,
PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */,
);
path = Pay;
sourceTree = "<group>";
Expand Down Expand Up @@ -2205,6 +2208,7 @@
PAY0000012EE1000000000009 /* PayConfirmView.swift in Sources */,
PAY0000012EE100000000000B /* PaySuccessView.swift in Sources */,
PAY0000012EE100000000000C /* PayConfirmingView.swift in Sources */,
PAY0000012EE100000000000D /* PayDataCollectionWebView.swift in Sources */,
44650A3F2E951D71004F2144 /* TonSigner.swift in Sources */,
44650A432F1E0002004F2144 /* TronSigner.swift in Sources */,
C55D3494295DFA750004314A /* WelcomePresenter.swift in Sources */,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ struct PayContainerView: View {
case .intro:
PayIntroView()
.environmentObject(presenter)
case .webviewDataCollection:
if let urlString = presenter.paymentOptionsResponse?.collectData?.url,
let url = URL(string: urlString) {
PayDataCollectionWebView(
url: url,
onComplete: { presenter.onICWebViewComplete() },
onError: { error in presenter.onICWebViewError(error) }
)
.frame(height: 550)
.background(Color.whiteBackground)
.cornerRadius(34)
.padding(.horizontal, 10)
.padding(.bottom, 10)
}
case .nameInput:
PayNameInputView()
.environmentObject(presenter)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import SwiftUI
import WebKit

struct PayDataCollectionWebView: UIViewRepresentable {
let url: URL
let onComplete: () -> Void
let onError: (String) -> Void

func makeCoordinator() -> Coordinator {
Coordinator(onComplete: onComplete, onError: onError)
}

func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()

// Register message handler matching the IC page's expected name
config.userContentController.add(context.coordinator, name: "payDataCollectionComplete")

let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator
webView.backgroundColor = .white
webView.scrollView.backgroundColor = .white

print("💳 [PayWebView] Loading URL: \(url)")
webView.load(URLRequest(url: url))
return webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {}

class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate {
let onComplete: () -> Void
let onError: (String) -> Void

init(onComplete: @escaping () -> Void, onError: @escaping (String) -> Void) {
self.onComplete = onComplete
self.onError = onError
}

// JavaScript calls: window.webkit.messageHandlers.payDataCollectionComplete.postMessage({...})
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
print("💳 [PayWebView] Received message: \(message.body)")

guard let body = message.body as? [String: Any],
let type = body["type"] as? String else {
print("💳 [PayWebView] Invalid message format: \(message.body)")
onError("Invalid message format")
return
}

print("💳 [PayWebView] Message type: \(type)")

switch type {
case "IC_COMPLETE":
let success = body["success"] as? Bool ?? false
print("💳 [PayWebView] IC_COMPLETE received, success: \(success)")
if success {
DispatchQueue.main.async { [weak self] in
self?.onComplete()
}
}
case "IC_ERROR":
let error = body["error"] as? String ?? "Unknown error"
print("💳 [PayWebView] IC_ERROR received: \(error)")
DispatchQueue.main.async { [weak self] in
self?.onError(error)
}
default:
print("💳 [PayWebView] Unknown message type: \(type)")
DispatchQueue.main.async { [weak self] in
self?.onError("Unknown message type: \(type)")
}
}
}

// Capture console.log from JavaScript
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
print("💳 [PayWebView] JS Alert: \(message)")
completionHandler()
}

// Handle navigation errors
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("💳 [PayWebView] Navigation failed: \(error)")
DispatchQueue.main.async { [weak self] in
self?.onError("Navigation failed: \(error.localizedDescription)")
}
}

func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
print("💳 [PayWebView] Failed to load: \(error)")
DispatchQueue.main.async { [weak self] in
self?.onError("Failed to load page: \(error.localizedDescription)")
}
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("💳 [PayWebView] Page loaded successfully")
}
}
}

#if DEBUG
struct PayDataCollectionWebView_Previews: PreviewProvider {
static var previews: some View {
PayDataCollectionWebView(
url: URL(string: "https://example.com")!,
onComplete: {},
onError: { _ in }
)
}
}
#endif
54 changes: 42 additions & 12 deletions Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import Commons

enum PayFlowStep: Int, CaseIterable {
case intro = 0
case nameInput = 1
case dateOfBirth = 2
case confirmation = 3
case confirming = 4 // Loading state while payment is being processed
case success = 5
case webviewDataCollection = 1 // WebView IC when collectData.url is present
case nameInput = 2
case dateOfBirth = 3
case confirmation = 4
case confirming = 5 // Loading state while payment is being processed
case success = 6
}

final class PayPresenter: ObservableObject {
Expand All @@ -31,6 +32,10 @@ final class PayPresenter: ObservableObject {
// User info for travel rule
@Published var firstName: String = ""
@Published var lastName: String = ""

// Payment result info (from confirmPayment response)
@Published var paymentResultInfo: ConfirmPaymentResultResponse?

@Published var dateOfBirth: Date = {
// Default to 1990-01-01
var components = DateComponents()
Expand Down Expand Up @@ -95,14 +100,29 @@ final class PayPresenter: ObservableObject {
let collectData = paymentOptionsResponse?.collectData
print("💳 [Pay] startFlow - collectData: \(String(describing: collectData))")
print("💳 [Pay] startFlow - paymentOptionsResponse: \(String(describing: paymentOptionsResponse))")

if collectData != nil {

if let webviewUrl = collectData?.url, !webviewUrl.isEmpty {
// Use WebView for IC (URL from API)
currentStep = .webviewDataCollection
} else if collectData != nil {
// Fallback: Field-by-field collection
currentStep = .nameInput
} else {
// No user data needed, go directly to confirmation
currentStep = .confirmation
}
}

/// Called when IC WebView completes successfully
func onICWebViewComplete() {
currentStep = .confirmation
}

/// Called when IC WebView encounters an error
func onICWebViewError(_ error: String) {
errorMessage = "Information capture failed: \(error)"
showError = true
}

func submitUserInfo() {
guard !firstName.isEmpty && !lastName.isEmpty else {
Expand All @@ -128,15 +148,22 @@ final class PayPresenter: ObservableObject {
switch currentStep {
case .intro:
dismiss()
case .webviewDataCollection:
currentStep = .intro
case .nameInput:
currentStep = .intro
case .dateOfBirth:
currentStep = .nameInput
case .confirmation:
// If info capture was required, go back to dateOfBirth
// Otherwise, go back to intro (skip info capture screens)
if paymentOptionsResponse?.collectData != nil {
currentStep = .dateOfBirth
// If info capture was required via WebView, go back to intro
// If info capture was required via fields, go back to dateOfBirth
// Otherwise, go back to intro
if let collectData = paymentOptionsResponse?.collectData {
if let webviewUrl = collectData.url, !webviewUrl.isEmpty {
currentStep = .intro
} else {
currentStep = .dateOfBirth
}
} else {
currentStep = .intro
}
Expand Down Expand Up @@ -182,8 +209,10 @@ final class PayPresenter: ObservableObject {
}

// 3. Collect user data if required (travel rule)
// Skip if data was collected via WebView (url is present)
var collectedData: [CollectDataFieldResult]? = nil
if let collectDataAction = paymentOptionsResponse?.collectData {
if let collectDataAction = paymentOptionsResponse?.collectData,
collectDataAction.url == nil || collectDataAction.url?.isEmpty == true {
collectedData = collectDataAction.fields.map { field -> CollectDataFieldResult in
let value = resolveFieldValue(for: field)
return CollectDataFieldResult(id: field.id, value: value)
Expand All @@ -200,6 +229,7 @@ final class PayPresenter: ObservableObject {
)

print("Payment confirmed: \(result)")
self.paymentResultInfo = result
self.currentStep = .success

// Notify balances screen to refresh
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import WalletConnectPay

struct PaySuccessView: View {
@EnvironmentObject var presenter: PayPresenter
Expand Down Expand Up @@ -55,7 +56,34 @@ struct PaySuccessView: View {
.foregroundColor(.grey8)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)


// Payment result info (txId and token amount)
if let resultInfo = presenter.paymentResultInfo?.info {
VStack(spacing: 8) {
HStack {
Text("Paid")
.foregroundColor(.secondary)
Spacer()
Text(formatTokenAmount(resultInfo.optionAmount))
.fontWeight(.medium)
.foregroundColor(.grey8)
}
HStack {
Text("Transaction")
.foregroundColor(.secondary)
Spacer()
Text(truncatedTxId(resultInfo.txId))
.foregroundColor(.blue)
}
}
.font(.system(size: 14, design: .rounded))
.padding()
.background(Color.grey95.opacity(0.5))
.cornerRadius(12)
.padding(.horizontal, 20)
.padding(.top, 12)
}

Spacer()
.frame(minHeight: 30, maxHeight: 50)

Expand Down Expand Up @@ -96,6 +124,29 @@ struct PaySuccessView: View {
private var formattedAmount: String {
presenter.paymentInfo?.formattedAmount ?? "$0.00"
}

/// Format token amount from PaymentResultInfo
private func formatTokenAmount(_ amount: PayAmount) -> String {
let value = Double(amount.value) ?? 0
let decimals = Int(amount.display.decimals)
let displayValue = value / pow(10.0, Double(decimals))

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 6

let formatted = formatter.string(from: NSNumber(value: displayValue)) ?? "\(displayValue)"
return "\(formatted) \(amount.display.assetSymbol)"
}

/// Truncate transaction ID for display (first 6 and last 4 characters)
private func truncatedTxId(_ txId: String) -> String {
guard txId.count > 10 else { return txId }
let prefix = String(txId.prefix(6))
let suffix = String(txId.suffix(4))
return "\(prefix)...\(suffix)"
}
}

#if DEBUG
Expand Down
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var dependencies: [Package.Dependency] = [
if yttriumDebug {
dependencies.append(.package(path: "../yttrium"))
} else {
dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.10.25")))
dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.10.28")))
}

let yttriumTarget = buildYttriumWrapperTarget()
Expand Down
Loading