diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 95eca08c..ad86964a 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -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 */ @@ -618,6 +619,7 @@ PAY0000022EE100000000000A /* PayDateOfBirthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDateOfBirthView.swift; sourceTree = ""; }; PAY0000022EE100000000000B /* PaySuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaySuccessView.swift; sourceTree = ""; }; PAY0000022EE100000000000C /* PayConfirmingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayConfirmingView.swift; sourceTree = ""; }; + PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayDataCollectionWebView.swift; sourceTree = ""; }; SIGNTD0002000000000001 /* SignTypedDataSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignTypedDataSigner.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1627,6 +1629,7 @@ PAY0000022EE1000000000009 /* PayConfirmView.swift */, PAY0000022EE100000000000B /* PaySuccessView.swift */, PAY0000022EE100000000000C /* PayConfirmingView.swift */, + PAY0000022EE100000000000D /* PayDataCollectionWebView.swift */, ); path = Pay; sourceTree = ""; @@ -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 */, diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 06093385..19cadd89 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -276,8 +276,8 @@ "repositoryURL": "https://github.com/reown-com/yttrium", "state": { "branch": null, - "revision": "f995616cb474a5ee8b7e422f6ba24e523afffa8d", - "version": "0.10.30" + "revision": "2f2b9dbf293b6c30d4a6b413ceaf14ea6244f295", + "version": "0.10.32" } } ] diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayConfirmView.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayConfirmView.swift index dcfb78e5..07234624 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayConfirmView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayConfirmView.swift @@ -46,37 +46,39 @@ struct PayConfirmView: View { .padding(.top, 16) if let info = presenter.paymentInfo { - // Merchant icon - if let iconUrl = info.merchant.iconUrl { - AsyncImage(url: URL(string: iconUrl)) { phase in - if let image = phase.image { - image - .resizable() - .aspectRatio(contentMode: .fit) - } else { - merchantPlaceholder(name: info.merchant.name) + // Merchant icon with verified badge + ZStack(alignment: .bottomTrailing) { + if let iconUrl = info.merchant.iconUrl { + AsyncImage(url: URL(string: iconUrl)) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + } else { + merchantPlaceholder(name: info.merchant.name) + } } - } - .frame(width: 64, height: 64) - .cornerRadius(12) - .padding(.top, 16) - } else { - merchantPlaceholder(name: info.merchant.name) .frame(width: 64, height: 64) - .padding(.top, 16) - } - - // Payment title - HStack(spacing: 6) { - Text("Pay \(info.formattedAmount) to \(info.merchant.name)") - .font(.system(size: 18, weight: .semibold, design: .rounded)) - .foregroundColor(.grey8) - + .cornerRadius(12) + } else { + merchantPlaceholder(name: info.merchant.name) + .frame(width: 64, height: 64) + } + + // Verified badge Image(systemName: "checkmark.seal.fill") - .font(.system(size: 14)) + .font(.system(size: 20)) .foregroundColor(.blue) + .background(Circle().fill(Color.white).frame(width: 16, height: 16)) + .offset(x: 4, y: 4) } .padding(.top, 16) + + // Payment title + Text("Pay \(info.formattedAmount) to \(info.merchant.name)") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundColor(.grey8) + .padding(.top, 16) // Payment details VStack(spacing: 0) { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayContainerView.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayContainerView.swift index b83b3570..40534d27 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayContainerView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayContainerView.swift @@ -21,6 +21,20 @@ struct PayContainerView: View { case .intro: PayIntroView() .environmentObject(presenter) + case .webviewDataCollection: + if let url = presenter.buildICWebViewURL() { + PayDataCollectionWebView( + url: url, + onClose: { presenter.goBack() }, + onComplete: { presenter.onICWebViewComplete() }, + onError: { error in presenter.onICWebViewError(error) }, + onFormDataChanged: { fullName, dob, pobAddress in presenter.onICFormDataChanged(fullName: fullName, dob: dob, pobAddress: pobAddress) } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.top, 50) + .background(Color.whiteBackground) + .ignoresSafeArea(edges: .bottom) + } case .nameInput: PayNameInputView() .environmentObject(presenter) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift new file mode 100644 index 00000000..6a16e417 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayDataCollectionWebView.swift @@ -0,0 +1,218 @@ +import SwiftUI +import WebKit + +struct PayDataCollectionWebView: View { + let url: URL + let onClose: () -> Void + let onComplete: () -> Void + let onError: (String) -> Void + let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void + + @State private var isLoading = true + + var body: some View { + ZStack(alignment: .topTrailing) { + PayWebViewRepresentable( + url: url, + isLoading: $isLoading, + onComplete: onComplete, + onError: onError, + onFormDataChanged: onFormDataChanged + ) + + // Close button + Button(action: onClose) { + Image(systemName: "xmark") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.gray) + .frame(width: 28, height: 28) + .background(Color.white.opacity(0.9)) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) + } + .padding(.top, 56) + .padding(.trailing, 20) + + if isLoading { + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.2) + Text("Loading...") + .font(.system(size: 14, design: .rounded)) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.white) + } + } + } +} + +private struct PayWebViewRepresentable: UIViewRepresentable { + let url: URL + @Binding var isLoading: Bool + let onComplete: () -> Void + let onError: (String) -> Void + let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(isLoading: $isLoading, onComplete: onComplete, onError: onError, onFormDataChanged: onFormDataChanged) + } + + 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 + webView.isOpaque = false + + print("💳 [PayWebView] Loading URL: \(url)") + webView.load(URLRequest(url: url)) + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate { + @Binding var isLoading: Bool + let onComplete: () -> Void + let onError: (String) -> Void + let onFormDataChanged: (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void + + init(isLoading: Binding, onComplete: @escaping () -> Void, onError: @escaping (String) -> Void, onFormDataChanged: @escaping (_ fullName: String?, _ dob: String?, _ pobAddress: String?) -> Void) { + self._isLoading = isLoading + self.onComplete = onComplete + self.onError = onError + self.onFormDataChanged = onFormDataChanged + } + + // 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 + // Extract form data from completion message (same fields as prefill) + let fullName = body["fullName"] as? String + let dob = body["dob"] as? String + let pobAddress = body["pobAddress"] as? String + print("💳 [PayWebView] IC_COMPLETE received, success: \(success), fullName: \(fullName ?? "nil"), dob: \(dob ?? "nil"), pobAddress: \(pobAddress ?? "nil")") + if success { + DispatchQueue.main.async { [weak self] in + self?.onFormDataChanged(fullName, dob, pobAddress) + 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) + } + case "IC_FORM_DATA": + let fullName = body["fullName"] as? String + let dob = body["dob"] as? String + let pobAddress = body["pobAddress"] as? String + print("💳 [PayWebView] IC_FORM_DATA received - fullName: \(fullName ?? "nil"), dob: \(dob ?? "nil"), pobAddress: \(pobAddress ?? "nil")") + DispatchQueue.main.async { [weak self] in + self?.onFormDataChanged(fullName, dob, pobAddress) + } + default: + // Ignore unknown message types silently (don't treat as error) + print("💳 [PayWebView] 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() + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + DispatchQueue.main.async { [weak self] in + self?.isLoading = true + } + } + + // 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?.isLoading = false + 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?.isLoading = false + self?.onError("Failed to load page: \(error.localizedDescription)") + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + print("💳 [PayWebView] Page loaded successfully") + DispatchQueue.main.async { [weak self] in + self?.isLoading = false + } + } + + // Handle link clicks - open external links in Safari + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + + // Allow initial page load and same-origin navigations + if navigationAction.navigationType == .other { + decisionHandler(.allow) + return + } + + // For link clicks, open in Safari + if navigationAction.navigationType == .linkActivated { + print("💳 [PayWebView] Opening external link in Safari: \(url)") + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } + } +} + +#if DEBUG +struct PayDataCollectionWebView_Previews: PreviewProvider { + static var previews: some View { + PayDataCollectionWebView( + url: URL(string: "https://example.com")!, + onClose: {}, + onComplete: {}, + onError: { _ in }, + onFormDataChanged: { _, _, _ in } + ) + } +} +#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayIntroView.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayIntroView.swift index 51e2015c..44b0c9bd 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayIntroView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayIntroView.swift @@ -36,37 +36,39 @@ struct PayIntroView: View { // No payment options available noPaymentOptionsView } else if let info = presenter.paymentInfo { - // Merchant icon - if let iconUrl = info.merchant.iconUrl { - AsyncImage(url: URL(string: iconUrl)) { phase in - if let image = phase.image { - image - .resizable() - .aspectRatio(contentMode: .fit) - } else { - merchantPlaceholder(name: info.merchant.name) + // Merchant icon with verified badge + ZStack(alignment: .bottomTrailing) { + if let iconUrl = info.merchant.iconUrl { + AsyncImage(url: URL(string: iconUrl)) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + } else { + merchantPlaceholder(name: info.merchant.name) + } } - } - .frame(width: 64, height: 64) - .cornerRadius(12) - .padding(.top, 16) - } else { - merchantPlaceholder(name: info.merchant.name) .frame(width: 64, height: 64) - .padding(.top, 16) - } - - // Payment title - HStack(spacing: 6) { - Text("Pay \(info.formattedAmount) to \(info.merchant.name)") - .font(.system(size: 18, weight: .semibold, design: .rounded)) - .foregroundColor(.grey8) - + .cornerRadius(12) + } else { + merchantPlaceholder(name: info.merchant.name) + .frame(width: 64, height: 64) + } + + // Verified badge Image(systemName: "checkmark.seal.fill") - .font(.system(size: 14)) + .font(.system(size: 20)) .foregroundColor(.blue) + .background(Circle().fill(Color.white).frame(width: 16, height: 16)) + .offset(x: 4, y: 4) } .padding(.top, 16) + + // Payment title + Text("Pay \(info.formattedAmount) to \(info.merchant.name)") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundColor(.grey8) + .padding(.top, 16) // Steps VStack(spacing: 16) { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift index 70ca70a5..8adb187a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PayPresenter.swift @@ -5,18 +5,29 @@ 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 { private let router: PayRouter private let importAccount: ImportAccount private var disposeBag = Set() - + + // Default test user data for IC form prefill (PoC) + private static let defaultPrefillFullName = "Test User" + private static let defaultPrefillDob = "1990-01-15" + private static let defaultPrefillPobAddress = "New York, USA" + + // User-entered IC form data (captured from WebView) + var icFormFullName: String? + var icFormDob: String? + var icFormPobAddress: String? + // Flow state @Published var currentStep: PayFlowStep = .intro @Published var isLoading = false @@ -31,6 +42,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() @@ -101,14 +116,107 @@ 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 + } + + /// Called when IC WebView reports form data changes + func onICFormDataChanged(fullName: String?, dob: String?, pobAddress: String?) { + if let fullName = fullName, !fullName.isEmpty { + icFormFullName = fullName + } + if let dob = dob, !dob.isEmpty { + icFormDob = dob + } + if let pobAddress = pobAddress, !pobAddress.isEmpty { + icFormPobAddress = pobAddress + } + print("💳 [Pay] IC form data updated - fullName: \(icFormFullName ?? "nil"), dob: \(icFormDob ?? "nil"), pobAddress: \(icFormPobAddress ?? "nil")") + } + + /// Build IC WebView URL with prefill query parameter + func buildICWebViewURL() -> URL? { + guard let baseUrlString = paymentOptionsResponse?.collectData?.url, + !baseUrlString.isEmpty else { + return nil + } + + let schema = paymentOptionsResponse?.collectData?.schema + guard let prefillParam = buildPrefillParam(schema: schema) else { + return URL(string: baseUrlString) + } + + guard var components = URLComponents(string: baseUrlString) else { + return URL(string: baseUrlString) + } + + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "prefill", value: prefillParam)) + components.queryItems = queryItems + + return components.url + } + + /// Build Base64-encoded prefill JSON based on schema's required fields + private func buildPrefillParam(schema: String?) -> String? { + guard let schema = schema else { return nil } + + // Parse schema JSON + guard let schemaData = schema.data(using: .utf8), + let schemaJson = try? JSONSerialization.jsonObject(with: schemaData) as? [String: Any], + let requiredArray = schemaJson["required"] as? [String] else { + return nil + } + + // Build prefill data based on required fields + // Use user-entered values if available, otherwise fall back to defaults + var prefillData: [String: String] = [:] + + if requiredArray.contains("fullName") { + prefillData["fullName"] = icFormFullName ?? Self.defaultPrefillFullName + } + + if requiredArray.contains("dob") { + prefillData["dob"] = icFormDob ?? Self.defaultPrefillDob + } + + if requiredArray.contains("pobAddress") { + prefillData["pobAddress"] = icFormPobAddress ?? Self.defaultPrefillPobAddress + } + + // Only return if we have data to prefill + guard !prefillData.isEmpty else { return nil } + + // Encode to JSON and Base64 + guard let jsonData = try? JSONSerialization.data(withJSONObject: prefillData), + let base64 = jsonData.base64EncodedString() + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return nil + } + + print("💳 [Pay] Built prefill param: \(prefillData) -> \(base64)") + return base64 + } func submitUserInfo() { guard !firstName.isEmpty && !lastName.isEmpty else { @@ -134,15 +242,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 + // If info capture was required via WebView, go back to WebView + // If info capture was required via fields, go back to dateOfBirth // If no data collection required (intro was skipped), dismiss entirely - if paymentOptionsResponse?.collectData != nil { - currentStep = .dateOfBirth + if let collectData = paymentOptionsResponse?.collectData { + if let webviewUrl = collectData.url, !webviewUrl.isEmpty { + currentStep = .webviewDataCollection + } else { + currentStep = .dateOfBirth + } } else { dismiss() } @@ -188,8 +303,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) @@ -206,6 +323,7 @@ final class PayPresenter: ObservableObject { ) print("Payment confirmed: \(result)") + self.paymentResultInfo = result self.currentStep = .success // Notify balances screen to refresh diff --git a/Example/WalletApp/PresentationLayer/Wallet/Pay/PaySuccessView.swift b/Example/WalletApp/PresentationLayer/Wallet/Pay/PaySuccessView.swift index a8b01ad3..5ed64a32 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Pay/PaySuccessView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Pay/PaySuccessView.swift @@ -1,4 +1,5 @@ import SwiftUI +import WalletConnectPay struct PaySuccessView: View { @EnvironmentObject var presenter: PayPresenter @@ -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) @@ -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 diff --git a/Package.resolved b/Package.resolved index 93e089f5..392648b6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/reown-com/yttrium", "state": { "branch": null, - "revision": "f995616cb474a5ee8b7e422f6ba24e523afffa8d", - "version": "0.10.30" + "revision": "2f2b9dbf293b6c30d4a6b413ceaf14ea6244f295", + "version": "0.10.32" } } ] diff --git a/Package.swift b/Package.swift index b8c63a8d..25c8af48 100644 --- a/Package.swift +++ b/Package.swift @@ -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.30"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.10.32"))) } let yttriumTarget = buildYttriumWrapperTarget()