diff --git a/.github/actions/appetize-build/action.yml b/.github/actions/appetize-build/action.yml
index ff43db8a33..b7b8be43ac 100644
--- a/.github/actions/appetize-build/action.yml
+++ b/.github/actions/appetize-build/action.yml
@@ -49,6 +49,9 @@ inputs:
pr-number:
description: PR number
required: true
+ stripe-publishable-key:
+ description: 'Stripe publishable key'
+ required: true
outputs:
appetize-url:
description: Appetize URL
@@ -80,6 +83,10 @@ runs:
- name: Install SMB
shell: bash
run: npm install --save slack-message-builder
+ - name: Create secrets.defaults.properties
+ shell: bash
+ run: |
+ echo "STRIPE_PUBLISHABLE_KEY=${{ inputs.stripe-publishable-key }}" > Debug\ App/secrets.defaults.properties
- name: Test, Build, and Distribute app to Appetize 🚀
shell: bash
run: |
diff --git a/.github/workflows/build-test-upload.yml b/.github/workflows/build-test-upload.yml
index 66789ac128..bc1247b370 100644
--- a/.github/workflows/build-test-upload.yml
+++ b/.github/workflows/build-test-upload.yml
@@ -251,6 +251,7 @@ jobs:
slack-channel: ${{ secrets.SLACK_MOBILE_SDK_CHANNEL }}
slack-reporter-token: ${{ secrets.SLACK_REPORTER_BOT_TOKEN }}
github-run-id: ${{ github.run_id }}
+ stripe-publishable-key: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
- uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e #v3.1.0
if: ${{ success() }}
id: find_comment
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 959bccf524..74958b3c0f 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -53,6 +53,7 @@ jobs:
slack-channel: ${{ secrets.SLACK_MOBILE_SDK_CHANNEL }}
slack-reporter-token: ${{ secrets.SLACK_REPORTER_BOT_TOKEN }}
github-run-id: ${{ github.run_id }}
+ stripe-publishable-key: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
build-and-upload-to-firebase-and-browserstack:
runs-on: macos-13
timeout-minutes: 20
@@ -96,6 +97,11 @@ jobs:
env:
FIREBASE_CREDENTIALS: ${{ secrets.FIREBASE_CREDENTIALS }}
+ - name: Create secrets.defaults.properties
+ shell: bash
+ run: |
+ echo "STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }}" > Debug\ App/secrets.defaults.properties
+
- name: Distribute internally on Firebase and upload to Browserstack 🚀
run: |
bundle exec fastlane qa_release
diff --git a/.gitignore b/.gitignore
index fbb2dd48a2..57dcd29802 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,7 +58,7 @@ playground.xcworkspace
.build/
Package.swift.orig
-
+Package.resolved
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Debug App/Info.plist b/Debug App/Info.plist
index 846f6bc115..4ef3fd64b0 100644
--- a/Debug App/Info.plist
+++ b/Debug App/Info.plist
@@ -69,8 +69,6 @@
UIInterfaceOrientationPortrait
- UIUserInterfaceStyle
- Light
com.apple.developer.nfc.readersession.formats
TAG
diff --git a/Debug App/Primer.io Debug App SPM.xcodeproj/project.pbxproj b/Debug App/Primer.io Debug App SPM.xcodeproj/project.pbxproj
index aad6e4e041..7d7823c369 100644
--- a/Debug App/Primer.io Debug App SPM.xcodeproj/project.pbxproj
+++ b/Debug App/Primer.io Debug App SPM.xcodeproj/project.pbxproj
@@ -29,7 +29,6 @@
E11F475D2B06C5A40091C31F /* BanksListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11F47592B06C5A40091C31F /* BanksListView.swift */; };
E11F475E2B06C5A40091C31F /* BanksListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11F475A2B06C5A40091C31F /* BanksListModel.swift */; };
E11F475F2B06C5A40091C31F /* ImageViewWithUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11F475B2B06C5A40091C31F /* ImageViewWithUrl.swift */; };
- E1AB44D02AFE139600639DC5 /* URL+NetworkHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB44CF2AFE139600639DC5 /* URL+NetworkHeaders.swift */; };
F00C65F82ACC67FA00187028 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC5687FF32E8661F1A00CE5 /* AppDelegate.swift */; };
F00C65F92ACC67FA00187028 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1594BC5C96ECC3F46C811B2F /* Data+Extensions.swift */; };
F00C65FA2ACC67FA00187028 /* Range+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9128566126AA7F571FFECA3A /* Range+Extensions.swift */; };
@@ -62,6 +61,7 @@
F08F63E02BA0BE83006EF9A9 /* SessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63DF2BA0BE83006EF9A9 /* SessionConfiguration.swift */; };
F08F63E32BA0BEAD006EF9A9 /* AppetizeConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63E12BA0BEAD006EF9A9 /* AppetizeConfigProvider.swift */; };
F08F63E42BA0BEAD006EF9A9 /* MetadataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63E22BA0BEAD006EF9A9 /* MetadataParser.swift */; };
+ F09978842D0326860058E4B5 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09978832D0326860058E4B5 /* Metadata.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -158,6 +158,7 @@
F08F63DF2BA0BE83006EF9A9 /* SessionConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionConfiguration.swift; sourceTree = ""; };
F08F63E12BA0BEAD006EF9A9 /* AppetizeConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppetizeConfigProvider.swift; sourceTree = ""; };
F08F63E22BA0BEAD006EF9A9 /* MetadataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = ""; };
+ F09978832D0326860058E4B5 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Metadata.swift; path = Sources/Model/Metadata.swift; sourceTree = ""; };
F816A2444633C4336A7CB071 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; };
F9023841AFCE8E3205CB713A /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; };
FB1F71737862EF5D0F4FE5AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
@@ -243,6 +244,7 @@
1C9799A622D0CCDBBF94925B = {
isa = PBXGroup;
children = (
+ F09978832D0326860058E4B5 /* Metadata.swift */,
F04510DE2ACD4EA500A0A48C /* primer-sdk-ios */,
FBDF3F8B5F93A0EC28048640 /* Project */,
DF30711EB149C64C364BB79A /* Products */,
@@ -493,6 +495,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ F09978842D0326860058E4B5 /* Metadata.swift in Sources */,
8723ADE42B8E0FAB00A5FE23 /* MerchantHeadlessKlarnaInitializationView.swift in Sources */,
F08F63E42BA0BEAD006EF9A9 /* MetadataParser.swift in Sources */,
044F805D2C6B4F9800E9F878 /* MerchantHeadlessStripeAchFieldsViewModel.swift in Sources */,
@@ -515,7 +518,6 @@
8723ADDD2B8E0F5100A5FE23 /* MerchantHelpers.swift in Sources */,
F00C66022ACC67FA00187028 /* TestScenario.swift in Sources */,
F00C66032ACC67FA00187028 /* TransactionResponse.swift in Sources */,
- E1AB44D02AFE139600639DC5 /* URL+NetworkHeaders.swift in Sources */,
E11F475D2B06C5A40091C31F /* BanksListView.swift in Sources */,
F00C66052ACC67FA00187028 /* Networking.swift in Sources */,
F00C66062ACC67FA00187028 /* MerchantDropInUIViewController.swift in Sources */,
diff --git a/Debug App/Primer.io Debug App.xcodeproj/project.pbxproj b/Debug App/Primer.io Debug App.xcodeproj/project.pbxproj
index fd4db6159e..25bf41ca0d 100644
--- a/Debug App/Primer.io Debug App.xcodeproj/project.pbxproj
+++ b/Debug App/Primer.io Debug App.xcodeproj/project.pbxproj
@@ -57,13 +57,13 @@
DE53DA2D0AD108306C92E198 /* UIViewController+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FD8065D40A2D691F643F3B /* UIViewController+API.swift */; };
E11F47542B06C5030091C31F /* BanksListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11F47502B06C5030091C31F /* BanksListView.swift */; };
E11F47562B06C5030091C31F /* ImageViewWithUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11F47522B06C5030091C31F /* ImageViewWithUrl.swift */; };
- E1A2971F2B021ABA005ADA51 /* URL+NetworkHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AB44D12AFE13AF00639DC5 /* URL+NetworkHeaders.swift */; };
EA7FAA4F8476BD3711D628CB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B18D7E7738BF86467B0F1465 /* Images.xcassets */; };
F02F496FD20B5291C044F62C /* MerchantResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E3C8FE62D22147335F2455 /* MerchantResultViewController.swift */; };
F03699592AC2E63700E4179D /* BuildFile in Sources */ = {isa = PBXBuildFile; };
F08F63D82B9B5A7C006EF9A9 /* SessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63D72B9B5A7C006EF9A9 /* SessionConfiguration.swift */; };
F08F63DA2B9B5BC5006EF9A9 /* AppetizeConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63D92B9B5BC5006EF9A9 /* AppetizeConfigProvider.swift */; };
F08F63DC2B9F27B0006EF9A9 /* MetadataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08F63DB2B9F27B0006EF9A9 /* MetadataParser.swift */; };
+ F09978822D00B7620058E4B5 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09978812D00B7620058E4B5 /* Metadata.swift */; };
F0C2147F6FA26527BE55549A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = FC701AFD94F96F0F1D108D1A /* LaunchScreen.xib */; };
FD5ADBCFA70DB606339F3AF2 /* TransactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D3D6CF0F006A06B7CEC71B /* TransactionResponse.swift */; };
/* End PBXBuildFile section */
@@ -200,7 +200,6 @@
E11F47512B06C5030091C31F /* BanksListModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BanksListModel.swift; sourceTree = ""; };
E11F47522B06C5030091C31F /* ImageViewWithUrl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewWithUrl.swift; sourceTree = ""; };
E129D66F2B162022004694F9 /* MerchantHeadlessCheckoutBankViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MerchantHeadlessCheckoutBankViewController.swift; sourceTree = ""; };
- E1AB44D12AFE13AF00639DC5 /* URL+NetworkHeaders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+NetworkHeaders.swift"; sourceTree = ""; };
E1C3EF0BA039C0A50EDE13A5 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Main.strings; sourceTree = ""; };
E3F4FB59DFC387F26CA944EB /* Pods_Debug_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Debug_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E7640DB186F9638C2F556F77 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = ""; };
@@ -208,6 +207,7 @@
F08F63D72B9B5A7C006EF9A9 /* SessionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConfiguration.swift; sourceTree = ""; };
F08F63D92B9B5BC5006EF9A9 /* AppetizeConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppetizeConfigProvider.swift; sourceTree = ""; };
F08F63DB2B9F27B0006EF9A9 /* MetadataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = ""; };
+ F09978812D00B7620058E4B5 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; };
F816A2444633C4336A7CB071 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; };
F9023841AFCE8E3205CB713A /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; };
FB1F71737862EF5D0F4FE5AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
@@ -374,7 +374,6 @@
isa = PBXGroup;
children = (
E8DE1E4FB055B60582977315 /* Networking.swift */,
- E1AB44D12AFE13AF00639DC5 /* URL+NetworkHeaders.swift */,
);
path = Network;
sourceTree = "";
@@ -418,6 +417,7 @@
A6AEF11B151368BF993C3EA9 /* TestScenario.swift */,
70D3D6CF0F006A06B7CEC71B /* TransactionResponse.swift */,
F08F63D72B9B5A7C006EF9A9 /* SessionConfiguration.swift */,
+ F09978812D00B7620058E4B5 /* Metadata.swift */,
);
path = Model;
sourceTree = "";
@@ -693,11 +693,11 @@
AAE3B30B64B6822A20987FCA /* CreateClientToken.swift in Sources */,
F08F63D82B9B5A7C006EF9A9 /* SessionConfiguration.swift in Sources */,
87FC1ACB2BE5194100C9F474 /* MerchantHeadlessStripeAchFieldsViewModel.swift in Sources */,
+ F09978822D00B7620058E4B5 /* Metadata.swift in Sources */,
04F323652BD40A0E00F5927C /* MerchantHeadlessCheckoutBankViewController.swift in Sources */,
876141082B8346650058CA8C /* MerchantHeadlessKlarnaInitializationView.swift in Sources */,
C75A11E6AEEFC2B7A29BBC04 /* TestScenario.swift in Sources */,
F08F63DC2B9F27B0006EF9A9 /* MetadataParser.swift in Sources */,
- E1A2971F2B021ABA005ADA51 /* URL+NetworkHeaders.swift in Sources */,
FD5ADBCFA70DB606339F3AF2 /* TransactionResponse.swift in Sources */,
876140D52B63F7DB0058CA8C /* MerchantHeadlessCheckoutKlarnaViewController.swift in Sources */,
049A055E2B4BF057002CEEBA /* MerchantHeadlessVaultManagerViewController.swift in Sources */,
diff --git a/Debug App/Resources/Localized Views/Base.lproj/Main.storyboard b/Debug App/Resources/Localized Views/Base.lproj/Main.storyboard
index a1d5cf8240..a59e62704e 100644
--- a/Debug App/Resources/Localized Views/Base.lproj/Main.storyboard
+++ b/Debug App/Resources/Localized Views/Base.lproj/Main.storyboard
@@ -173,14 +173,14 @@
-
+
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
+
-
+
-
+
+
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
-
-
-
+
+
-
-
+
+
-
+
@@ -571,7 +567,7 @@
-
+
@@ -643,8 +639,8 @@
-
-
+
+
@@ -661,7 +657,7 @@
-
+
@@ -746,8 +742,8 @@
-
-
+
+
@@ -765,8 +761,8 @@
-
-
+
+
@@ -876,8 +872,8 @@
-
-
+
+
@@ -1277,8 +1273,8 @@
-
-
+
+
@@ -1386,11 +1382,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -1415,7 +1411,7 @@
-
+
@@ -1430,7 +1426,7 @@
-
+
@@ -1466,9 +1462,7 @@
-
-
-
+
@@ -1983,13 +1977,13 @@
-
+
-
+
diff --git a/Debug App/Sources/Model/CreateClientToken.swift b/Debug App/Sources/Model/CreateClientToken.swift
index d387591abc..15d9f304c9 100644
--- a/Debug App/Sources/Model/CreateClientToken.swift
+++ b/Debug App/Sources/Model/CreateClientToken.swift
@@ -55,162 +55,19 @@ enum Environment: String, Codable {
}
-struct CreateClientTokenRequest: Codable {
- let orderId: String
- let amount: Int?
- let currencyCode: String
- let customerId: String?
- let metadata: [String: String]?
- let customer: Customer?
- let order: Order?
- let paymentMethod: PaymentMethod?
-}
-
-public struct Customer: Codable {
- let firstName: String?
- let lastName: String?
- let emailAddress: String?
- let billingAddress: Address?
- let shippingAddress: Address?
- let mobileNumber: String?
- let nationalDocumentId: String?
-}
-
-public struct LineItem: Codable {
- let itemId: String?
- let description: String?
- let amount: Int?
- let discountAmount: Int?
- let quantity: Int?
- let taxAmount: Int?
- let taxCode: String?
-
- public init (
- itemId: String?,
- description: String?,
- amount: Int?,
- discountAmount: Int?,
- quantity: Int?,
- taxAmount: Int?,
- taxCode: String?
- ) {
- self.itemId = itemId
- self.description = description
- self.amount = amount
- self.discountAmount = discountAmount
- self.quantity = quantity
- self.taxAmount = taxAmount
- self.taxCode = taxCode
- }
-}
-
-public struct Order: Codable {
- let countryCode: String?
- // let fees: Fees?
- let lineItems: [LineItem]?
- let shipping: Shipping?
-
- public init (
- countryCode: String?,
- // fees: Fees?,
- lineItems: [LineItem]?,
- shipping: Shipping?
- ) {
- self.countryCode = countryCode
- // self.fees = fees
- self.lineItems = lineItems
- self.shipping = shipping
- }
-}
-
-public struct Fees: Codable {
- let amount: UInt?
- let description: String?
-
- public init (
- amount: UInt?,
- description: String?
- ) {
- self.amount = amount
- self.description = description
- }
-}
-
-public struct Shipping: Codable {
- let amount: UInt
-
- public init(amount: UInt) {
- self.amount = amount
- }
-}
-
-public struct PaymentMethod: Codable {
- let vaultOnSuccess: Bool
-
- public init(vaultOnSuccess: Bool) {
- self.vaultOnSuccess = vaultOnSuccess
- }
-}
-
-struct ClientSessionRequestBody {
+struct ClientSessionRequestBody: Encodable {
var clientToken: String?
var customerId: String?
var orderId: String?
- var currencyCode: Currency?
+ var currencyCode: String?
var amount: Int?
- var metadata: [String: Any]?
+ var metadata: Metadata?
var customer: ClientSessionRequestBody.Customer?
var order: ClientSessionRequestBody.Order?
var paymentMethod: ClientSessionRequestBody.PaymentMethod?
var testParams: Test.Params?
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let clientToken = clientToken {
- dic["clientToken"] = clientToken
- }
-
- if let customerId = customerId {
- dic["customerId"] = customerId
- }
-
- if let orderId = orderId {
- dic["orderId"] = orderId
- }
-
- if let currencyCode = currencyCode {
- dic["currencyCode"] = currencyCode.code
- }
-
- if let amount = amount {
- dic["amount"] = amount
- }
-
- if let metadata = metadata {
- dic["metadata"] = metadata
- }
-
- if let customer = customer {
- dic["customer"] = customer.dictionaryValue
- }
-
- if let order = order {
- dic["order"] = order.dictionaryValue
- }
-
- if let paymentMethod = paymentMethod {
- dic["paymentMethod"] = paymentMethod.dictionaryValue
- }
-
- if let testParams = testParams {
- dic["testParams"] = try? testParams.asDictionary()
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
-
struct Customer: Codable {
var firstName: String?
var lastName: String?
@@ -218,60 +75,12 @@ struct ClientSessionRequestBody {
var mobileNumber: String?
var billingAddress: Address?
var shippingAddress: Address?
-
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let firstName = firstName {
- dic["firstName"] = firstName
- }
-
- if let lastName = lastName {
- dic["lastName"] = lastName
- }
-
- if let emailAddress = emailAddress {
- dic["emailAddress"] = emailAddress
- }
-
- if let mobileNumber = mobileNumber {
- dic["mobileNumber"] = mobileNumber
- }
-
- if let mobileNumber = mobileNumber {
- dic["mobileNumber"] = mobileNumber
- }
-
- if let billingAddress = billingAddress {
- dic["billingAddress"] = billingAddress.dictionaryValue
- }
-
- if let shippingAddress = shippingAddress {
- dic["shippingAddress"] = shippingAddress.dictionaryValue
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
}
struct Order: Codable {
var countryCode: CountryCode?
var lineItems: [LineItem]?
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let countryCode = countryCode {
- dic["countryCode"] = countryCode.rawValue
- }
-
- if let lineItems = lineItems {
- dic["lineItems"] = lineItems.compactMap({ $0.dictionaryValue })
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
-
struct LineItem: Codable {
var itemId: String?
@@ -281,93 +90,20 @@ struct ClientSessionRequestBody {
var discountAmount: Int?
var taxAmount: Int?
var productType: String?
-
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let itemId = itemId {
- dic["itemId"] = itemId
- }
-
- if let description = description {
- dic["description"] = description
- }
-
- if let amount = amount {
- dic["amount"] = amount
- }
-
- if let quantity = quantity {
- dic["quantity"] = quantity
- }
-
- if let taxAmount = taxAmount {
- dic["taxAmount"] = taxAmount
- }
-
- if let discountAmount = discountAmount {
- dic["discountAmount"] = discountAmount
- }
-
- if let productType = productType {
- dic["productType"] = productType
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
}
}
struct PaymentMethod: Codable {
- let vaultOnSuccess: Bool?
+ var vaultOnSuccess: Bool?
+ var vaultOnAgreement: Bool?
var options: PaymentMethodOptionGroup?
let descriptor: String?
let paymentType: String?
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let vaultOnSuccess = vaultOnSuccess {
- dic["vaultOnSuccess"] = vaultOnSuccess
- }
-
- if let options = options {
- dic["options"] = options.dictionaryValue
- }
-
- if let descriptor = descriptor {
- dic["descriptor"] = descriptor
- }
-
- if let paymentType = paymentType {
- dic["paymentType"] = paymentType
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
-
struct PaymentMethodOptionGroup: Codable {
var KLARNA: PaymentMethodOption?
var PAYMENT_CARD: PaymentMethodOption?
var APPLE_PAY: PaymentMethodOption?
-
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let KLARNA = KLARNA {
- dic["KLARNA"] = KLARNA.dictionaryValue
- }
-
- if let PAYMENT_CARD = PAYMENT_CARD {
- dic["PAYMENT_CARD"] = PAYMENT_CARD.dictionaryValue
- }
-
- if let APPLE_PAY = APPLE_PAY {
- dic["APPLE_PAY"] = APPLE_PAY.dictionaryValue
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
}
struct PaymentMethodOption: Codable {
@@ -431,61 +167,15 @@ struct ClientSessionRequestBody {
captureVaultedCardCvv = try container.decodeIfPresent(Bool.self, forKey: .captureVaultedCardCvv) ?? false
}
-
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let surcharge = surcharge {
- dic["surcharge"] = surcharge.dictionaryValue
- }
-
- if let instalmentDuration = instalmentDuration {
- dic["instalmentDuration"] = instalmentDuration
- }
-
- if let extraMerchantData = extraMerchantData {
- dic["extraMerchantData"] = extraMerchantData
- }
-
- if let captureVaultedCardCvv = captureVaultedCardCvv, captureVaultedCardCvv == true {
- dic["captureVaultedCardCvv"] = captureVaultedCardCvv
- }
-
- if let merchantName = merchantName {
- dic["merchantName"] = merchantName
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
}
struct SurchargeOption: Codable {
var amount: Int?
-
- var dictionaryValue: [String: Any]? {
- var dic: [String: Any] = [:]
-
- if let amount = amount {
- dic["amount"] = amount
- }
-
- return dic.keys.count == 0 ? nil : dic
- }
}
}
}
-extension Encodable {
- func asDictionary() throws -> [String: Any] {
- let data = try JSONEncoder().encode(self)
- guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
- throw NSError()
- }
- return dictionary
- }
-}
-
extension String {
var jwtTokenPayload: JWTToken? {
diff --git a/Debug App/Sources/Model/Metadata.swift b/Debug App/Sources/Model/Metadata.swift
new file mode 100644
index 0000000000..c4bb84c2ae
--- /dev/null
+++ b/Debug App/Sources/Model/Metadata.swift
@@ -0,0 +1,50 @@
+//
+// Metadata.swift
+// Debug App
+//
+// Created by Niall Quinn on 04/12/24.
+// Copyright © 2024 Primer API Ltd. All rights reserved.
+//
+
+import Foundation
+
+enum Metadata: Codable {
+ case string(String)
+ case bool(Bool)
+ case dictionary([String: Metadata])
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ if let string = try? container.decode(String.self) {
+ self = .string(string)
+ } else if let bool = try? container.decode(Bool.self) {
+ self = .bool(bool)
+ } else if let dict = try? container.decode([String: Metadata].self) {
+ self = .dictionary(dict)
+ } else {
+ throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid metadata type")
+ }
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .string(let value): try container.encode(value)
+ case .bool(let value): try container.encode(value)
+ case .dictionary(let value): try container.encode(value)
+ }
+ }
+
+ mutating func add(_ value: Metadata, forKey key: String) throws {
+ if case .dictionary(var dict) = self {
+ dict[key] = value
+ self = .dictionary(dict)
+ } else {
+ throw MetadataError.notDictionary
+ }
+ }
+
+ enum MetadataError: Error {
+ case notDictionary
+ }
+}
diff --git a/Debug App/Sources/Model/SessionConfiguration.swift b/Debug App/Sources/Model/SessionConfiguration.swift
index 368744e037..007d74667b 100644
--- a/Debug App/Sources/Model/SessionConfiguration.swift
+++ b/Debug App/Sources/Model/SessionConfiguration.swift
@@ -29,7 +29,6 @@ struct SessionConfiguration: Codable, Equatable {
let postalCode: String
let vault: Bool
- let newWorkflows: Bool
let environment: String
let customApiKey: String
diff --git a/Debug App/Sources/Network/Networking.swift b/Debug App/Sources/Network/Networking.swift
index 886fd1a640..83e471262a 100644
--- a/Debug App/Sources/Network/Networking.swift
+++ b/Debug App/Sources/Network/Networking.swift
@@ -10,20 +10,19 @@ import Foundation
import PrimerSDK
enum APIVersion: String {
- case v2 = "2021-09-27"
- case v2_1 = "2.1"
- case v2_2 = "2.2"
- case v3 = "2021-10-19"
- case v4 = "2021-12-01"
- case v5 = "2021-12-10"
+ case v2 = "2021-09-27"
+ case v2_1 = "2.1"
+ case v2_2 = "2.2"
+ case v2_3 = "2.3"
+ case v2_4 = "2.4"
}
enum HTTPMethod: String {
- case get = "GET"
- case post = "POST"
- case put = "PUT"
+ case get = "GET"
+ case post = "POST"
+ case put = "PUT"
case delete = "DELETE"
- case patch = "PATCH"
+ case patch = "PATCH"
}
enum NetworkError: Error {
@@ -38,147 +37,161 @@ enum NetworkError: Error {
private let logger = PrimerLogging.shared.logger
class Networking {
-
+
var endpoint: String {
- get {
- if environment == .local {
- return "https://primer-mock-back-end.herokuapp.com"
- } else {
- return "https://us-central1-primerdemo-8741b.cloudfunctions.net"
- }
+ if environment == .local {
+ return "https://primer-mock-back-end.herokuapp.com"
+ } else {
+ return "https://us-central1-primerdemo-8741b.cloudfunctions.net"
}
}
-
+
func request(
apiVersion: APIVersion?,
url: URL,
method: HTTPMethod,
- headers: [String: String]?,
+ headers: [String: String]? = nil,
queryParameters: [String: String]?,
body: Data?,
- completion: @escaping (_ result: Result) -> Void) {
+ completion: @escaping (_ result: Result) -> Void
+ ) {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
-
+
if let queryParameters = queryParameters {
components.queryItems = queryParameters.map { (key, value) in
URLQueryItem(name: key, value: value)
}
}
- components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
-
+ components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(
+ of: "+", with: "%2B")
+
logger.debug(message: "URL: \(components.url!.absoluteString )")
-
+
var request = URLRequest(url: components.url!)
request.httpMethod = method.rawValue
-
+
request.addValue(environment.rawValue, forHTTPHeaderField: "environment")
if method != .get {
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
}
-
+
if let customDefinedApiKey = customDefinedApiKey {
request.addValue(customDefinedApiKey, forHTTPHeaderField: "x-api-key")
}
-
+
if let headers = headers {
// We have a dedicated argument that takes x-api-key into account
// in case a custom one gets defined before SDK initialization
// so in case this array contains the same key, it won't be added
- for header in headers.filter({ $0.value != "x-api-key"}) {
+ for header in headers.filter({ $0.key != "x-api-key" }) {
request.addValue(header.value, forHTTPHeaderField: header.key)
}
}
-
+
if let apiVersion = apiVersion {
request.addValue(apiVersion.rawValue, forHTTPHeaderField: "x-api-version")
request.addValue("IOS", forHTTPHeaderField: "Client")
}
-
- let headerDescriptions = request.allHTTPHeaderFields?.map { key, value in
+
+ let headerDescriptions =
+ request.allHTTPHeaderFields?.map { key, value in
return "\(key) = \(value)"
} ?? []
logger.debug(message: "Request Headers:\n\(headerDescriptions.joined(separator: "\n"))")
-
+
if let body = body {
request.httpBody = body
if let bodyJson = try? JSONSerialization.jsonObject(with: body, options: .allowFragments) {
logger.debug(message: "Request Body (json):\n\(bodyJson)")
}
}
-
- URLSession.shared.dataTask(with: request, completionHandler: { (data, response, err) in
- DispatchQueue.main.async {
- logger.debug(message: "Url: \(request.url?.absoluteString ?? "unknown")")
-
- if err != nil {
- logger.debug(message: "Error: \(err!)")
- completion(.failure(err!))
- return
- }
-
- guard let httpResponse = response as? HTTPURLResponse else {
- logger.debug(message: "Error: Invalid response")
- completion(.failure(NetworkError.invalidResponse))
- return
- }
-
- if httpResponse.statusCode < 200 || httpResponse.statusCode > 399 {
- logger.debug(message: "Status Code: \(httpResponse.statusCode)")
- if let data = data, let resJson = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: Any] {
- logger.debug(message: "Response Body (json):\n\(resJson)")
+
+ URLSession.shared.dataTask(
+ with: request,
+ completionHandler: { (data, response, err) in
+ DispatchQueue.main.async {
+ logger.debug(message: "Url: \(request.url?.absoluteString ?? "unknown")")
+
+ if err != nil {
+ logger.debug(message: "Error: \(err!)")
+ completion(.failure(err!))
+ return
}
-
- guard let data = data else {
- logger.error(message: "No data")
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ logger.debug(message: "Error: Invalid response")
completion(.failure(NetworkError.invalidResponse))
return
}
-
- do {
- let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
- logger.debug(message: "Response Body (json):\n\(json)")
- } catch {
- logger.error(message: "Failed to parse response body: \(error)")
+
+ if httpResponse.statusCode < 200 || httpResponse.statusCode > 399 {
+ logger.debug(message: "Status Code: \(httpResponse.statusCode)")
+ if let data = data,
+ let resJson =
+ (try? JSONSerialization.jsonObject(with: data, options: .allowFragments))
+ as? [String: Any]
+ {
+ logger.debug(message: "Response Body (json):\n\(resJson)")
+ }
+
+ guard let data = data else {
+ logger.error(message: "No data")
+ completion(.failure(NetworkError.invalidResponse))
+ return
+ }
+
+ do {
+ let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
+ logger.debug(message: "Response Body (json):\n\(json)")
+ } catch {
+ logger.error(message: "Failed to parse response body: \(error)")
+ }
+ completion(.failure(NetworkError.invalidResponse))
+ return
}
- completion(.failure(NetworkError.invalidResponse))
- return
- }
-
- guard let data = data else {
+
+ guard let data = data else {
+ logger.debug(message: "Status Code: \(httpResponse.statusCode)")
+ logger.debug(message: "Response Body: No data")
+ completion(.failure(NetworkError.invalidResponse))
+ return
+ }
+
logger.debug(message: "Status Code: \(httpResponse.statusCode)")
- logger.debug(message: "Response Body: No data")
- completion(.failure(NetworkError.invalidResponse))
- return
- }
-
- logger.debug(message: "Status Code: \(httpResponse.statusCode)")
- if let resJson = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: Any] {
- logger.debug(message: "Response Body (json):\n\(resJson)")
- } else {
- logger.debug(message: "Response Body (text):\n\(String(describing: String(data: data, encoding: .utf8)))")
+ if let resJson = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments))
+ as? [String: Any]
+ {
+ logger.debug(message: "Response Body (json):\n\(resJson)")
+ } else {
+ logger.debug(
+ message:
+ "Response Body (text):\n\(String(describing: String(data: data, encoding: .utf8)))")
+ }
+
+ completion(.success(data))
}
-
- completion(.success(data))
}
- }).resume()
+ ).resume()
}
-
- static func resumePayment(_ paymentId: String,
- withToken resumeToken: String,
- completion: @escaping (Payment.Response?, Error?) -> Void) {
+
+ static func resumePayment(
+ _ paymentId: String,
+ withToken resumeToken: String,
+ completion: @escaping (Payment.Response?, Error?) -> Void
+ ) {
let url = environment.baseUrl.appendingPathComponent("/api/payments/\(paymentId)/resume")
-
+
let body = Payment.ResumeRequest(token: resumeToken)
-
+
var bodyData: Data!
-
+
do {
bodyData = try JSONEncoder().encode(body)
} catch {
completion(nil, NetworkError.missingParams)
return
}
-
+
let networking = Networking()
networking.request(
apiVersion: nil,
@@ -186,7 +199,8 @@ class Networking {
method: .post,
headers: nil,
queryParameters: nil,
- body: bodyData) { result in
+ body: bodyData
+ ) { result in
switch result {
case .success(let data):
do {
@@ -195,42 +209,44 @@ class Networking {
} catch {
completion(nil, error)
}
-
+
case .failure(let err):
completion(nil, err)
}
}
}
-
+
static func createPayment(
with paymentMethodTokenData: PrimerPaymentMethodTokenData,
customDefinedApiKey: String? = nil,
completion: @escaping (Payment.Response?, Error?) -> Void
) {
let url = environment.baseUrl.appendingPathComponent("/api/payments/")
-
+
guard let token = paymentMethodTokenData.token else {
let err = PrimerError.invalidClientToken(
- userInfo: ["file": #file,
- "class": "\(Self.self)",
- "function": #function,
- "line": "\(#line)"],
+ userInfo: [
+ "file": #file,
+ "class": "\(Self.self)",
+ "function": #function,
+ "line": "\(#line)",
+ ],
diagnosticsId: UUID().uuidString)
completion(nil, err)
return
}
-
+
let body = Payment.CreateRequest(token: token)
-
+
var bodyData: Data!
-
+
do {
bodyData = try JSONEncoder().encode(body)
} catch {
completion(nil, NetworkError.missingParams)
return
}
-
+
let networking = Networking()
networking.request(
apiVersion: .v2,
@@ -238,7 +254,8 @@ class Networking {
method: .post,
headers: nil,
queryParameters: nil,
- body: bodyData) { result in
+ body: bodyData
+ ) { result in
switch result {
case .success(let data):
do {
@@ -247,49 +264,52 @@ class Networking {
} catch {
completion(nil, error)
}
-
+
case .failure(let err):
completion(nil, err)
}
}
}
-
- static func requestClientSession(requestBody: ClientSessionRequestBody, customDefinedApiKey: String? = nil, completion: @escaping (String?, Error?) -> Void) {
+
+ static func requestClientSession(
+ requestBody: ClientSessionRequestBody, customDefinedApiKey: String? = nil,
+ completion: @escaping (String?, Error?) -> Void
+ ) {
let url = environment.baseUrl.appendingPathComponent("/api/client-session")
-
+
var bodyData: Data!
-
+
do {
- if let requestBodyJson = requestBody.dictionaryValue {
- bodyData = try JSONSerialization.data(withJSONObject: requestBodyJson, options: .fragmentsAllowed)
- } else {
- completion(nil, NetworkError.serializationError)
- return
- }
+ let encoder = JSONEncoder()
+ bodyData = try encoder.encode(requestBody)
} catch {
completion(nil, NetworkError.missingParams)
return
}
-
+
let networking = Networking()
networking.request(
- apiVersion: .v2_2,
+ apiVersion: .v2_4,
url: url,
method: .post,
- headers: URL.requestSessionHTTPHeaders(useNewWorkflows: useNewWorkflows),
queryParameters: nil,
body: bodyData
) { result in
switch result {
case .success(let data):
do {
- if let token = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any])?["clientToken"] as? String {
+ if let token =
+ (try JSONSerialization.jsonObject(with: data, options: .allowFragments)
+ as? [String: Any])?["clientToken"] as? String
+ {
completion(token, nil)
} else {
- let err = NSError(domain: "example", code: 10, userInfo: [NSLocalizedDescriptionKey: "Failed to find client token"])
+ let err = NSError(
+ domain: "example", code: 10,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to find client token"])
completion(nil, err)
}
-
+
} catch {
completion(nil, error)
}
@@ -298,27 +318,26 @@ class Networking {
}
}
}
-
- static func patchClientSession(clientToken: String, requestBody: ClientSessionRequestBody, customDefinedApiKey: String? = nil, completion: @escaping (String?, Error?) -> Void) {
+
+ static func patchClientSession(
+ clientToken: String, requestBody: ClientSessionRequestBody, customDefinedApiKey: String? = nil,
+ completion: @escaping (String?, Error?) -> Void
+ ) {
let url = environment.baseUrl.appendingPathComponent("/api/client-session")
-
+
var tmpRequestBody = requestBody
tmpRequestBody.clientToken = clientToken
-
+
let bodyData: Data!
-
+
do {
- if let requestBodyJson = tmpRequestBody.dictionaryValue {
- bodyData = try JSONSerialization.data(withJSONObject: requestBodyJson, options: .fragmentsAllowed)
- } else {
- completion(nil, NetworkError.serializationError)
- return
- }
+ let encoder = JSONEncoder()
+ bodyData = try encoder.encode(requestBody)
} catch {
completion(nil, NetworkError.missingParams)
return
}
-
+
let networking = Networking()
networking.request(
apiVersion: .v2_1,
@@ -326,17 +345,23 @@ class Networking {
method: .patch,
headers: nil,
queryParameters: nil,
- body: bodyData) { result in
+ body: bodyData
+ ) { result in
switch result {
case .success(let data):
do {
- if let token = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any])?["clientToken"] as? String {
+ if let token =
+ (try JSONSerialization.jsonObject(with: data, options: .allowFragments)
+ as? [String: Any])?["clientToken"] as? String
+ {
completion(token, nil)
} else {
- let err = NSError(domain: "example", code: 10, userInfo: [NSLocalizedDescriptionKey: "Failed to find client token"])
+ let err = NSError(
+ domain: "example", code: 10,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to find client token"])
completion(nil, err)
}
-
+
} catch {
completion(nil, error)
}
@@ -347,8 +372,10 @@ class Networking {
}
}
-internal extension String {
- func toDate(withFormat f: String = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timeZone: TimeZone? = nil) -> Date? {
+extension String {
+ func toDate(withFormat f: String = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timeZone: TimeZone? = nil)
+ -> Date?
+ {
let df = DateFormatter()
df.dateFormat = f
df.locale = Locale(identifier: "en_US_POSIX")
@@ -358,23 +385,23 @@ internal extension String {
}
public struct Payment {
-
+
public struct CreateRequest: Encodable {
let paymentMethodToken: String
-
+
public init(token: String) {
self.paymentMethodToken = token
}
}
-
+
public struct ResumeRequest: Encodable {
let resumeToken: String
-
+
public init(token: String) {
self.resumeToken = token
}
}
-
+
public struct Response: Codable {
public let id: String?
public let paymentId: String?
@@ -391,18 +418,19 @@ public struct Payment {
public let requiredAction: Payment.Response.RequiredAction?
public let status: Status
public let paymentFailureReason: PrimerPaymentErrorCode.RawValue?
-
+
public enum CodingKeys: String, CodingKey {
- case id, paymentId, amount, currencyCode, customer, customerId, order, orderId, requiredAction, status, paymentFailureReason
+ case id, paymentId, amount, currencyCode, customer, customerId, order, orderId,
+ requiredAction, status, paymentFailureReason
case dateStr = "date"
}
-
+
public struct RequiredAction: Codable {
public let clientToken: String
public let name: RequiredActionName
public let description: String?
}
-
+
/// This enum is giong to be simplified removing the following cases:
/// - authorized
/// - settled
diff --git a/Debug App/Sources/Network/URL+NetworkHeaders.swift b/Debug App/Sources/Network/URL+NetworkHeaders.swift
deleted file mode 100644
index ddd86def5f..0000000000
--- a/Debug App/Sources/Network/URL+NetworkHeaders.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// URL+NetworkHeaders.swift
-// Debug App
-//
-// Created by Alexandra Lovin on 09.11.2023.
-// Copyright © 2023 Primer API Ltd. All rights reserved.
-//
-
-import Foundation
-extension URL {
- static func requestSessionHTTPHeaders(useNewWorkflows: Bool) -> [String: String]? {
- useNewWorkflows ? ["Legacy-Workflows": "false"] : nil
- }
-}
diff --git a/Debug App/Sources/Utilities/MetadataParser.swift b/Debug App/Sources/Utilities/MetadataParser.swift
index 88a904db10..d9b9e54feb 100644
--- a/Debug App/Sources/Utilities/MetadataParser.swift
+++ b/Debug App/Sources/Utilities/MetadataParser.swift
@@ -10,11 +10,11 @@ import Foundation
struct MetadataParser {
- func parse(_ metadata: String?) -> [String: Any] {
+ func parse(_ metadata: String?) -> [String: String] {
guard let metadata = metadata else { return [:] }
if let jsonData = metadata.data(using: .utf8),
- let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
+ let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
return jsonObject
}
@@ -22,10 +22,9 @@ struct MetadataParser {
.split(separator: "\n")
.map { $0.split(separator: "=").map { String($0).trimmingCharacters(in: .whitespaces) } }
.filter { $0.count > 1 }
- .map { (keyValue: [String]) -> (String, Any) in
+ .map { (keyValue: [String]) -> (String, String) in
let (key, value) = (keyValue[0], keyValue[1])
- let parsedValue = parseValue(value)
- return (key, parsedValue)
+ return (key, value)
}
return Dictionary(uniqueKeysWithValues: keyValuePairs)
diff --git a/Debug App/Sources/View Controllers/Merchant Helpers/MerchantHelpers.swift b/Debug App/Sources/View Controllers/Merchant Helpers/MerchantHelpers.swift
index 985e99012b..70b6bc5235 100644
--- a/Debug App/Sources/View Controllers/Merchant Helpers/MerchantHelpers.swift
+++ b/Debug App/Sources/View Controllers/Merchant Helpers/MerchantHelpers.swift
@@ -32,7 +32,7 @@ struct MerchantMockDataManager {
return ClientSessionRequestBody(
customerId: customerId,
orderId: "ios-order-\(String.randomString(length: 8))",
- currencyCode: CurrencyLoader().getCurrency("EUR"),
+ currencyCode: CurrencyLoader().getCurrency("EUR")?.code,
amount: nil,
metadata: nil,
customer: ClientSessionRequestBody.Customer(
diff --git a/Debug App/Sources/View Controllers/MerchantDropInUIViewController.swift b/Debug App/Sources/View Controllers/MerchantDropInUIViewController.swift
index 7f36deeb1a..462b34b500 100644
--- a/Debug App/Sources/View Controllers/MerchantDropInUIViewController.swift
+++ b/Debug App/Sources/View Controllers/MerchantDropInUIViewController.swift
@@ -277,7 +277,8 @@ extension MerchantDropInUIViewController {
print("\n\nMERCHANT APP\n\(#function)\nError: \(error)")
self.primerError = error
self.logs.append(#function)
-
+ self.checkoutData = data
+
let message = "Merchant App | ERROR: \(error.localizedDescription)"
decisionHandler(.fail(withErrorMessage: message))
}
diff --git a/Debug App/Sources/View Controllers/MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.swift b/Debug App/Sources/View Controllers/MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.swift
index b50ea0823e..fe647d7370 100644
--- a/Debug App/Sources/View Controllers/MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.swift
+++ b/Debug App/Sources/View Controllers/MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.swift
@@ -312,6 +312,7 @@ extension MerchantHeadlessCheckoutAvailablePaymentMethodsViewController {
print("\n\nMERCHANT APP\n\(#function)\nerror: \(err)\ncheckoutData: \(String(describing: checkoutData))")
self.logs.append(#function)
self.primerError = err
+ self.checkoutData = checkoutData
self.hideLoadingOverlay()
if let lastViewController = navigationController?.children.last {
diff --git a/Debug App/Sources/View Controllers/MerchantHeadlessVaultManagerViewController.swift b/Debug App/Sources/View Controllers/MerchantHeadlessVaultManagerViewController.swift
index 7271bd2772..e8b878202e 100644
--- a/Debug App/Sources/View Controllers/MerchantHeadlessVaultManagerViewController.swift
+++ b/Debug App/Sources/View Controllers/MerchantHeadlessVaultManagerViewController.swift
@@ -22,7 +22,6 @@ class MerchantHeadlessVaultManagerViewController: UIViewController, PrimerHeadle
var settings: PrimerSettings!
var clientSession: ClientSessionRequestBody?
var clientToken: String?
- var mandateDelegate: ACHMandateDelegate?
var logs: [String] = []
var primerError: Error?
@@ -75,7 +74,6 @@ class MerchantHeadlessVaultManagerViewController: UIViewController, PrimerHeadle
private func startPrimerHeadlessUniversalCheckout(with clientToken: String) {
PrimerHeadlessUniversalCheckout.current.start(withClientToken: clientToken, settings: self.settings, completion: { (_, _) in
self.vaultedManager = PrimerHeadlessUniversalCheckout.VaultManager()
- self.mandateDelegate = self.vaultedManager
do {
try self.vaultedManager?.configure()
@@ -135,15 +133,7 @@ class MerchantHeadlessVaultManagerViewController: UIViewController, PrimerHeadle
self.activityIndicator = nil
}
}
-
- private func showMandate() {
- showAlert(title: "Mandate acceptance", message: "Would you like to accept this mandate?") {
- self.mandateDelegate?.acceptMandate()
- } cancelHandler: {
- self.mandateDelegate?.declineMandate()
- }
- }
-
+
private func showAlert(title: String, message: String, okHandler: (() -> Void)? = nil, cancelHandler: (() -> Void)? = nil) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in okHandler?() }
@@ -174,9 +164,12 @@ extension MerchantHeadlessVaultManagerViewController: UITableViewDataSource, UIT
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
showLoadingOverlay()
let vaultedPaymentMethod = self.availablePaymentMethods[indexPath.row]
- let vaultedCardAdditionalData = PrimerVaultedCardAdditionalData(cvv: "737")
+ if vaultedPaymentMethod.paymentMethodType == "PAYMENT_CARD" {
+ self.vaultedManager?.startPaymentFlow(vaultedPaymentMethodId: vaultedPaymentMethod.id, vaultedPaymentMethodAdditionalData: PrimerVaultedCardAdditionalData(cvv: "737"))
+ return
+ }
- self.vaultedManager?.startPaymentFlow(vaultedPaymentMethodId: vaultedPaymentMethod.id, vaultedPaymentMethodAdditionalData: vaultedCardAdditionalData)
+ self.vaultedManager?.startPaymentFlow(vaultedPaymentMethodId: vaultedPaymentMethod.id)
}
}
@@ -280,10 +273,6 @@ extension MerchantHeadlessVaultManagerViewController {
DispatchQueue.main.async {
self.hideLoadingOverlay()
}
-
- if additionalInfo is ACHMandateAdditionalInfo {
- showMandate()
- }
}
func primerHeadlessUniversalCheckoutDidEnterResumePendingWithPaymentAdditionalInfo(_ additionalInfo: PrimerCheckoutAdditionalInfo?) {
diff --git a/Debug App/Sources/View Controllers/MerchantResultViewController.swift b/Debug App/Sources/View Controllers/MerchantResultViewController.swift
index ebaf106d32..e8091d1d07 100644
--- a/Debug App/Sources/View Controllers/MerchantResultViewController.swift
+++ b/Debug App/Sources/View Controllers/MerchantResultViewController.swift
@@ -47,14 +47,18 @@ final class MerchantResultViewController: UIViewController {
}
if error != nil || checkoutData != nil {
- let paymentEncodable = PrimerPaymentResultEncodable(id: checkoutData?.payment?.id,
- orderId: checkoutData?.payment?.orderId)
+ let paymentEncodable = PrimerPaymentResultEncodable(id: checkoutData?.payment?.id,
+ orderId: checkoutData?.payment?.orderId)
let encodable = PrimerResultEncodable(payment: paymentEncodable, error: error)
guard let data = try? JSONEncoder().encode(encodable), let string = String(data: data, encoding: .utf8) else {
responseTextView.text = "[\"Couldn't encode result to JSON\"]"
return
}
responseTextView.text = string
+
+ if let error = self.error {
+ responseTextView.text.append("\n\(error.localizedDescription)")
+ }
} else if let error = self.error {
responseTextView.text = "[\"\(error.localizedDescription)\"]"
} else {
@@ -63,8 +67,8 @@ final class MerchantResultViewController: UIViewController {
responseStatus.font = .systemFont(ofSize: 17, weight: .medium)
- responseStatus.textColor = error == nil ? .green : .red
- responseStatus.text = error == nil ? "Success" : "Failure"
+ responseStatus.textColor = self.error == nil ? .green : .red
+ responseStatus.text = self.error == nil ? "Success" : "Failure"
if logs.count > 0 {
if let data = try? JSONSerialization.data(withJSONObject: logs) {
diff --git a/Debug App/Sources/View Controllers/MerchantSessionAndSettingsViewController.swift b/Debug App/Sources/View Controllers/MerchantSessionAndSettingsViewController.swift
index 57be90528b..3cf2c1f42c 100644
--- a/Debug App/Sources/View Controllers/MerchantSessionAndSettingsViewController.swift
+++ b/Debug App/Sources/View Controllers/MerchantSessionAndSettingsViewController.swift
@@ -12,43 +12,41 @@ import UIKit
var environment: Environment = .sandbox
var customDefinedApiKey: String?
var performPaymentAfterVaulting: Bool = false
-var useNewWorkflows = true
var paymentSessionType: MerchantMockDataManager.SessionType = .generic
class MerchantSessionAndSettingsViewController: UIViewController {
-
+
enum RenderMode: Int {
case createClientSession = 0
case clientToken
case testScenario
}
-
+
// MARK: Stack Views
-
+
@IBOutlet weak var environmentStackView: UIStackView!
@IBOutlet weak var testParamsGroupStackView: UIStackView!
@IBOutlet weak var apiKeyStackView: UIStackView!
- @IBOutlet weak var useNewWorkflowsStackView: UIStackView!
@IBOutlet weak var klarnaEMDStackView: UIStackView!
@IBOutlet weak var clientTokenStackView: UIStackView!
@IBOutlet weak var sdkSettingsStackView: UIStackView!
@IBOutlet weak var orderStackView: UIStackView!
@IBOutlet weak var customerStackView: UIStackView!
@IBOutlet weak var surchargeGroupStackView: UIStackView!
-
+
// MARK: Testing Mode Inputs
-
+
@IBOutlet weak var testingModeSegmentedControl: UISegmentedControl!
-
+
// MARK: Environment Inputs
-
+
@IBOutlet weak var environmentSegmentedControl: UISegmentedControl!
@IBOutlet weak var apiKeyTextField: UITextField!
@IBOutlet weak var clientTokenTextField: UITextField!
@IBOutlet weak var metadataTextField: UITextField!
-
+
// MARK: Test Inputs
-
+
@IBOutlet weak var testScenarioTextField: UITextField!
@IBOutlet weak var testResultSegmentedControl: UISegmentedControl!
@IBOutlet weak var testParamsStackView: UIStackView!
@@ -59,20 +57,19 @@ class MerchantSessionAndSettingsViewController: UIViewController {
@IBOutlet weak var testErrorDescriptionTextField: UITextField!
@IBOutlet weak var test3DSStackView: UIStackView!
@IBOutlet weak var test3DSScenarioTextField: UITextField!
-
+
// MARK: SDK Settings Inputs
- @IBOutlet weak var useNewWorkflowsSwitch: UISwitch!
@IBOutlet weak var checkoutFlowSegmentedControl: UISegmentedControl!
+ @IBOutlet weak var vaultingFlowSegmentedControl: UISegmentedControl!
@IBOutlet weak var merchantNameTextField: UITextField!
@IBOutlet weak var applyThemingSwitch: UISwitch!
- @IBOutlet weak var vaultPaymentsSwitch: UISwitch!
@IBOutlet weak var disableSuccessScreenSwitch: UISwitch!
@IBOutlet weak var disableErrorScreenSwitch: UISwitch!
@IBOutlet weak var gesturesDismissalSwitch: UISwitch!
@IBOutlet weak var closeButtonDismissalSwitch: UISwitch!
@IBOutlet weak var disableInitScreenSwitch: UISwitch!
@IBOutlet weak var enableCVVRecaptureFlowSwitch: UISwitch!
-
+
// MARK: Apple Pay Inputs
@IBOutlet weak var applePayCaptureBillingAddressSwitch: UISwitch!
@IBOutlet weak var applePayCheckProvidedNetworksSwitch: UISwitch!
@@ -92,21 +89,21 @@ class MerchantSessionAndSettingsViewController: UIViewController {
@IBOutlet weak var applePayShippingContactPostalAddressSwitch: UISwitch!
// MARK: Order Inputs
-
+
@IBOutlet weak var currencyTextField: UITextField!
@IBOutlet weak var countryCodeTextField: UITextField!
@IBOutlet weak var orderIdTextField: UITextField!
@IBOutlet weak var lineItemsStackView: UIStackView!
@IBOutlet weak var totalAmountLabel: UILabel!
-
+
// MARK: Customer Inputs
-
+
@IBOutlet weak var customerIdTextField: UITextField!
@IBOutlet weak var customerFirstNameTextField: UITextField!
@IBOutlet weak var customerLastNameTextField: UITextField!
@IBOutlet weak var customerEmailTextField: UITextField!
@IBOutlet weak var customerMobileNumberTextField: UITextField!
-
+
@IBOutlet weak var billingAddressSwitch: UISwitch!
@IBOutlet weak var billingAddressStackView: UIStackView!
@IBOutlet weak var billingAddressFirstNameTextField: UITextField!
@@ -117,7 +114,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
@IBOutlet weak var billingAddressStateTextField: UITextField!
@IBOutlet weak var billingAddressPostalCodeTextField: UITextField!
@IBOutlet weak var billingAddressCountryTextField: UITextField!
-
+
@IBOutlet weak var shippingAddressStackView: UIStackView!
@IBOutlet weak var shippingAddressSwitch: UISwitch!
@IBOutlet weak var shippinAddressFirstNameTextField: UITextField!
@@ -128,16 +125,16 @@ class MerchantSessionAndSettingsViewController: UIViewController {
@IBOutlet weak var shippinAddressStateTextField: UITextField!
@IBOutlet weak var shippinAddressPostalCodeTextField: UITextField!
@IBOutlet weak var shippinAddressCountryTextField: UITextField!
-
+
// MARK: Surcharge Inputs
-
+
@IBOutlet weak var surchargeSwitch: UISwitch!
@IBOutlet weak var surchargeStackView: UIStackView!
@IBOutlet weak var applePaySurchargeTextField: UITextField!
-
+
@IBOutlet weak var primerSDKButton: UIButton!
@IBOutlet weak var primerHeadlessSDKButton: UIButton!
-
+
var lineItems: [ClientSessionRequestBody.Order.LineItem] {
get {
return self.clientSession.order?.lineItems ?? []
@@ -146,24 +143,24 @@ class MerchantSessionAndSettingsViewController: UIViewController {
self.clientSession.order?.lineItems = newValue
}
}
-
+
let testScenarioPicker = UIPickerView()
let testFailureFlowPicker = UIPickerView()
let test3DSScenarioPicker = UIPickerView()
-
+
var renderMode: RenderMode = .createClientSession
-
+
var selectedPaymentHandling: PrimerPaymentHandling = .auto
-
+
var clientSession = MerchantMockDataManager.getClientSession(sessionType: .generic)
-
+
var selectedTestScenario: Test.Scenario?
var selectedTestFlow: Test.Flow?
var selectedTest3DSScenario: Test.Params.ThreeDS.Scenario?
-
+
var applyTheme: Bool = false
var payAfterVaultSuccess: Bool = false
-
+
var applePayCaptureBillingAddress = false
var applePayBillingAdditionalContactFields: [PrimerApplePayOptions.RequiredContactField]? = []
var applePayCaptureShippingDetails = false
@@ -176,25 +173,25 @@ class MerchantSessionAndSettingsViewController: UIViewController {
self.testingModeSegmentedControl.accessibilityIdentifier = "Testing Mode Segmented Control"
self.clientTokenTextField.accessibilityIdentifier = "Client Token Text Field"
}
-
+
// MARK: - VIEW LIFE-CYCLE
-
+
override func viewDidLoad() {
super.viewDidLoad()
-
+
self.setAccessibilityIds()
testScenarioPicker.dataSource = self
testScenarioPicker.delegate = self
testScenarioTextField.inputView = testScenarioPicker
-
+
testFailureFlowPicker.dataSource = self
testFailureFlowPicker.delegate = self
testFailureFlowTextField.inputView = testFailureFlowPicker
-
+
test3DSScenarioPicker.dataSource = self
test3DSScenarioPicker.delegate = self
test3DSScenarioTextField.inputView = test3DSScenarioPicker
-
+
switch environment {
case .dev:
environmentSegmentedControl.selectedSegmentIndex = 0
@@ -207,40 +204,43 @@ class MerchantSessionAndSettingsViewController: UIViewController {
default:
environmentSegmentedControl.selectedSegmentIndex = 1
}
-
+
self.apiKeyTextField.text = customDefinedApiKey
-
+
let viewTap = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
view.addGestureRecognizer(viewTap)
-
+
merchantNameTextField.text = "Primer Merchant"
populateSessionSettingsFields()
-
- customerIdTextField.addTarget(self, action: #selector(customerIdChanged(_:)), for: .editingDidEnd)
-
+
+ customerIdTextField.addTarget(
+ self, action: #selector(customerIdChanged(_:)), for: .editingDidEnd)
+
handleAppetizeIfNeeded(AppetizeConfigProvider())
-
+
render()
-
- NotificationCenter.default.addObserver(self, selector: #selector(handleAppetizeConfig), name: NSNotification.Name.appetizeURLHandled, object: nil)
+
+ NotificationCenter.default.addObserver(
+ self, selector: #selector(handleAppetizeConfig), name: NSNotification.Name.appetizeURLHandled,
+ object: nil)
}
-
+
@objc func handleAppetizeConfig(_ notification: NSNotification) {
if let payloadProvider = notification.object as? DeeplinkConfigProvider {
handleAppetizeIfNeeded(AppetizeConfigProvider(payloadProvider: payloadProvider))
}
}
-
+
private func handleAppetizeIfNeeded(_ configProvider: AppetizeConfigProvider) {
if let config = configProvider.fetchConfig() {
updateUI(for: config)
}
}
-
+
@objc func viewTapped() {
view.endEditing(true)
}
-
+
func render() {
switch renderMode {
case .createClientSession:
@@ -252,9 +252,8 @@ class MerchantSessionAndSettingsViewController: UIViewController {
orderStackView.isHidden = false
customerStackView.isHidden = false
surchargeGroupStackView.isHidden = false
- useNewWorkflowsStackView.isHidden = false
klarnaEMDStackView.isHidden = false
-
+
case .clientToken:
environmentStackView.isHidden = false
testParamsGroupStackView.isHidden = true
@@ -264,9 +263,8 @@ class MerchantSessionAndSettingsViewController: UIViewController {
orderStackView.isHidden = true
customerStackView.isHidden = true
surchargeGroupStackView.isHidden = true
- useNewWorkflowsStackView.isHidden = true
klarnaEMDStackView.isHidden = true
-
+
case .testScenario:
environmentStackView.isHidden = true
testParamsGroupStackView.isHidden = false
@@ -276,17 +274,16 @@ class MerchantSessionAndSettingsViewController: UIViewController {
orderStackView.isHidden = false
customerStackView.isHidden = false
surchargeGroupStackView.isHidden = false
- useNewWorkflowsStackView.isHidden = true
klarnaEMDStackView.isHidden = true
-
+
testParamsStackView.isHidden = (selectedTestScenario == nil)
-
+
if testResultSegmentedControl.selectedSegmentIndex == 0 {
testFailureStackView.isHidden = true
} else {
testFailureStackView.isHidden = false
}
-
+
switch selectedTestScenario {
case .testNative3DS:
if testResultSegmentedControl.selectedSegmentIndex == 0 {
@@ -298,50 +295,51 @@ class MerchantSessionAndSettingsViewController: UIViewController {
test3DSStackView.isHidden = true
}
}
-
- gesturesDismissalSwitch.isOn = true // Default value
- closeButtonDismissalSwitch.isOn = false // Default false
-
+
+ gesturesDismissalSwitch.isOn = true // Default value
+ closeButtonDismissalSwitch.isOn = false // Default false
+
lineItemsStackView.removeAllArrangedSubviews()
lineItemsStackView.alignment = .fill
lineItemsStackView.distribution = .fill
-
+
for (index, lineItem) in lineItems.enumerated() {
let horizontalStackView = UIStackView()
horizontalStackView.tag = index
horizontalStackView.axis = .horizontal
horizontalStackView.alignment = .fill
horizontalStackView.distribution = .fill
-
+
let nameLbl = UILabel()
nameLbl.text = (lineItem.description ?? "") + " x\(lineItem.quantity ?? 1)"
nameLbl.textAlignment = .left
nameLbl.font = UIFont.systemFont(ofSize: 14)
horizontalStackView.addArrangedSubview(nameLbl)
-
+
let priceLbl = UILabel()
priceLbl.text = "\(lineItem.amount ?? 0)"
priceLbl.textAlignment = .right
priceLbl.font = UIFont.systemFont(ofSize: 14)
horizontalStackView.addArrangedSubview(priceLbl)
-
- let lineItemTapGesture = UITapGestureRecognizer(target: self, action: #selector(lineItemTapped))
+
+ let lineItemTapGesture = UITapGestureRecognizer(
+ target: self, action: #selector(lineItemTapped))
horizontalStackView.addGestureRecognizer(lineItemTapGesture)
-
+
lineItemsStackView.addArrangedSubview(horizontalStackView)
}
-
+
let totalAmount = lineItems.compactMap({ (($0.quantity ?? 0) * ($0.amount ?? 0)) }).reduce(0, +)
totalAmountLabel.text = "\(totalAmount)"
}
-
+
// MARK: - ACTIONS
-
+
@objc func lineItemTapped(_ sender: UITapGestureRecognizer) {
guard let index = sender.view?.tag, lineItems.count > index else {
return
}
-
+
let lineItem = lineItems[index]
let vc = MerchantNewLineItemViewController.instantiate(lineItem: lineItem)
vc.onLineItemEdited = { lineItem in
@@ -354,12 +352,12 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
navigationController?.pushViewController(vc, animated: true)
}
-
+
@IBAction func testingModeSegmentedControlValueChanged(_ sender: UISegmentedControl) {
renderMode = RenderMode(rawValue: sender.selectedSegmentIndex)!
render()
}
-
+
@IBAction func environmentSegmentedControlValuewChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
@@ -374,11 +372,11 @@ class MerchantSessionAndSettingsViewController: UIViewController {
fatalError()
}
}
-
+
@IBAction func testResultSegmentedControlValueChanged(_ sender: UISegmentedControl) {
render()
}
-
+
@IBAction func checkoutFlowSegmentedControlValueChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
@@ -389,7 +387,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
fatalError()
}
}
-
+
@IBAction func addLineItemButtonTapped(_ sender: Any) {
let vc = MerchantNewLineItemViewController.instantiate(lineItem: nil)
vc.onLineItemAdded = { lineItem in
@@ -398,28 +396,24 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
navigationController?.pushViewController(vc, animated: true)
}
-
+
@IBAction func billingAddressSwitchValueChanged(_ sender: UISwitch) {
billingAddressStackView.isHidden = !sender.isOn
}
-
+
@IBAction func shippingAddressSwitchValueChanged(_ sender: UISwitch) {
shippingAddressStackView.isHidden = !sender.isOn
}
-
+
@IBAction func surchargeSwitchValueChanged(_ sender: UISwitch) {
surchargeStackView.isHidden = !sender.isOn
}
- @IBAction func useNewWorkflowsSwitchValueChanged(_ sender: UISwitch) {
- useNewWorkflows = sender.isOn
- }
-
@IBAction func oneTimePaymentValueChanged(_ sender: UISwitch) {
paymentSessionType = sender.isOn ? .klarnaWithEMD : .generic
populateSessionSettingsFields()
}
-
+
@IBAction func applePayCaptureBillingAddressSwitchValueChanged(_ sender: UISwitch) {
applePayBillingControlStackView.isHidden = !sender.isOn
applePayCaptureBillingAddress = sender.isOn
@@ -494,7 +488,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
@IBAction func applePayRequireShippingMethodSwitchChanged(_ sender: UISwitch) {
applePayRequireShippingMethod = sender.isOn
}
-
+
@IBAction func applePayShippingContactNameSwitchChanged(_ sender: UISwitch) {
if sender.isOn {
var fields = applePayShippingAdditionalContactFields ?? []
@@ -510,7 +504,6 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
}
-
@IBAction func applePayShippingContactEmailField(_ sender: UISwitch) {
if sender.isOn {
var fields = applePayShippingAdditionalContactFields ?? []
@@ -525,7 +518,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
}
}
-
+
@IBAction func applePayShippingContactPhoneSwitchChanged(_ sender: UISwitch) {
if sender.isOn {
var fields = applePayShippingAdditionalContactFields ?? []
@@ -555,13 +548,13 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
}
}
-
+
@IBAction func applePayCheckProvidedNetworksSwitchValueChanged(_ sender: UISwitch) {
applePayCheckProvidedNetworks = sender.isOn
}
func configureClientSession() {
- clientSession.currencyCode = CurrencyLoader().getCurrency(currencyTextField.text ?? "")
+ clientSession.currencyCode = CurrencyLoader().getCurrency(currencyTextField.text ?? "")?.code
clientSession.order?.countryCode = CountryCode(rawValue: countryCodeTextField.text ?? "")
clientSession.orderId = orderIdTextField.text
clientSession.customerId = customerIdTextField.text
@@ -569,7 +562,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
clientSession.customer?.lastName = customerLastNameTextField.text
clientSession.customer?.emailAddress = customerEmailTextField.text
clientSession.customer?.mobileNumber = customerMobileNumberTextField.text
-
+
if billingAddressSwitch.isOn {
clientSession.customer?.billingAddress?.firstName = billingAddressFirstNameTextField.text
clientSession.customer?.billingAddress?.lastName = billingAddressLastNameTextField.text
@@ -582,7 +575,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
} else {
clientSession.customer?.billingAddress = nil
}
-
+
if shippingAddressSwitch.isOn {
clientSession.customer?.shippingAddress?.firstName = shippinAddressFirstNameTextField.text
clientSession.customer?.shippingAddress?.lastName = shippinAddressLastNameTextField.text
@@ -595,46 +588,76 @@ class MerchantSessionAndSettingsViewController: UIViewController {
} else {
clientSession.customer?.shippingAddress = nil
}
-
- clientSession.paymentMethod = MerchantMockDataManager.getPaymentMethod(sessionType: paymentSessionType)
+
+ clientSession.paymentMethod = MerchantMockDataManager.getPaymentMethod(
+ sessionType: paymentSessionType)
if paymentSessionType == .generic && enableCVVRecaptureFlowSwitch.isOn {
- let option = ClientSessionRequestBody.PaymentMethod.PaymentMethodOption(surcharge: nil,
- instalmentDuration: nil,
- extraMerchantData: nil,
- captureVaultedCardCvv: enableCVVRecaptureFlowSwitch.isOn,
- merchantName: nil)
-
+ let option = ClientSessionRequestBody.PaymentMethod.PaymentMethodOption(
+ surcharge: nil,
+ instalmentDuration: nil,
+ extraMerchantData: nil,
+ captureVaultedCardCvv: enableCVVRecaptureFlowSwitch.isOn,
+ merchantName: nil)
+
clientSession.paymentMethod?.options?.PAYMENT_CARD = option
}
- let applePayOptions = ClientSessionRequestBody.PaymentMethod.PaymentMethodOption(surcharge: nil,
- instalmentDuration: nil,
- extraMerchantData: nil,
- captureVaultedCardCvv: nil,
- merchantName: "Primer Merchant iOS")
-
+ let applePayOptions = ClientSessionRequestBody.PaymentMethod.PaymentMethodOption(
+ surcharge: nil,
+ instalmentDuration: nil,
+ extraMerchantData: nil,
+ captureVaultedCardCvv: nil,
+ merchantName: "Primer Merchant iOS")
+
clientSession.paymentMethod?.options?.APPLE_PAY = applePayOptions
- if let metadata = metadataTextField.text, !metadata.isEmpty {
- clientSession.metadata = MetadataParser().parse(metadata)
+ if vaultingFlowSegmentedControl.selectedSegmentIndex == 1 {
+ clientSession.paymentMethod?.vaultOnSuccess = true
+ clientSession.paymentMethod?.vaultOnAgreement = nil
+ } else if vaultingFlowSegmentedControl.selectedSegmentIndex == 2 {
+ clientSession.paymentMethod?.vaultOnAgreement = true
+ clientSession.paymentMethod?.vaultOnSuccess = nil
+ } else {
+ clientSession.paymentMethod?.vaultOnSuccess = nil
+ clientSession.paymentMethod?.vaultOnAgreement = nil
+ }
+
+ clientSession.metadata = .dictionary([
+ "deviceInfo": .dictionary([
+ "ipAddress": .string("127.0.0.1"),
+ "userAgent": .string("iOS")
+ ])
+ ])
+
+ if let metadata = metadataTextField.text, !metadata.isEmpty, var metadataDict = clientSession.metadata {
+ metadataTextField.text?.components(separatedBy: ",").forEach {
+ let tuple = String($0).components(separatedBy: "=")
+ guard tuple.count == 2
+ else { return }
+ let key = tuple[0].trimmingCharacters(in: .whitespaces)
+ let value = tuple[1].trimmingCharacters(in: .whitespaces)
+ try? metadataDict.add(.string(value), forKey: key)
+ }
+ clientSession.metadata = metadataDict
}
}
-
+
func populateSessionSettingsFields() {
clientSession = MerchantMockDataManager.getClientSession(sessionType: paymentSessionType)
-
- enableCVVRecaptureFlowSwitch.isOn = clientSession.paymentMethod?.options?.PAYMENT_CARD?.captureVaultedCardCvv == true
-
- currencyTextField.text = clientSession.currencyCode?.code
+
+ enableCVVRecaptureFlowSwitch.isOn =
+ clientSession.paymentMethod?.options?.PAYMENT_CARD?.captureVaultedCardCvv == true
+
+ currencyTextField.text = clientSession.currencyCode
countryCodeTextField.text = clientSession.order?.countryCode?.rawValue
orderIdTextField.text = clientSession.orderId
-
+
customerIdTextField.text = clientSession.customerId
customerFirstNameTextField.text = clientSession.customer?.firstName
customerLastNameTextField.text = clientSession.customer?.lastName
customerEmailTextField.text = clientSession.customer?.emailAddress
customerMobileNumberTextField.text = clientSession.customer?.mobileNumber
-
+
billingAddressSwitch.isOn = true
billingAddressFirstNameTextField.text = clientSession.customer?.billingAddress?.firstName
billingAddressLastNameTextField.text = clientSession.customer?.billingAddress?.lastName
@@ -644,7 +667,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
billingAddressStateTextField.text = clientSession.customer?.billingAddress?.state
billingAddressPostalCodeTextField.text = clientSession.customer?.billingAddress?.postalCode
billingAddressCountryTextField.text = clientSession.customer?.billingAddress?.countryCode
-
+
shippingAddressSwitch.isOn = true
shippinAddressFirstNameTextField.text = clientSession.customer?.shippingAddress?.firstName
shippinAddressLastNameTextField.text = clientSession.customer?.shippingAddress?.lastName
@@ -655,54 +678,58 @@ class MerchantSessionAndSettingsViewController: UIViewController {
shippinAddressPostalCodeTextField.text = clientSession.customer?.shippingAddress?.postalCode
shippinAddressCountryTextField.text = clientSession.customer?.shippingAddress?.countryCode
}
-
+
func configureTestScenario() {
guard let selectedTestScenario = selectedTestScenario else {
- let alert = UIAlertController(title: "Error", message: "Please choose Test Scenario", preferredStyle: .alert)
+ let alert = UIAlertController(
+ title: "Error", message: "Please choose Test Scenario", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}
-
+
var testParams = Test.Params(
scenario: selectedTestScenario,
result: .success,
network: nil,
polling: nil,
threeDS: nil)
-
+
if testResultSegmentedControl.selectedSegmentIndex == 1 {
guard let selectedTestFlow = selectedTestFlow else {
- let alert = UIAlertController(title: "Error", message: "Please choose failure flow in the Failure Parameters", preferredStyle: .alert)
+ let alert = UIAlertController(
+ title: "Error", message: "Please choose failure flow in the Failure Parameters",
+ preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}
-
+
let failure = Test.Params.Failure(
flow: selectedTestFlow,
error: Test.Params.Failure.Error(
errorId: testErrorIdTextField.text ?? "test-error-id",
description: testErrorDescriptionTextField.text ?? "test-error-description"))
-
+
testParams.result = .failure(failure: failure)
-
+
} else if case .testNative3DS = selectedTestScenario {
guard let selectedTest3DSScenario = selectedTest3DSScenario else {
- let alert = UIAlertController(title: "Error", message: "Please choose 3DS scenario", preferredStyle: .alert)
+ let alert = UIAlertController(
+ title: "Error", message: "Please choose 3DS scenario", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}
testParams.threeDS = Test.Params.ThreeDS(scenario: selectedTest3DSScenario)
}
-
+
clientSession.testParams = testParams
}
-
+
@IBAction func primerSDKButtonTapped(_ sender: Any) {
customDefinedApiKey = (apiKeyTextField.text ?? "").isEmpty ? nil : apiKeyTextField.text
-
+
let selectedDismissalMechanisms: [DismissalMechanism] = {
var mechanisms = [DismissalMechanism]()
if gesturesDismissalSwitch.isOn {
@@ -713,14 +740,14 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
return mechanisms
}()
-
+
let uiOptions = PrimerUIOptions(
isInitScreenEnabled: !disableInitScreenSwitch.isOn,
isSuccessScreenEnabled: !disableSuccessScreenSwitch.isOn,
isErrorScreenEnabled: !disableErrorScreenSwitch.isOn,
dismissalMechanism: selectedDismissalMechanisms,
theme: applyThemingSwitch.isOn ? CheckoutTheme.tropical : nil)
-
+
let mandateData = PrimerStripeOptions.MandateData.templateMandate(merchantName: "Primer Inc.")
let shippingOptions = applePayCaptureShippingDetails ?
@@ -731,7 +758,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
PrimerApplePayOptions.BillingOptions(requiredBillingContactFields: applePayBillingAdditionalContactFields) : nil
let stripePublishableKey = SecretsManager.shared.value(forKey: .stripePublishableKey)
-
+
let settings = PrimerSettings(
paymentHandling: selectedPaymentHandling,
paymentMethodOptions: PrimerPaymentMethodOptions(
@@ -748,22 +775,24 @@ class MerchantSessionAndSettingsViewController: UIViewController {
uiOptions: uiOptions,
debugOptions: PrimerDebugOptions(is3DSSanityCheckEnabled: false)
)
-
+
switch renderMode {
case .createClientSession, .testScenario:
configureClientSession()
if case .testScenario = renderMode {
configureTestScenario()
}
- let vc = MerchantDropInUIViewController.instantiate(settings: settings, clientSession: clientSession, clientToken: nil)
+ let vc = MerchantDropInUIViewController.instantiate(
+ settings: settings, clientSession: clientSession, clientToken: nil)
navigationController?.pushViewController(vc, animated: true)
-
+
case .clientToken:
- let vc = MerchantDropInUIViewController.instantiate(settings: settings, clientSession: nil, clientToken: clientTokenTextField.text)
+ let vc = MerchantDropInUIViewController.instantiate(
+ settings: settings, clientSession: nil, clientToken: clientTokenTextField.text)
navigationController?.pushViewController(vc, animated: true)
}
}
-
+
@IBAction func primerHeadlessButtonTapped(_ sender: Any) {
customDefinedApiKey = (apiKeyTextField.text ?? "").isEmpty ? nil : apiKeyTextField.text
@@ -775,7 +804,7 @@ class MerchantSessionAndSettingsViewController: UIViewController {
PrimerApplePayOptions.BillingOptions(requiredBillingContactFields: applePayBillingAdditionalContactFields) : nil
let stripePublishableKey = SecretsManager.shared.value(forKey: .stripePublishableKey)
-
+
let settings = PrimerSettings(
paymentHandling: selectedPaymentHandling,
paymentMethodOptions: PrimerPaymentMethodOptions(
@@ -792,25 +821,27 @@ class MerchantSessionAndSettingsViewController: UIViewController {
uiOptions: nil,
debugOptions: PrimerDebugOptions(is3DSSanityCheckEnabled: false)
)
-
+
switch renderMode {
case .createClientSession, .testScenario:
configureClientSession()
if case .testScenario = renderMode {
configureTestScenario()
}
- let vc = MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.instantiate(settings: settings,
- clientSession: clientSession,
- clientToken: nil)
+ let vc = MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.instantiate(
+ settings: settings,
+ clientSession: clientSession,
+ clientToken: nil)
navigationController?.pushViewController(vc, animated: true)
case .clientToken:
- let vc = MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.instantiate(settings: settings,
- clientSession: nil,
- clientToken: clientTokenTextField.text)
+ let vc = MerchantHeadlessCheckoutAvailablePaymentMethodsViewController.instantiate(
+ settings: settings,
+ clientSession: nil,
+ clientToken: clientTokenTextField.text)
navigationController?.pushViewController(vc, animated: true)
}
}
-
+
@objc func customerIdChanged(_ textField: UITextField!) {
guard let text = customerIdTextField.text else { return }
UserDefaults.standard.set(text, forKey: MerchantMockDataManager.customerIdStorageKey)
@@ -818,11 +849,11 @@ class MerchantSessionAndSettingsViewController: UIViewController {
}
extension MerchantSessionAndSettingsViewController: UIPickerViewDataSource, UIPickerViewDelegate {
-
+
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
-
+
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == testScenarioPicker {
return Test.Scenario.allCases.count + 1
@@ -831,33 +862,35 @@ extension MerchantSessionAndSettingsViewController: UIPickerViewDataSource, UIPi
} else if pickerView == test3DSScenarioPicker {
return Test.Params.ThreeDS.Scenario.allCases.count + 1
}
-
+
return 0
}
-
- func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
+
+ func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int)
+ -> String?
+ {
if row == 0 {
return "-"
} else {
if pickerView == testScenarioPicker {
- return Test.Scenario.allCases[row-1].rawValue
+ return Test.Scenario.allCases[row - 1].rawValue
} else if pickerView == testFailureFlowPicker {
- return Test.Flow.allCases[row-1].rawValue
+ return Test.Flow.allCases[row - 1].rawValue
} else if pickerView == test3DSScenarioPicker {
- return Test.Params.ThreeDS.Scenario.allCases[row-1].rawValue
+ return Test.Params.ThreeDS.Scenario.allCases[row - 1].rawValue
}
}
-
+
return nil
}
-
+
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerView == testScenarioPicker {
if row == 0 {
selectedTestScenario = nil
testScenarioTextField.text = "-"
} else {
- selectedTestScenario = Test.Scenario.allCases[row-1]
+ selectedTestScenario = Test.Scenario.allCases[row - 1]
testScenarioTextField.text = selectedTestScenario?.rawValue
}
} else if pickerView == testFailureFlowPicker {
@@ -865,7 +898,7 @@ extension MerchantSessionAndSettingsViewController: UIPickerViewDataSource, UIPi
selectedTestFlow = nil
testFailureFlowTextField.text = "-"
} else {
- selectedTestFlow = Test.Flow.allCases[row-1]
+ selectedTestFlow = Test.Flow.allCases[row - 1]
testFailureFlowTextField.text = selectedTestFlow?.rawValue
}
} else if pickerView == test3DSScenarioPicker {
@@ -873,11 +906,11 @@ extension MerchantSessionAndSettingsViewController: UIPickerViewDataSource, UIPi
selectedTest3DSScenario = nil
test3DSScenarioTextField.text = "-"
} else {
- selectedTest3DSScenario = Test.Params.ThreeDS.Scenario.allCases[row-1]
+ selectedTest3DSScenario = Test.Params.ThreeDS.Scenario.allCases[row - 1]
test3DSScenarioTextField.text = selectedTest3DSScenario?.rawValue
}
}
-
+
render()
}
}
@@ -886,7 +919,7 @@ extension MerchantSessionAndSettingsViewController {
private func updateUI(for config: SessionConfiguration) {
apiKeyTextField.text = config.customApiKey
customerIdTextField.text = config.customerId.isEmpty ? "ios-customer-id" : config.customerId
-
+
switch config.env {
case .dev:
environmentSegmentedControl.selectedSegmentIndex = 0
@@ -900,28 +933,27 @@ extension MerchantSessionAndSettingsViewController {
environmentSegmentedControl.selectedSegmentIndex = 2
}
environment = config.env
-
+
switch config.paymentHandling {
case .auto:
checkoutFlowSegmentedControl.selectedSegmentIndex = 0
case .manual:
checkoutFlowSegmentedControl.selectedSegmentIndex = 1
}
-
+
currencyTextField.text = config.currency
countryCodeTextField.text = config.countryCode
-
- let lineItem = ClientSessionRequestBody.Order.LineItem(itemId: "ld-lineitem",
- description: "Fancy Shoes",
- amount: Int(config.value) ?? 100,
- quantity: 1,
- discountAmount: nil,
- taxAmount: nil)
-
+
+ let lineItem = ClientSessionRequestBody.Order.LineItem(
+ itemId: "ld-lineitem",
+ description: "Fancy Shoes",
+ amount: Int(config.value) ?? 100,
+ quantity: 1,
+ discountAmount: nil,
+ taxAmount: nil)
+
self.lineItems = [lineItem]
-
+
metadataTextField.text = config.metadata
- useNewWorkflows = config.newWorkflows
- useNewWorkflowsSwitch.isOn = config.newWorkflows
}
}
diff --git a/Debug App/Sources/View Controllers/New UI/StripeAchHeadless/MerchantHeadlessCheckoutStripeAchViewController+StripeAch.swift b/Debug App/Sources/View Controllers/New UI/StripeAchHeadless/MerchantHeadlessCheckoutStripeAchViewController+StripeAch.swift
index 35c2dcbfd0..cda8a61d69 100644
--- a/Debug App/Sources/View Controllers/New UI/StripeAchHeadless/MerchantHeadlessCheckoutStripeAchViewController+StripeAch.swift
+++ b/Debug App/Sources/View Controllers/New UI/StripeAchHeadless/MerchantHeadlessCheckoutStripeAchViewController+StripeAch.swift
@@ -88,7 +88,7 @@ extension MerchantHeadlessCheckoutStripeAchViewController: PrimerHeadlessUnivers
func primerHeadlessUniversalCheckoutDidFail(withError err: any Error, checkoutData: PrimerCheckoutData?) {
print("\n\nMERCHANT APP\n\(#function)\nerror: \(err)\ncheckoutData: \(String(describing: checkoutData))")
logs.append(#function)
- presentResultsVC(checkoutData: nil, error: err)
+ presentResultsVC(checkoutData: checkoutData, error: err)
}
func primerHeadlessUniversalCheckoutDidReceiveAdditionalInfo(_ additionalInfo: PrimerCheckoutAdditionalInfo?) {
diff --git a/Debug App/Tests/AppetizeConfigTests.swift b/Debug App/Tests/AppetizeConfigTests.swift
index 818dd6f950..835f9451fd 100644
--- a/Debug App/Tests/AppetizeConfigTests.swift
+++ b/Debug App/Tests/AppetizeConfigTests.swift
@@ -79,7 +79,6 @@ private extension SessionConfiguration {
city: "mock-city",
postalCode: "mock-postalCode",
vault: false,
- newWorkflows: false,
environment: "mock-environment",
customApiKey: "mock-customApiKey",
metadata: "mock-metadata")
diff --git a/Debug App/Tests/MetadataParserTests.swift b/Debug App/Tests/MetadataParserTests.swift
index 907624e950..61f6ed38f3 100644
--- a/Debug App/Tests/MetadataParserTests.swift
+++ b/Debug App/Tests/MetadataParserTests.swift
@@ -13,29 +13,6 @@ final class MetadataParserTests: XCTestCase {
private let metadataParser = MetadataParser()
- func testParseMetadataWithJSON() throws {
- let jsonMetadata = #"{"key1": "value1", "key2": 123}"#
-
- let result = metadataParser.parse(jsonMetadata)
-
- XCTAssertEqual(result["key1"] as? String, "value1")
- XCTAssertEqual(result["key2"] as? Int, 123)
- }
-
- func testParseMetadataWithKeyValuePairs() throws {
- let keyValueMetadata = """
- key1=value1
- doubleKey=123.0
- intKey=123
- """
-
- let result = metadataParser.parse(keyValueMetadata)
-
- XCTAssertEqual(result["key1"] as? String, "value1")
- XCTAssertEqual(result["doubleKey"] as? Double, 123)
- XCTAssertEqual(result["intKey"] as? Int, 123)
- }
-
func testParseMetadataWithInvalidMetadata() throws {
let invalidMetadata = "invalid json"
diff --git a/Sources/PrimerSDK/Classes/Core/Constants/Colors.swift b/Sources/PrimerSDK/Classes/Core/Constants/Colors.swift
index 46693e5a52..08028a4e30 100644
--- a/Sources/PrimerSDK/Classes/Core/Constants/Colors.swift
+++ b/Sources/PrimerSDK/Classes/Core/Constants/Colors.swift
@@ -1,6 +1,7 @@
// MARK: Light
import UIKit
+import SwiftUI
public struct PrimerColors {
@@ -36,3 +37,28 @@ public struct PrimerColors {
static let klarnaPink = UIColor(red: 1, green: 0.702, blue: 0.78, alpha: 1)
static let blurredBackground = UIColor.black.withAlphaComponent(0.4)
}
+
+// Extension to produce a SwiftUI Color which works until iOS 13
+extension PrimerColors {
+ static func swiftColor(_ uiColor: UIColor) -> Color {
+ if #available(iOS 14.0, *) {
+ return Color(uiColor)
+ } else {
+ // For iOS 13, we need to manually convert using components
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+
+ return Color(
+ .sRGB,
+ red: Double(red),
+ green: Double(green),
+ blue: Double(blue),
+ opacity: Double(alpha)
+ )
+ }
+ }
+}
diff --git a/Sources/PrimerSDK/Classes/Core/Payment Services/CreateResumePaymentService.swift b/Sources/PrimerSDK/Classes/Core/Payment Services/CreateResumePaymentService.swift
index 0410d9d211..068cb0226f 100644
--- a/Sources/PrimerSDK/Classes/Core/Payment Services/CreateResumePaymentService.swift
+++ b/Sources/PrimerSDK/Classes/Core/Payment Services/CreateResumePaymentService.swift
@@ -9,6 +9,9 @@ import Foundation
internal protocol CreateResumePaymentServiceProtocol {
func createPayment(paymentRequest: Request.Body.Payment.Create) -> Promise
+ func completePayment(clientToken: DecodedJWTToken,
+ completeUrl: URL,
+ body: Request.Body.Payment.Complete) -> Promise
func resumePaymentWithPaymentId(_ paymentId: String, paymentResumeRequest: Request.Body.Payment.Resume) -> Promise
}
@@ -97,4 +100,34 @@ internal class CreateResumePaymentService: CreateResumePaymentServiceProtocol {
}
}
}
+
+ /**
+ * Completes a payment using the provided JWT token and URL.
+ *
+ * This private method performs an API call to complete a payment, using a decoded JWT token for authentication
+ * and a URL indicating where the completion request should be sent.
+ *
+ * - Parameters:
+ * - clientToken: A `DecodedJWTToken` representing the client's authentication token.
+ * - completeUrl: An `URL` indicating the endpoint for completing the ACH payment.
+ *
+ * - Returns: A `Promise` that resolves if the payment is completed successfully, or rejects if there is
+ * an error during the API call.
+ */
+ func completePayment(clientToken: DecodedJWTToken,
+ completeUrl: URL,
+ body: Request.Body.Payment.Complete) -> Promise {
+ return Promise { seal in
+ self.apiClient.completePayment(clientToken: clientToken,
+ url: completeUrl,
+ paymentRequest: body) { result in
+ switch result {
+ case .success:
+ seal.fulfill()
+ case .failure(let error):
+ seal.reject(error)
+ }
+ }
+ }
+ }
}
diff --git a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Services/ACHTokenizationService.swift b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Services/ACHTokenizationService.swift
index efb8d571b4..c2bbf9e4b0 100644
--- a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Services/ACHTokenizationService.swift
+++ b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Services/ACHTokenizationService.swift
@@ -71,37 +71,37 @@ class ACHTokenizationService: ACHTokenizationDelegate, ACHValidationDelegate {
else {
throw ACHHelpers.getInvalidTokenError()
}
-
+
guard paymentMethod.id != nil else {
throw ACHHelpers.getInvalidValueError(
key: "configuration.id",
value: paymentMethod.id
)
}
-
+
if AppState.current.amount == nil {
throw ACHHelpers.getInvalidSettingError(name: "amount")
}
-
+
if AppState.current.currency == nil {
throw ACHHelpers.getInvalidSettingError(name: "currency")
}
-
+
let lineItems = clientSession?.order?.lineItems ?? []
if lineItems.isEmpty {
throw ACHHelpers.getInvalidValueError(key: "lineItems")
}
-
+
if !(lineItems.filter({ $0.amount == nil })).isEmpty {
throw ACHHelpers.getInvalidValueError(key: "settings.orderItems")
}
-
+
guard let publishableKey = PrimerSettings.current.paymentMethodOptions.stripeOptions?.publishableKey,
!publishableKey.isEmpty
else {
throw ACHHelpers.getInvalidValueError(key: "stripeOptions.publishableKey")
}
-
+
do {
_ = try PrimerSettings.current.paymentMethodOptions.validSchemeForUrlScheme()
} catch let error {
diff --git a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Stripe/Component/StripeAchTokenizationViewModel.swift b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Stripe/Component/StripeAchTokenizationViewModel.swift
index 0acc423b25..82e3cff5be 100644
--- a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Stripe/Component/StripeAchTokenizationViewModel.swift
+++ b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Composable/ACH/Stripe/Component/StripeAchTokenizationViewModel.swift
@@ -165,7 +165,9 @@ class StripeAchTokenizationViewModel: PaymentMethodTokenizationViewModel {
return self.awaitUserInput()
}
.then { () -> Promise in
- return self.completePayment(clientToken: decodedJWTToken, completeUrl: sdkCompleteUrl)
+ return self.createResumePaymentService.completePayment(clientToken: decodedJWTToken,
+ completeUrl: sdkCompleteUrl,
+ body: StripeAchTokenizationViewModel.defaultCompleteBodyWithTimestamp)
}
.done {
seal.fulfill(nil)
@@ -326,36 +328,6 @@ class StripeAchTokenizationViewModel: PaymentMethodTokenizationViewModel {
}
}
}
-
- /**
- * Completes a payment using the provided JWT token and URL.
- *
- * This private method performs an API call to complete a payment, using a decoded JWT token for authentication
- * and a URL indicating where the completion request should be sent.
- *
- * - Parameters:
- * - clientToken: A `DecodedJWTToken` representing the client's authentication token.
- * - completeUrl: An `URL` indicating the endpoint for completing the ACH payment.
- *
- * - Returns: A `Promise` that resolves if the payment is completed successfully, or rejects if there is
- * an error during the API call.
- */
- private func completePayment(clientToken: DecodedJWTToken, completeUrl: URL) -> Promise {
- return Promise { seal in
- let apiClient: PrimerAPIClientAchProtocol = PrimerAPIConfigurationModule.apiClient ?? PrimerAPIClient()
- let timeZone = TimeZone(abbreviation: "UTC")
- let timeStamp = Date().toString(timeZone: timeZone)
- let body = Request.Body.Payment.Complete(mandateSignatureTimestamp: timeStamp, paymentMethodId: returnedStripeAchPaymentId)
- apiClient.completePayment(clientToken: clientToken, url: completeUrl, paymentRequest: body) { result in
- switch result {
- case .success:
- seal.fulfill()
- case .failure(let error):
- seal.reject(error)
- }
- }
- }
- }
}
// MARK: Drop-In
@@ -425,6 +397,13 @@ extension StripeAchTokenizationViewModel: ACHUserDetailsDelegate {
}
}
}
+
+ static var defaultCompleteBodyWithTimestamp: Request.Body.Payment.Complete {
+ let timeZone = TimeZone(abbreviation: "UTC")
+ let timeStamp = Date().toString(timeZone: timeZone)
+
+ return Request.Body.Payment.Complete(mandateSignatureTimestamp: timeStamp)
+ }
}
// MARK: Headless
diff --git a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/Payment Method Managers/PrimerPaymentMethodManager.swift b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/Payment Method Managers/PrimerPaymentMethodManager.swift
index d65c5ec328..b746ea2e6a 100644
--- a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/Payment Method Managers/PrimerPaymentMethodManager.swift
+++ b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/Payment Method Managers/PrimerPaymentMethodManager.swift
@@ -12,6 +12,7 @@ public enum PrimerPaymentMethodManagerCategory: String {
case rawData = "RAW_DATA"
case nolPay = "NOL_PAY"
case componentWithRedirect = "COMPONENT_WITH_REDIRECT"
+ case stripeAch = "STRIPE_ACH"
}
internal protocol PrimerPaymentMethodManager {
diff --git a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/VaultManager.swift b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/VaultManager.swift
index bfd602a91d..b635a2a0af 100644
--- a/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/VaultManager.swift
+++ b/Sources/PrimerSDK/Classes/Core/PrimerHeadlessUniversalCheckout/Managers/VaultManager.swift
@@ -25,7 +25,6 @@ extension PrimerHeadlessUniversalCheckout {
private(set) var resumePaymentId: String?
private var webViewController: SFSafariViewController?
private var webViewCompletion: ((_ authorizationToken: String?, _ error: PrimerError?) -> Void)?
- private var achMandateCompletion: ((Result) -> Void)?
lazy var createResumePaymentService: CreateResumePaymentServiceProtocol = {
CreateResumePaymentService(paymentMethodType: paymentMethodType)
@@ -516,13 +515,9 @@ extension PrimerHeadlessUniversalCheckout {
}
firstly {
- sendAdditionalInfoEvent()
- }
- .then { () -> Promise in
- return self.awaitShowMandateResponse()
- }
- .then { () -> Promise in
- return self.completePayment(clientToken: decodedJWTToken, completeUrl: sdkCompleteUrl)
+ self.createResumePaymentService.completePayment(clientToken: decodedJWTToken,
+ completeUrl: sdkCompleteUrl,
+ body: StripeAchTokenizationViewModel.defaultCompleteBodyWithTimestamp)
}
.done {
seal.fulfill(nil)
@@ -813,111 +808,6 @@ extension PrimerHeadlessUniversalCheckout {
}
}
}
-
- /**
- * Completes a payment using the provided JWT token and URL.
- *
- * This private method performs an API call to complete a payment, using a decoded JWT token for authentication
- * and a URL indicating where the completion request should be sent.
- *
- * - Parameters:
- * - clientToken: A `DecodedJWTToken` representing the client's authentication token.
- * - completeUrl: An `URL` indicating the endpoint for completing the ACH payment.
- *
- * - Returns: A `Promise` that resolves if the payment is completed successfully, or rejects if there is
- * an error during the API call.
- */
- private func completePayment(clientToken: DecodedJWTToken, completeUrl: URL) -> Promise {
- return Promise { seal in
- let apiClient: PrimerAPIClientAchProtocol = PrimerAPIConfigurationModule.apiClient ?? PrimerAPIClient()
- let timeZone = TimeZone(abbreviation: "UTC")
- let timeStamp = Date().toString(timeZone: timeZone)
-
- let body = Request.Body.Payment.Complete(mandateSignatureTimestamp: timeStamp)
- apiClient.completePayment(clientToken: clientToken, url: completeUrl, paymentRequest: body) { result in
- switch result {
- case .success:
- seal.fulfill()
- case .failure(let error):
- seal.reject(error)
- }
- }
- }
- }
-
- /**
- * Sends additional information via delegate `PrimerHeadlessUniversalCheckoutDelegate` if implemented in the headless checkout context.
- *
- * This private method checks if the checkout is being conducted in a headless mode and whether the delegate
- * for handling additional information is implemented. It ensures that additional information events are only
- * sent if the delegate and its respective method are available, otherwise, it handles the absence of the delegate
- * method by logging an error and rejecting the promise.
- *
- * - Returns: A promise that resolves if additional information is successfully handled or sent, or rejects if
- * there are issues with the delegate implementation or if the delegate method is not implemented.
- */
- private func sendAdditionalInfoEvent() -> Promise {
- return Promise { seal in
- guard PrimerHeadlessUniversalCheckout.current.delegate != nil else {
- seal.fulfill()
- return
- }
-
- let delegate = PrimerHeadlessUniversalCheckout.current.delegate
- let isAdditionalInfoImplemented = delegate?.primerHeadlessUniversalCheckoutDidReceiveAdditionalInfo != nil
-
- guard isAdditionalInfoImplemented else {
- let logMessage =
- """
- Delegate function 'primerHeadlessUniversalCheckoutDidReceiveAdditionalInfo(_ additionalInfo: PrimerCheckoutAdditionalInfo?)'\
- hasn't been implemented. No events will be sent to your delegate instance.
- """
- logger.warn(message: logMessage)
-
- let message = "Couldn't continue due to unimplemented delegate method `primerHeadlessUniversalCheckoutDidReceiveAdditionalInfo`"
- let error = PrimerError.unableToPresentPaymentMethod(paymentMethodType: self.paymentMethodType,
- userInfo: .errorUserInfoDictionary(additionalInfo: [
- "message": message
- ]),
- diagnosticsId: UUID().uuidString)
-
- seal.reject(error)
- return
- }
-
- let additionalInfo = ACHMandateAdditionalInfo()
- PrimerDelegateProxy.primerDidReceiveAdditionalInfo(additionalInfo)
- seal.fulfill()
- }
- }
-
- /**
- * Waits for a response from the ACHMandateDelegate method.
- * The response is returned in stripeMandateCompletion handler.
- */
- private func awaitShowMandateResponse() -> Promise {
- return Promise { seal in
- self.achMandateCompletion = { result in
- switch result {
- case .success:
- seal.fulfill()
- case .failure(let error):
- seal.reject(error)
- }
- }
- }
- }
- }
-}
-
-extension PrimerHeadlessUniversalCheckout.VaultManager: ACHMandateDelegate {
- public func acceptMandate() {
- achMandateCompletion?(.success(()))
- }
-
- public func declineMandate() {
- let error = ACHHelpers.getCancelledError(paymentMethodType: paymentMethodType)
- achMandateCompletion?(.failure(error))
}
}
diff --git a/Sources/PrimerSDK/Classes/Data Models/ImageName.swift b/Sources/PrimerSDK/Classes/Data Models/ImageName.swift
index 32d1c982af..5696d7bef9 100644
--- a/Sources/PrimerSDK/Classes/Data Models/ImageName.swift
+++ b/Sources/PrimerSDK/Classes/Data Models/ImageName.swift
@@ -28,6 +28,7 @@ public enum ImageName: String {
lock,
rightArrow,
bank,
+ achBank,
camera,
error,
klarna,
diff --git a/Sources/PrimerSDK/Classes/Data Models/PrimerPaymentMethod.swift b/Sources/PrimerSDK/Classes/Data Models/PrimerPaymentMethod.swift
index 2b268e63d7..269e6e7f1d 100644
--- a/Sources/PrimerSDK/Classes/Data Models/PrimerPaymentMethod.swift
+++ b/Sources/PrimerSDK/Classes/Data Models/PrimerPaymentMethod.swift
@@ -238,7 +238,7 @@ class PrimerPaymentMethod: Codable, LogReporter {
categories.append(PrimerPaymentMethodManagerCategory.componentWithRedirect)
case .stripeAch:
- categories.append(PrimerPaymentMethodManagerCategory.nativeUI)
+ categories.append(PrimerPaymentMethodManagerCategory.stripeAch)
case .adyenBlik:
categories.append(PrimerPaymentMethodManagerCategory.rawData)
diff --git a/Sources/PrimerSDK/Classes/Data Models/TokenizationResponse.swift b/Sources/PrimerSDK/Classes/Data Models/TokenizationResponse.swift
index c73a8a8c5c..29f9d019e8 100644
--- a/Sources/PrimerSDK/Classes/Data Models/TokenizationResponse.swift
+++ b/Sources/PrimerSDK/Classes/Data Models/TokenizationResponse.swift
@@ -67,6 +67,7 @@ extension Response.Body.Tokenization {
case .payPalBillingAgreement: return .paypal2
case .goCardlessMandate: return .bank
case .klarnaCustomerToken: return .klarna
+ case .stripeAch: return .achBank
default: return .creditCard
}
}
@@ -108,6 +109,14 @@ extension Response.Body.Tokenization {
expiry: "",
imageName: self.icon,
paymentMethodType: self.paymentInstrumentType)
+ case .stripeAch:
+ return CardButtonViewModel(
+ network: self.paymentInstrumentData?.bankName ?? "Bank account",
+ cardholder: "•••• \(self.paymentInstrumentData?.accountNumberLastFourDigits ?? "")",
+ last4: "",
+ expiry: "",
+ imageName: self.icon,
+ paymentMethodType: self.paymentInstrumentType)
default:
return nil
}
@@ -144,6 +153,9 @@ extension Response.Body.Tokenization {
public let paymentMethodType: String?
public let sessionInfo: SessionInfo?
+ public let bankName: String?
+ public let accountNumberLastFourDigits: String?
+
// swiftlint:disable:next nesting
public struct SessionInfo: Codable {
public let locale: String?
diff --git a/Sources/PrimerSDK/Classes/Services/Network/Protocols/PrimerAPIClientCreateResumePaymentProtocol.swift b/Sources/PrimerSDK/Classes/Services/Network/Protocols/PrimerAPIClientCreateResumePaymentProtocol.swift
index 8795061adf..ba2a51caac 100644
--- a/Sources/PrimerSDK/Classes/Services/Network/Protocols/PrimerAPIClientCreateResumePaymentProtocol.swift
+++ b/Sources/PrimerSDK/Classes/Services/Network/Protocols/PrimerAPIClientCreateResumePaymentProtocol.swift
@@ -18,4 +18,10 @@ protocol PrimerAPIClientCreateResumePaymentProtocol {
paymentId: String,
paymentResumeRequest: Request.Body.Payment.Resume,
completion: @escaping APICompletion)
+
+ func completePayment(
+ clientToken: DecodedJWTToken,
+ url: URL,
+ paymentRequest: Request.Body.Payment.Complete,
+ completion: @escaping APICompletion)
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateView.swift b/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateView.swift
index b0e8f1a8b9..3c9ddcdc34 100644
--- a/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateView.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateView.swift
@@ -64,6 +64,7 @@ struct ACHMandateView: View {
.disabled(viewModel.shouldDisableViews)
.padding([.horizontal, .bottom])
}
+ .background(PrimerColors.swiftColor(PrimerColors.white))
}
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateViewController.swift b/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateViewController.swift
index 5e2edc6216..bfcb47fc0e 100644
--- a/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/ACH Mandate Sheet/ACHMandateViewController.swift
@@ -30,7 +30,7 @@ class ACHMandateViewController: PrimerViewController {
override func viewDidLoad() {
super.viewDidLoad()
- view.backgroundColor = .white
+ view.backgroundColor = PrimerColors.white
addMandateView()
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsView.swift b/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsView.swift
index 13b861eb8e..61531f777b 100644
--- a/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsView.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsView.swift
@@ -16,7 +16,7 @@ struct ACHUserDetailsView: View {
var onBackPressed: () -> Void
var body: some View {
-
+ VStack {
ZStack {
Button {
onBackPressed()
@@ -41,9 +41,8 @@ struct ACHUserDetailsView: View {
.font(.system(size: 18, weight: .medium))
.addAccessibilityIdentifier(identifier: AccessibilityIdentifier.StripeAchUserDetailsComponent.title.rawValue)
}
- .padding(.top, -3)
+ .background(PrimerColors.swiftColor(PrimerColors.white))
- VStack {
HStack {
Text(Strings.UserDetails.subtitle)
.font(.system(size: 17))
@@ -81,13 +80,19 @@ struct ACHUserDetailsView: View {
Spacer()
Button(action: submitAction) {
- Text(Strings.UserDetails.continueButton)
- .font(.system(size: 17, weight: .medium))
- .foregroundColor(viewModel.isValidForm ? Color.white : Color.gray)
- .frame(maxWidth: .infinity)
- .padding()
- .background(viewModel.isValidForm ? Color.blue : Color.gray.opacity(0.2))
- .cornerRadius(10)
+ ZStack {
+ if viewModel.shouldDisableViews {
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: UIColor.black)
+ } else {
+ Text(Strings.UserDetails.continueButton)
+ .font(.system(size: 17, weight: .medium))
+ }
+ }
+ .foregroundColor(viewModel.isValidForm ? Color.white : Color.gray)
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(viewModel.isValidForm ? Color.blue : Color.gray.opacity(0.2))
+ .cornerRadius(10)
}
.disabled(!viewModel.isValidForm)
.padding(.horizontal)
@@ -95,6 +100,7 @@ struct ACHUserDetailsView: View {
}
.disabled(viewModel.shouldDisableViews)
.frame(height: 380)
+ .background(PrimerColors.swiftColor(PrimerColors.white))
}
private func submitAction() {
@@ -121,7 +127,7 @@ struct CustomTextFieldView: View {
TextField("", text: $text)
.padding(.horizontal, 10)
.frame(height: 44)
- .background(Color.white)
+ .background(PrimerColors.swiftColor(PrimerColors.white))
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
diff --git a/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsViewController.swift b/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsViewController.swift
index ca0aff79b7..29b4701914 100644
--- a/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/ACH UserDetails Sheet/ACHUserDetailsViewController.swift
@@ -41,7 +41,7 @@ class ACHUserDetailsViewController: PrimerViewController {
override func viewDidLoad() {
super.viewDidLoad()
- view.backgroundColor = .white
+ view.backgroundColor = PrimerColors.white
addStripeFormView()
setupStripeACHDelegatesAndStart()
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/Components/PrimerCustomResult/PrimerResultPaymentStatusView.swift b/Sources/PrimerSDK/Classes/User Interface/Components/PrimerCustomResult/PrimerResultPaymentStatusView.swift
index b9b919f869..840e158523 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Components/PrimerCustomResult/PrimerResultPaymentStatusView.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Components/PrimerCustomResult/PrimerResultPaymentStatusView.swift
@@ -68,5 +68,6 @@ struct PrimerResultPaymentStatusView: View {
}
}
.padding(.horizontal)
+ .background(PrimerColors.swiftColor(PrimerColors.white))
}
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/Primer/CardButton.swift b/Sources/PrimerSDK/Classes/User Interface/Primer/CardButton.swift
index b849486d72..9bc542cabe 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Primer/CardButton.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Primer/CardButton.swift
@@ -15,30 +15,20 @@ internal class CardButton: PrimerButton {
private var last4Label = UILabel()
private var expiryLabel = UILabel()
private var border = PrimerView()
- private var checkView = UIImageView()
- private weak var checkmarkViewWidthConstraint: NSLayoutConstraint?
- private weak var checkmarkViewTrailingConstraint: NSLayoutConstraint?
- private weak var checkmarkViewLeadingConstraint: NSLayoutConstraint?
- private weak var checkmarkViewHeightConstraint: NSLayoutConstraint?
-
- var showIcon = true
-
- func render(model: CardButtonViewModel?, showIcon: Bool = true) {
+ func render(model: CardButtonViewModel?) {
guard let model = model else { return }
accessibilityIdentifier = "saved_payment_method_button"
let theme: PrimerThemeProtocol = DependencyContainer.resolve()
backgroundColor = theme.paymentMethodButton.color(for: .enabled)
- addCheckmarkView()
- if showIcon {
-
+ if model.paymentMethodType == .paymentCard || model.paymentMethodType == .cardOffSession {
+ addCardIcon(image: CardNetwork(cardNetworkStr: model.network).icon)
} else {
- toggleIcon()
+ addCardIcon(image: model.imageName.image)
}
- addCardIcon(image: CardNetwork(cardNetworkStr: model.network).icon)
addBorder()
switch model.paymentMethodType {
@@ -95,10 +85,13 @@ internal class CardButton: PrimerButton {
iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 17).isActive = true
iconView.heightAnchor.constraint(equalToConstant: 24).isActive = true
iconView.widthAnchor.constraint(equalToConstant: 24).isActive = true
+ } else if iconView.image == ImageName.achBank.image {
+ iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
+ iconView.widthAnchor.constraint(equalToConstant: 56).isActive = true
} else {
iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
- iconView.heightAnchor.constraint(equalToConstant: 28).isActive = true
- iconView.widthAnchor.constraint(equalToConstant: 38).isActive = true
+ iconView.heightAnchor.constraint(equalToConstant: 41).isActive = true
+ iconView.widthAnchor.constraint(equalToConstant: 56).isActive = true
}
}
@@ -155,9 +148,7 @@ internal class CardButton: PrimerButton {
last4Label.textColor = theme.paymentMethodButton.text.color
addSubview(last4Label)
last4Label.translatesAutoresizingMaskIntoConstraints = false
- checkmarkViewLeadingConstraint = last4Label.trailingAnchor
- .constraint(equalTo: checkView.leadingAnchor, constant: -14)
- checkmarkViewLeadingConstraint?.isActive = true
+ last4Label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14).isActive = true
last4Label.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
@@ -186,40 +177,6 @@ internal class CardButton: PrimerButton {
border.isUserInteractionEnabled = false
}
- private func addCheckmarkView() {
- let theme: PrimerThemeProtocol = DependencyContainer.resolve()
- checkView = UIImageView(image: ImageName.check2.image)
-
- // color
- let tintedIcon = ImageName.check2.image?.withRenderingMode(.alwaysTemplate)
- checkView.tintColor = theme.paymentMethodButton.text.color
- checkView.image = tintedIcon
-
- addSubview(checkView)
- checkView.translatesAutoresizingMaskIntoConstraints = false
- checkView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
- checkmarkViewTrailingConstraint = checkView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14)
- checkmarkViewWidthConstraint = checkView.widthAnchor.constraint(equalToConstant: 14)
- checkmarkViewTrailingConstraint?.isActive = true
- checkmarkViewWidthConstraint?.isActive = true
- checkmarkViewHeightConstraint = checkView.heightAnchor.constraint(equalToConstant: 22)
- checkmarkViewHeightConstraint?.isActive = true
- }
-
- func showDeleteIcon(_ flag: Bool) {
- checkView.image = flag ? ImageName.delete.image : ImageName.check2.image
- }
-
- func toggleIcon() {
- checkmarkViewTrailingConstraint?.constant = showIcon ? -14 : 0
- checkmarkViewWidthConstraint?.constant = showIcon ? 14 : 0
- checkmarkViewHeightConstraint?.constant = showIcon ? 14 : 0
- }
-
- func showCheckmarkIcon(_ val: Bool) {
- checkView.isHidden = !val
- }
-
func hideBorder() {
border.isHidden = true
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerContainerViewController.swift b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerContainerViewController.swift
index 32151d49b3..0a8027d00c 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerContainerViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerContainerViewController.swift
@@ -30,6 +30,7 @@ class PrimerContainerViewController: PrimerViewController {
override func viewDidLoad() {
super.viewDidLoad()
+ self.view.accessibilityIdentifier = "checkout_sheet_content"
scrollView.accessibilityIdentifier = "primer_container_scroll_view"
view.addSubview(mockedNavigationBar)
diff --git a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerFormViewController.swift b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerFormViewController.swift
index d5ef3658e2..1b807ea378 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerFormViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerFormViewController.swift
@@ -116,7 +116,7 @@ class PrimerFormViewController: PrimerViewController {
paymentMethodsStack.addArrangedSubview(unknownFeesContainerView)
}
- paymentMethodsContainerStack.addArrangedSubview( paymentMethodsStack)
+ paymentMethodsContainerStack.addArrangedSubview(paymentMethodsStack)
stackView.addArrangedSubview(paymentMethodsContainerStack)
}
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerUniversalCheckoutViewController.swift b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerUniversalCheckoutViewController.swift
index bca7939ffc..745f99bd30 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Root/PrimerUniversalCheckoutViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Root/PrimerUniversalCheckoutViewController.swift
@@ -197,7 +197,7 @@ internal class PrimerUniversalCheckoutViewController: PrimerFormViewController {
savedCardView.backgroundColor = .white
savedCardView.translatesAutoresizingMaskIntoConstraints = false
savedCardView.heightAnchor.constraint(equalToConstant: 64.0).isActive = true
- savedCardView.render(model: cardButtonViewModel, showIcon: false)
+ savedCardView.render(model: cardButtonViewModel)
paymentMethodStackView.addArrangedSubview(savedCardView)
}
diff --git a/Sources/PrimerSDK/Classes/User Interface/TokenizationViewModels/CheckoutWithVaultedPaymentMethodViewModel.swift b/Sources/PrimerSDK/Classes/User Interface/TokenizationViewModels/CheckoutWithVaultedPaymentMethodViewModel.swift
index 3ba507145b..c90d77c5e1 100644
--- a/Sources/PrimerSDK/Classes/User Interface/TokenizationViewModels/CheckoutWithVaultedPaymentMethodViewModel.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/TokenizationViewModels/CheckoutWithVaultedPaymentMethodViewModel.swift
@@ -61,6 +61,8 @@ class CheckoutWithVaultedPaymentMethodViewModel: LogReporter {
.done { checkoutData in
if let checkoutData = checkoutData {
PrimerDelegateProxy.primerDidCompleteCheckoutWithData(checkoutData)
+ } else if let checkoutData = self.paymentCheckoutData {
+ PrimerDelegateProxy.primerDidCompleteCheckoutWithData(checkoutData)
}
self.handleSuccessfulFlow()
@@ -461,7 +463,34 @@ Make sure you call the decision handler otherwise the SDK will hang.
private func handleDecodedClientTokenIfNeeded(_ decodedJWTToken: DecodedJWTToken, paymentMethodTokenData: PrimerPaymentMethodTokenData) -> Promise {
return Promise { seal in
- if decodedJWTToken.intent == RequiredActionName.threeDSAuthentication.rawValue {
+
+ if decodedJWTToken.intent?.contains("STRIPE_ACH") == true {
+ if let sdkCompleteUrlString = decodedJWTToken.sdkCompleteUrl,
+ let sdkCompleteUrl = URL(string: sdkCompleteUrlString) {
+
+ DispatchQueue.main.async {
+ PrimerUIManager.primerRootViewController?.enableUserInteraction(true)
+ }
+
+ firstly {
+ self.createResumePaymentService.completePayment(clientToken: decodedJWTToken,
+ completeUrl: sdkCompleteUrl,
+ body: StripeAchTokenizationViewModel.defaultCompleteBodyWithTimestamp)
+ }
+ .done {
+ seal.fulfill(nil)
+ }
+ .catch { err in
+ seal.reject(err)
+ }
+
+ } else {
+ let err = PrimerError.invalidClientToken(userInfo: .errorUserInfoDictionary(),
+ diagnosticsId: UUID().uuidString)
+ ErrorHandler.handle(error: err)
+ seal.reject(err)
+ }
+ } else if decodedJWTToken.intent == RequiredActionName.threeDSAuthentication.rawValue {
let threeDSService = ThreeDSService()
threeDSService.perform3DS(
@@ -495,9 +524,14 @@ Make sure you call the decision handler otherwise the SDK will hang.
}
func handleSuccessfulFlow() {
- let categories = self.config.paymentMethodManagerCategories
- PrimerUIManager.dismissOrShowResultScreen(type: .success,
- paymentMethodManagerCategories: categories ?? [])
+ if let paymentMethodType = config.internalPaymentMethodType,
+ paymentMethodType == .stripeAch {
+ PrimerUIManager.showResultScreen(for: paymentMethodType, error: nil)
+ } else {
+ let categories = self.config.paymentMethodManagerCategories
+ PrimerUIManager.dismissOrShowResultScreen(type: .success,
+ paymentMethodManagerCategories: categories ?? [])
+ }
}
func handleFailureFlow(errorMessage: String?) {
diff --git a/Sources/PrimerSDK/Classes/User Interface/Vault/VaultPaymentMethodViewController.swift b/Sources/PrimerSDK/Classes/User Interface/Vault/VaultPaymentMethodViewController.swift
index 05cad32273..c91937bbea 100644
--- a/Sources/PrimerSDK/Classes/User Interface/Vault/VaultPaymentMethodViewController.swift
+++ b/Sources/PrimerSDK/Classes/User Interface/Vault/VaultPaymentMethodViewController.swift
@@ -47,8 +47,8 @@ internal class VaultedPaymentInstrumentCell: UITableViewCell {
if cardNetworkImageView.superview == nil {
horizontalStackView.addArrangedSubview(cardNetworkImageView)
cardNetworkImageView.translatesAutoresizingMaskIntoConstraints = false
- cardNetworkImageView.widthAnchor.constraint(equalToConstant: 28).isActive = true
- cardNetworkImageView.heightAnchor.constraint(equalToConstant: 38).isActive = true
+ cardNetworkImageView.widthAnchor.constraint(equalToConstant: 41).isActive = true
+ cardNetworkImageView.heightAnchor.constraint(equalToConstant: 56).isActive = true
}
if verticalLeftStackView.superview == nil {
@@ -109,12 +109,18 @@ internal class VaultedPaymentInstrumentCell: UITableViewCell {
verticalRightStackView.distribution = .fillEqually
verticalRightStackView.spacing = 0
- if let network = paymentMethod.cardButtonViewModel?.network {
- let cardNetworkImage = CardNetwork(cardNetworkStr: network).icon
- cardNetworkImageView.image = cardNetworkImage
+ if paymentMethod.cardButtonViewModel?.paymentMethodType == .paymentCard || paymentMethod.cardButtonViewModel?.paymentMethodType == .cardOffSession {
+
+ if let network = paymentMethod.cardButtonViewModel?.network {
+ let cardNetworkImage = CardNetwork(cardNetworkStr: network).icon
+ cardNetworkImageView.image = cardNetworkImage
+ } else {
+ cardNetworkImageView.image = paymentMethod.cardButtonViewModel?.imageName.image
+ }
} else {
cardNetworkImageView.image = paymentMethod.cardButtonViewModel?.imageName.image
}
+
cardNetworkImageView.contentMode = .scaleAspectFit
checkmarkImageView.image = isDeleting ?
diff --git a/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/Contents.json b/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/Contents.json
new file mode 100644
index 0000000000..30d8580b65
--- /dev/null
+++ b/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "ach-bank.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/ach-bank.pdf b/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/ach-bank.pdf
new file mode 100644
index 0000000000..43d42915d1
Binary files /dev/null and b/Sources/PrimerSDK/Resources/Icons.xcassets/Logos/achBank.imageset/ach-bank.pdf differ
diff --git a/Tests/Primer/Managers/VaultManagerTests.swift b/Tests/Primer/Managers/VaultManagerTests.swift
index ec200f98f8..67781ee64d 100644
--- a/Tests/Primer/Managers/VaultManagerTests.swift
+++ b/Tests/Primer/Managers/VaultManagerTests.swift
@@ -17,8 +17,6 @@ final class VaultManagerTests: XCTestCase {
var tokenizationService: MockTokenizationService!
var createResumePaymentService: MockCreateResumePaymentService!
-
- var mandateDelegate: ACHMandateDelegate?
private var vaultService: MockVaultService!
@@ -49,8 +47,6 @@ final class VaultManagerTests: XCTestCase {
sut.tokenizationService = tokenizationService
sut.createResumePaymentService = createResumePaymentService
sut.vaultService = vaultService
-
- mandateDelegate = sut
PrimerHeadlessUniversalCheckout.current.delegate = headlessCheckoutDelegate
@@ -60,7 +56,6 @@ final class VaultManagerTests: XCTestCase {
override func tearDownWithError() throws {
sut = nil
rawDataManagerDelegate = nil
- mandateDelegate = nil
SDKSessionHelper.tearDown()
@@ -234,14 +229,6 @@ final class VaultManagerTests: XCTestCase {
expectCreatePayment.fulfill()
return self.paymentACHResponseBody
}
-
- let expectDidReceiveMandateAdditionalInfo = self.expectation(description: "didReceiveMandateAdditionalInfo is called")
- headlessCheckoutDelegate.onDidReceiveAdditionalInfo = { additionalInfo in
- expectDidReceiveMandateAdditionalInfo.fulfill()
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
- self.mandateDelegate?.acceptMandate()
- }
- }
let expectDidCompleteCheckout = self.expectation(description: "Headless checkout completed")
headlessCheckoutDelegate.onDidCompleteCheckoutWithData = { _ in
@@ -257,7 +244,6 @@ final class VaultManagerTests: XCTestCase {
wait(for: [
expectExchangeTokenData,
expectCreatePayment,
- expectDidReceiveMandateAdditionalInfo,
expectDidCompleteCheckout
], timeout: 20.0, enforceOrder: true)
}
diff --git a/Tests/Primer/Services/CreateResumePaymentServiceTests.swift b/Tests/Primer/Services/CreateResumePaymentServiceTests.swift
index 1a513d3486..60d984f65e 100644
--- a/Tests/Primer/Services/CreateResumePaymentServiceTests.swift
+++ b/Tests/Primer/Services/CreateResumePaymentServiceTests.swift
@@ -225,6 +225,31 @@ final class CreateResumePaymentServiceTests: XCTestCase {
waitForExpectations(timeout: 5, handler: nil)
}
+ func test_complete_token_completeUrl() throws {
+ let response = Response.Body.Complete.successResponse
+ let apiClient = MockCreateResumeAPI(completeResponse: response)
+
+ let createResumeService = CreateResumePaymentService(paymentMethodType: "STRIPE_ACH",
+ apiClient: apiClient)
+
+ AppState.current.clientToken = MockAppState.mockClientToken
+ let expectation = self.expectation(description: "Promise fulfilled")
+
+ guard let clientToken = PrimerAPIConfigurationModule.decodedJWTToken else {
+ XCTFail()
+ return
+ }
+
+ let _ = createResumeService.completePayment(clientToken: clientToken,
+ completeUrl: URL(string: "https://example.com")!,
+ body: StripeAchTokenizationViewModel.defaultCompleteBodyWithTimestamp
+ ).done {
+ expectation.fulfill()
+ }
+
+ waitForExpectations(timeout: 5, handler: nil)
+ }
+
}
@@ -232,10 +257,14 @@ private class MockCreateResumeAPI: PrimerAPIClientCreateResumePaymentProtocol {
var resumeResponse: APIResult?
var createResponse: APIResult?
+ var completeResponse: APIResult?
- init(resumeResponse: APIResult? = nil, createResponse: APIResult? = nil) {
+ init(resumeResponse: APIResult? = nil,
+ createResponse: APIResult? = nil,
+ completeResponse: APIResult? = nil) {
self.resumeResponse = resumeResponse
self.createResponse = createResponse
+ self.completeResponse = completeResponse
}
func createPayment(clientToken: DecodedJWTToken, paymentRequestBody: Request.Body.Payment.Create, completion: @escaping APICompletion) {
@@ -253,6 +282,16 @@ private class MockCreateResumeAPI: PrimerAPIClientCreateResumePaymentProtocol {
}
completion(resumeResponse)
}
+
+ func completePayment(clientToken: DecodedJWTToken,
+ url: URL, paymentRequest: Request.Body.Payment.Complete,
+ completion: @escaping APICompletion) {
+ guard let completeResponse else {
+ XCTFail("No complete response set")
+ return
+ }
+ completion(completeResponse)
+ }
}
private extension Response.Body.Payment {
@@ -324,3 +363,9 @@ private extension Response.Body.Payment {
diagnosticsId: ""))
}
}
+
+private extension Response.Body.Complete {
+ static var successResponse: APIResult {
+ .success(.init())
+ }
+}
diff --git a/Tests/Primer/v2/HeadlessVaultManagerTests.swift b/Tests/Primer/v2/HeadlessVaultManagerTests.swift
index 661449b6aa..919ec06cb3 100644
--- a/Tests/Primer/v2/HeadlessVaultManagerTests.swift
+++ b/Tests/Primer/v2/HeadlessVaultManagerTests.swift
@@ -102,7 +102,9 @@ final class HeadlessVaultManagerTests: XCTestCase {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil),
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil),
threeDSecureAuthentication: nil,
token: "anything",
tokenType: .multiUse,
@@ -301,7 +303,9 @@ final class HeadlessVaultManagerTests: XCTestCase {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil),
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil),
threeDSecureAuthentication: nil,
token: "anything",
tokenType: .multiUse,
@@ -389,7 +393,9 @@ final class HeadlessVaultManagerTests: XCTestCase {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil),
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil),
analyticsId: "analytics-id")
]
@@ -466,7 +472,9 @@ final class HeadlessVaultManagerTests: XCTestCase {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil),
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil),
analyticsId: "analytics-id")
]
@@ -505,7 +513,9 @@ final class HeadlessVaultManagerTests: XCTestCase {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil),
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil),
analyticsId: "analytics-id")
]
diff --git a/Tests/Utilities/Mocks.swift b/Tests/Utilities/Mocks.swift
index 9b3e4aacae..4aa1a423d9 100644
--- a/Tests/Utilities/Mocks.swift
+++ b/Tests/Utilities/Mocks.swift
@@ -74,7 +74,9 @@ class Mocks {
productId: nil,
paymentMethodConfigId: nil,
paymentMethodType: nil,
- sessionInfo: nil)
+ sessionInfo: nil,
+ bankName: nil,
+ accountNumberLastFourDigits: nil)
static var payment = Response.Body.Payment(
id: "mock_id",
diff --git a/Tests/Utilities/Mocks/Services/CreateResumePaymentService.swift b/Tests/Utilities/Mocks/Services/CreateResumePaymentService.swift
index 3bce02ca76..a1d3b559f8 100644
--- a/Tests/Utilities/Mocks/Services/CreateResumePaymentService.swift
+++ b/Tests/Utilities/Mocks/Services/CreateResumePaymentService.swift
@@ -9,6 +9,14 @@ import Foundation
@testable import PrimerSDK
class MockCreateResumePaymentService: CreateResumePaymentServiceProtocol {
+ func completePayment(clientToken: PrimerSDK.DecodedJWTToken,
+ completeUrl: URL,
+ body: Request.Body.Payment.Complete) -> PrimerSDK.Promise {
+ Promise { seal in
+ seal.fulfill()
+ }
+ }
+
static var apiClient: (any PrimerSDK.PrimerAPIClientProtocol)?