diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8b61ad19..71bc3552f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,10 +25,9 @@ jobs: - name: Update version run: | today=$(date +'%Y-%m-%d') - sed -i '' 's/## unreleased.*/## '"${{ github.event.inputs.version }}"' ('"$today"')/' CHANGELOG.md sed -i '' 's/\(s\.version *= *\).*/\1"'"${{ github.event.inputs.version }}"'\"/' PayPal.podspec - + sed -i '' 's/payPalSDKVersion: String =.*/payPalSDKVersion: String = "${{ github.event.inputs.version }}"/' Sources/CorePayments/PayPalCoreConstants.swift plutil -replace CFBundleShortVersionString -string ${{ github.event.inputs.version }} -- 'Demo/Demo/Info.plist' git add . diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fe8af01..44a94557c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ * PaymentButtons * Remove deprecated colors `black`, `silver`, `blue`, and `darkBlue` +* PayPalWebPayments + * Add `vault(url:)` method to `PayPalWebCheckoutClient` + * Add `PayPalVaultResult` type to return vault result + * Add `PayPalVaultDelegate` to handle results from vault flow + * Add `PayPalWebCheckoutClientError.paypalVaultResponseError` for missing or invalid response from vaulting + ## 1.1.0 (2023-11-16) * PayPalNativePayments * Bump `PayPalCheckout` to `1.2.0` diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index ccd5e871f..29848c086 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -9,17 +9,19 @@ /* Begin PBXBuildFile section */ 3B20273D2A89E3F00007907E /* CreateSetupTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */; }; 3B20273F2A89F24E0007907E /* CardVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */; }; - 3B2027412A8A72050007907E /* CardVaultState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027402A8A72050007907E /* CardVaultState.swift */; }; + 3B2027412A8A72050007907E /* VaultState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027402A8A72050007907E /* VaultState.swift */; }; 3B2027432A8A95EF0007907E /* SetupTokenResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */; }; 3B2027452A8AA78B0007907E /* UpdateSetupTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */; }; 3B22E8BA2A842D8900962E34 /* PaymentTokenRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */; }; 3B22E8BC2A84397600962E34 /* PaymentTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */; }; + 3B3C51142B20F7CE009125FE /* PayPalVaultResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3C51132B20F7CE009125FE /* PayPalVaultResultView.swift */; }; 3B4DD9A02A892A7000F4A716 /* CardVaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */; }; 3B4DD9A22A8982B000F4A716 /* CardFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4DD9A12A8982B000F4A716 /* CardFormView.swift */; }; 3B6472A72AFAEB3A004745C4 /* PayPalWebTransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6472A62AFAEB3A004745C4 /* PayPalWebTransactionView.swift */; }; 3B80D50E2A291C0800D2EAC4 /* ClientIDRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */; }; 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */; }; 3B8EF4DB2A932DA300A70D0B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */; }; + 3BA0A58B2B1E240300330681 /* VaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA0A58A2B1E240300330681 /* VaultViewModel.swift */; }; 3BA56FE72A9DC9D70081D14F /* CardPaymentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FE62A9DC9D70081D14F /* CardPaymentViewModel.swift */; }; 3BA56FE92A9DCA520081D14F /* CardPaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FE82A9DCA520081D14F /* CardPaymentState.swift */; }; 3BA56FEC2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FEB2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift */; }; @@ -35,6 +37,8 @@ 3BA570032AA053AE0081D14F /* PayPalWebCreateOrderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA570022AA053AE0081D14F /* PayPalWebCreateOrderView.swift */; }; 3BA570072AA0DF330081D14F /* PayPalWebButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA570062AA0DF330081D14F /* PayPalWebButtonsView.swift */; }; 3BA5700B2AA13C1C0081D14F /* CoreConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA5700A2AA13C1C0081D14F /* CoreConfigManager.swift */; }; + 3BB60B532B1F9F1100A298CF /* PayPalVaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB60B522B1F9F1100A298CF /* PayPalVaultView.swift */; }; + 3BB60B552B1FA00C00A298CF /* PayPalVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB60B542B1FA00C00A298CF /* PayPalVaultViewModel.swift */; }; 3BB7A9772A5CA6FD00C05140 /* MerchantIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */; }; 3BC622072A97115700251B85 /* RoundedBlueButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC622062A97115700251B85 /* RoundedBlueButtonStyle.swift */; }; 3BC622092A97198500251B85 /* LeadingText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC622082A97198500251B85 /* LeadingText.swift */; }; @@ -66,15 +70,14 @@ BC9D4D2627C6D1720089E5B1 /* XCUIElement+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9D4D2527C6D1720089E5B1 /* XCUIElement+Helpers.swift */; }; BE1766B326F911A2007EF438 /* URLResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE1766B226F911A2007EF438 /* URLResponseError.swift */; }; BE1766D926FA7BC8007EF438 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = BE1766D826FA7BC8007EF438 /* Settings.bundle */; }; + BE5898952B2B91F800AA196E /* LabelViewText.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5898942B2B91F800AA196E /* LabelViewText.swift */; }; BE8117642B07E778009867B9 /* PayPalWebResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117632B07E778009867B9 /* PayPalWebResultView.swift */; }; - BE8117662B080202009867B9 /* PayPalWebStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117652B080202009867B9 /* PayPalWebStatusView.swift */; }; BE8117682B080472009867B9 /* CurrentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117672B080472009867B9 /* CurrentState.swift */; }; BE9F36D82745490400AFC7DA /* FloatingLabelTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9F36D72745490400AFC7DA /* FloatingLabelTextField.swift */; }; BECD84A027036DC2007CCAE4 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECD849F27036DC2007CCAE4 /* Environment.swift */; }; BECD84A227036DDB007CCAE4 /* Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECD84A127036DDB007CCAE4 /* Intent.swift */; }; BED042312710833F00C80954 /* CardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BED042302710833F00C80954 /* CardType.swift */; }; BED04233271084DF00C80954 /* CardFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BED04232271084DF00C80954 /* CardFormatter.swift */; }; - BEDE304A275EA33500D275FD /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEDE3049275EA33500D275FD /* UIViewController+Extension.swift */; }; CB1AC3B82982AAD70081AED6 /* CardPayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB1AC3B72982AAD70081AED6 /* CardPayments.framework */; }; CB1AC3B92982AAD70081AED6 /* CardPayments.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CB1AC3B72982AAD70081AED6 /* CardPayments.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CB1AC3BB2982BB130081AED6 /* PaymentButtons.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB1AC3BA2982BB130081AED6 /* PaymentButtons.framework */; }; @@ -126,17 +129,19 @@ /* Begin PBXFileReference section */ 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSetupTokenView.swift; sourceTree = ""; }; 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultViewModel.swift; sourceTree = ""; }; - 3B2027402A8A72050007907E /* CardVaultState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultState.swift; sourceTree = ""; }; + 3B2027402A8A72050007907E /* VaultState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultState.swift; sourceTree = ""; }; 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenResultView.swift; sourceTree = ""; }; 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenView.swift; sourceTree = ""; }; 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenRequest.swift; sourceTree = ""; }; 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenResponse.swift; sourceTree = ""; }; + 3B3C51132B20F7CE009125FE /* PayPalVaultResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultResultView.swift; sourceTree = ""; }; 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultView.swift; sourceTree = ""; }; 3B4DD9A12A8982B000F4A716 /* CardFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormView.swift; sourceTree = ""; }; 3B6472A62AFAEB3A004745C4 /* PayPalWebTransactionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalWebTransactionView.swift; sourceTree = ""; }; 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDRequest.swift; sourceTree = ""; }; 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDResponse.swift; sourceTree = ""; }; 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 3BA0A58A2B1E240300330681 /* VaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultViewModel.swift; sourceTree = ""; }; 3BA56FE62A9DC9D70081D14F /* CardPaymentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaymentViewModel.swift; sourceTree = ""; }; 3BA56FE82A9DCA520081D14F /* CardPaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaymentState.swift; sourceTree = ""; }; 3BA56FEB2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateOrderCardPaymentView.swift; sourceTree = ""; }; @@ -152,6 +157,8 @@ 3BA570022AA053AE0081D14F /* PayPalWebCreateOrderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebCreateOrderView.swift; sourceTree = ""; }; 3BA570062AA0DF330081D14F /* PayPalWebButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebButtonsView.swift; sourceTree = ""; }; 3BA5700A2AA13C1C0081D14F /* CoreConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreConfigManager.swift; sourceTree = ""; }; + 3BB60B522B1F9F1100A298CF /* PayPalVaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultView.swift; sourceTree = ""; }; + 3BB60B542B1FA00C00A298CF /* PayPalVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultViewModel.swift; sourceTree = ""; }; 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantIntegration.swift; sourceTree = ""; }; 3BC622062A97115700251B85 /* RoundedBlueButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedBlueButtonStyle.swift; sourceTree = ""; }; 3BC622082A97198500251B85 /* LeadingText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeadingText.swift; sourceTree = ""; }; @@ -190,15 +197,14 @@ BE1766B226F911A2007EF438 /* URLResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponseError.swift; sourceTree = ""; }; BE1766D826FA7BC8007EF438 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; BE420F3628189A7A00D8D66A /* PayPalUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PayPalUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BE5898942B2B91F800AA196E /* LabelViewText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelViewText.swift; sourceTree = ""; }; BE8117632B07E778009867B9 /* PayPalWebResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebResultView.swift; sourceTree = ""; }; - BE8117652B080202009867B9 /* PayPalWebStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebStatusView.swift; sourceTree = ""; }; BE8117672B080472009867B9 /* CurrentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentState.swift; sourceTree = ""; }; BE9F36D72745490400AFC7DA /* FloatingLabelTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingLabelTextField.swift; sourceTree = ""; }; BECD849F27036DC2007CCAE4 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; BECD84A127036DDB007CCAE4 /* Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Intent.swift; sourceTree = ""; }; BED042302710833F00C80954 /* CardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardType.swift; sourceTree = ""; }; BED04232271084DF00C80954 /* CardFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormatter.swift; sourceTree = ""; }; - BEDE3049275EA33500D275FD /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; CB1AC3B72982AAD70081AED6 /* CardPayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CardPayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CB1AC3BA2982BB130081AED6 /* PaymentButtons.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PaymentButtons.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CB1AC3BF2982C4030081AED6 /* PayPalWebPayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PayPalWebPayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -236,17 +242,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 3B43290F2A8FD7FD00C5441A /* CardVaultViews */ = { + 3B2501052B2679F000903EAB /* VaultViews */ = { isa = PBXGroup; children = ( - 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */, 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */, 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */, - 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */, - 3BF999752A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift */, 3BF999772A8AD072009CBDF2 /* CreatePaymentTokenView.swift */, 3BF999792A8AE12C009CBDF2 /* PaymentTokenResultView.swift */, ); + path = VaultViews; + sourceTree = ""; + }; + 3B43290F2A8FD7FD00C5441A /* CardVaultViews */ = { + isa = PBXGroup; + children = ( + 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */, + 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */, + 3BF999752A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift */, + ); path = CardVaultViews; sourceTree = ""; }; @@ -272,13 +285,21 @@ 3BA570022AA053AE0081D14F /* PayPalWebCreateOrderView.swift */, 3BA570002AA052E80081D14F /* PayPalWebPaymentsView.swift */, BE8117632B07E778009867B9 /* PayPalWebResultView.swift */, - BE8117652B080202009867B9 /* PayPalWebStatusView.swift */, 3B6472A62AFAEB3A004745C4 /* PayPalWebTransactionView.swift */, 3BA56FFB2A9FEFE90081D14F /* PayPalWebViewModel.swift */, ); path = PayPalWebPayments; sourceTree = ""; }; + 3BB60B512B1F9EE400A298CF /* PayPalVaultViews */ = { + isa = PBXGroup; + children = ( + 3BB60B522B1F9F1100A298CF /* PayPalVaultView.swift */, + 3B3C51132B20F7CE009125FE /* PayPalVaultResultView.swift */, + ); + path = PayPalVaultViews; + sourceTree = ""; + }; 3BCCFE472A9D962E00C5102F /* CommonComponents */ = { isa = PBXGroup; children = ( @@ -289,6 +310,7 @@ 3BC6220A2A97204E00251B85 /* CircularProgressView.swift */, 3BC622082A97198500251B85 /* LeadingText.swift */, 3BA5700A2AA13C1C0081D14F /* CoreConfigManager.swift */, + BE5898942B2B91F800AA196E /* LabelViewText.swift */, ); path = CommonComponents; sourceTree = ""; @@ -412,9 +434,11 @@ children = ( CB34B32228BE3A9A001325B9 /* PayPalViewModel.swift */, 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */, - 3B2027402A8A72050007907E /* CardVaultState.swift */, + 3BA0A58A2B1E240300330681 /* VaultViewModel.swift */, + 3B2027402A8A72050007907E /* VaultState.swift */, 3BA56FE62A9DC9D70081D14F /* CardPaymentViewModel.swift */, 3BA56FE82A9DCA520081D14F /* CardPaymentState.swift */, + 3BB60B542B1FA00C00A298CF /* PayPalVaultViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -441,6 +465,8 @@ BEDE3047275E998700D275FD /* SwiftUIComponents */ = { isa = PBXGroup; children = ( + 3B2501052B2679F000903EAB /* VaultViews */, + 3BB60B512B1F9EE400A298CF /* PayPalVaultViews */, 3BA56FFF2A9FF6630081D14F /* PayPalWebPayments */, 3BA56FEA2A9DCBB30081D14F /* CardPaymentViews */, 3BCCFE472A9D962E00C5102F /* CommonComponents */, @@ -456,7 +482,6 @@ BEDE3048275EA31800D275FD /* Extensions */ = { isa = PBXGroup; children = ( - BEDE3049275EA33500D275FD /* UIViewController+Extension.swift */, CB9ED44B283FDA900081F4DE /* PaymentButtonEnums+Extension.swift */, 3BCCFE452A9D47AC00C5102F /* CardExtensions.swift */, ); @@ -570,7 +595,7 @@ buildActionMask = 2147483647; files = ( 3BA56FE92A9DCA520081D14F /* CardPaymentState.swift in Sources */, - 3B2027412A8A72050007907E /* CardVaultState.swift in Sources */, + 3B2027412A8A72050007907E /* VaultState.swift in Sources */, 80F33CED26F8E7A9006811B1 /* Order.swift in Sources */, 3B4DD9A02A892A7000F4A716 /* CardVaultView.swift in Sources */, BE8117642B07E778009867B9 /* PayPalWebResultView.swift in Sources */, @@ -578,6 +603,7 @@ 3BC622092A97198500251B85 /* LeadingText.swift in Sources */, 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */, 3BCCFE462A9D47AC00C5102F /* CardExtensions.swift in Sources */, + BE5898952B2B91F800AA196E /* LabelViewText.swift in Sources */, CB34B32328BE3A9A001325B9 /* PayPalViewModel.swift in Sources */, 3BC622072A97115700251B85 /* RoundedBlueButtonStyle.swift in Sources */, 3B4DD9A22A8982B000F4A716 /* CardFormView.swift in Sources */, @@ -592,16 +618,17 @@ 53B9E8EA28C93B4400719239 /* OrderRequestHelpers.swift in Sources */, 3BCCFE4B2A9D985F00C5102F /* FeatureSelectionView.swift in Sources */, CB9ED44C283FDA900081F4DE /* PaymentButtonEnums+Extension.swift in Sources */, + 3BB60B552B1FA00C00A298CF /* PayPalVaultViewModel.swift in Sources */, + 3BB60B532B1F9F1100A298CF /* PayPalVaultView.swift in Sources */, 3B80D50E2A291C0800D2EAC4 /* ClientIDRequest.swift in Sources */, 3BA56FF02A9DCCFD0081D14F /* CardOrderApproveView.swift in Sources */, BE8117682B080472009867B9 /* CurrentState.swift in Sources */, 3BDB348E2A7CB02C008100D7 /* SetupTokenRequest.swift in Sources */, + 3BA0A58B2B1E240300330681 /* VaultViewModel.swift in Sources */, 80F33CF326F8EA50006811B1 /* DemoSettings.swift in Sources */, 3BA56FE72A9DC9D70081D14F /* CardPaymentViewModel.swift in Sources */, - BE8117662B080202009867B9 /* PayPalWebStatusView.swift in Sources */, 3BA5700B2AA13C1C0081D14F /* CoreConfigManager.swift in Sources */, 80E4300C2AD82C8D003CA748 /* ShippingPreference.swift in Sources */, - BEDE304A275EA33500D275FD /* UIViewController+Extension.swift in Sources */, 80F33CE826F8DE29006811B1 /* DemoMerchantAPI.swift in Sources */, 80F33CEF26F8E7CC006811B1 /* CreateOrderParams.swift in Sources */, BECD84A227036DDB007CCAE4 /* Intent.swift in Sources */, @@ -615,6 +642,7 @@ 3BA56FFC2A9FEFE90081D14F /* PayPalWebViewModel.swift in Sources */, 3BDB34922A7CB5DE008100D7 /* SetupTokenResponse.swift in Sources */, 3BA56FF22A9DCD440081D14F /* CardApprovalResultView.swift in Sources */, + 3B3C51142B20F7CE009125FE /* PayPalVaultResultView.swift in Sources */, 3BA570012AA052E80081D14F /* PayPalWebPaymentsView.swift in Sources */, CBC16DD929ED90B600307117 /* UpdateOrderParams.swift in Sources */, 3BA56FFA2A9FE4180081D14F /* CardOrderCompletionResultView.swift in Sources */, diff --git a/Demo/Demo/Extensions/UIViewController+Extension.swift b/Demo/Demo/Extensions/UIViewController+Extension.swift deleted file mode 100644 index dbc1cc9a5..000000000 --- a/Demo/Demo/Extensions/UIViewController+Extension.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit -import PaymentButtons - -extension UIViewController { - - func removeChildViews() { - children.forEach { viewController in - viewController.willMove(toParent: nil) - viewController.view.removeFromSuperview() - viewController.removeFromParent() - } - } - - func makeLabel(_ text: String) -> UILabel { - let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 15) - label.text = text - return label - } -} diff --git a/Demo/Demo/Models/PaymentTokenResponse.swift b/Demo/Demo/Models/PaymentTokenResponse.swift index 407714a6b..c0566ba82 100644 --- a/Demo/Demo/Models/PaymentTokenResponse.swift +++ b/Demo/Demo/Models/PaymentTokenResponse.swift @@ -14,12 +14,18 @@ struct Customer: Codable, Equatable { struct PaymentSource: Decodable, Equatable { - let card: BasicCard + var card: CardPaymentSource? + var paypal: PayPalPaymentSource? } -struct BasicCard: Decodable, Equatable { - +struct CardPaymentSource: Decodable, Equatable { + let brand: String? let lastDigits: String let expiry: String } + +struct PayPalPaymentSource: Decodable, Equatable { + + let emailAddress: String +} diff --git a/Demo/Demo/Models/SetupTokenRequest.swift b/Demo/Demo/Models/SetupTokenRequest.swift index 4336c9aa7..a48ff900f 100644 --- a/Demo/Demo/Models/SetupTokenRequest.swift +++ b/Demo/Demo/Models/SetupTokenRequest.swift @@ -1,9 +1,71 @@ import Foundation -struct SetUpTokenRequest { - +struct VaultExperienceContext: Encodable { + + let returnUrl = "sdk.ios.paypal://vault/success" + let cancelUrl = "sdk.ios.paypal://vault/cancel" + + enum CodingKeys: String, CodingKey { + case returnUrl = "return_url" + case cancelUrl = "cancel_url" + } +} + +struct PayPal: Encodable { + + var usageType: String + let experienceContext = VaultExperienceContext() + + enum CodingKeys: String, CodingKey { + case usageType = "usage_type" + case experienceContext = "experience_context" + } +} + +enum PaymentSourceType: Encodable { + case card + case paypal(usageType: String) + + private enum CodingKeys: String, CodingKey { + case card, paypal + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .card: + try container.encode(EmptyBodyParams(), forKey: .card) + case .paypal(let usageType): + try container.encode(PayPal(usageType: usageType), forKey: .paypal) + } + } +} + +struct VaultCustomer: Encodable { + + var id: String? + + private enum CodingKeys: String, CodingKey { + case id + } +} + +struct SetupTokenRequestBody: Encodable { + + var customer: VaultCustomer? + let paymentSource: PaymentSourceType + + enum CodingKeys: String, CodingKey { + case paymentSource = "payment_source" + case customer + } +} + +struct SetUpTokenRequest: Encodable { + let customerID: String? - + let paymentSource: PaymentSourceType + var path: String { "/setup_tokens/" } @@ -15,17 +77,9 @@ struct SetUpTokenRequest { var headers: [String: String] { ["Content-Type": "application/json"] } - + var body: Data? { - let requestBody: [String: Any] = [ - "customer": [ - "id": customerID - ], - "payment_source": [ - "card": [:] - ] - ] - - return try? JSONSerialization.data(withJSONObject: requestBody) + let requestBodyParam = SetupTokenRequestBody(customer: VaultCustomer(id: customerID), paymentSource: paymentSource) + return try? JSONEncoder().encode(requestBodyParam) } } diff --git a/Demo/Demo/Models/SetupTokenResponse.swift b/Demo/Demo/Models/SetupTokenResponse.swift index f79552b4d..77c3dd2f7 100644 --- a/Demo/Demo/Models/SetupTokenResponse.swift +++ b/Demo/Demo/Models/SetupTokenResponse.swift @@ -8,9 +8,18 @@ struct SetUpTokenResponse: Decodable, Equatable { let id, status: String let customer: Customer? + let links: [Link] + var paypalURL: String? { + links.first { $0.rel == "approve" }?.href + } struct Customer: Decodable { let id: String } + + struct Link: Decodable { + + let href, rel, method: String + } } diff --git a/Demo/Demo/Networking/DemoMerchantAPI.swift b/Demo/Demo/Networking/DemoMerchantAPI.swift index cc0a9eff9..4ac8bc837 100644 --- a/Demo/Demo/Networking/DemoMerchantAPI.swift +++ b/Demo/Demo/Networking/DemoMerchantAPI.swift @@ -17,13 +17,17 @@ final class DemoMerchantAPI { private init() {} // MARK: Public Methods - - func getSetupToken(customerID: String? = nil, selectedMerchantIntegration: MerchantIntegration) async throws -> SetUpTokenResponse { + + func getSetupToken( + customerID: String? = nil, + selectedMerchantIntegration: MerchantIntegration, + paymentSourceType: PaymentSourceType + ) async throws -> SetUpTokenResponse { do { // TODO: pass in headers depending on integration type // Different request struct or integration type property // in SetUpTokenRequest to conditionally add header - let request = SetUpTokenRequest(customerID: customerID) + let request = SetUpTokenRequest(customerID: customerID, paymentSource: paymentSourceType) let urlRequest = try createSetupTokenUrlRequest( setupTokenRequest: request, environment: DemoSettings.environment, diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift index 50cadd5d7..67c800531 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift @@ -12,21 +12,22 @@ struct CardVaultView: View { VStack(spacing: 16) { CreateSetupTokenView( selectedMerchantIntegration: DemoSettings.merchantIntegration, - cardVaultViewModel: cardVaultViewModel + vaultViewModel: cardVaultViewModel, + paymentSourceType: PaymentSourceType.card ) - SetupTokenResultView(cardVaultViewModel: cardVaultViewModel) + SetupTokenResultView(vaultViewModel: cardVaultViewModel) if let setupToken = cardVaultViewModel.state.setupToken { UpdateSetupTokenView(cardVaultViewModel: cardVaultViewModel, setupToken: setupToken.id) } UpdateSetupTokenResultView(cardVaultViewModel: cardVaultViewModel) if let updateSetupToken = cardVaultViewModel.state.updateSetupToken { CreatePaymentTokenView( - cardVaultViewModel: cardVaultViewModel, + vaultViewModel: cardVaultViewModel, selectedMerchantIntegration: DemoSettings.merchantIntegration, setupToken: updateSetupToken.id ) } - PaymentTokenResultView(cardVaultViewModel: cardVaultViewModel) + PaymentTokenResultView(vaultViewModel: cardVaultViewModel) switch cardVaultViewModel.state.paymentTokenResponse { case .loaded, .error: VStack { diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift index b0eb571d5..db0c78eea 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift @@ -15,7 +15,7 @@ struct UpdateSetupTokenResultView: View { } } - func getSuccessView(updateSetupTokenResponse: CardVaultState.UpdateSetupTokenResult) -> some View { + func getSuccessView(updateSetupTokenResponse: UpdateSetupTokenResult) -> some View { VStack(spacing: 16) { HStack { Text("Vault Success") diff --git a/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift b/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift new file mode 100644 index 000000000..a1f63367c --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct LabelViewText: View { + + var titleText: String + var bodyText: String + + var body: some View { + HStack { + Text(titleText).fontWeight(.bold) + Text(bodyText) + } + } + + init(_ titleText: String, bodyText: String) { + self.titleText = titleText + self.bodyText = bodyText + } +} diff --git a/Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift b/Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift index 76b28afc9..7abd2b72f 100644 --- a/Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift +++ b/Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift @@ -43,6 +43,12 @@ struct FeatureSelectionView: View { } label: { Text("PayPal Web") } + NavigationLink { + PayPalVaultView() + .navigationTitle("PayPal Vaulting") + } label: { + Text("PayPal Vaulting") + } NavigationLink { SwiftUINativeCheckoutDemo() } label: { diff --git a/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultResultView.swift b/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultResultView.swift new file mode 100644 index 000000000..564fcb519 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultResultView.swift @@ -0,0 +1,41 @@ +import SwiftUI +import PayPalWebPayments + +struct PayPalVaultResultView: View { + + @ObservedObject var viewModel: PayPalVaultViewModel + + var body: some View { + switch viewModel.state.paypalVaultTokenResponse { + case .idle, .loading: + EmptyView() + case .loaded(let vaultResult): + getSuccessView(result: vaultResult) + case .error(let errorMessage): + ErrorView(errorMessage: errorMessage) + } + } + + func getSuccessView(result: PayPalVaultResult) -> some View { + VStack(spacing: 16) { + HStack { + Text("Vault Success") + .font(.system(size: 20)) + Spacer() + } + LeadingText("ID", weight: .bold) + LeadingText("\(result.tokenID)") + LeadingText("Status", weight: .bold) + LeadingText("APPROVED") + LeadingText("Approval Session ID", weight: .bold) + LeadingText("\(result.approvalSessionID)") + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultView.swift b/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultView.swift new file mode 100644 index 000000000..863d52694 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/PayPalVaultViews/PayPalVaultView.swift @@ -0,0 +1,64 @@ +import SwiftUI + +struct PayPalVaultView: View { + + @StateObject var paypalVaultViewModel = PayPalVaultViewModel() + + var body: some View { + ScrollView { + ScrollViewReader { scrollView in + VStack(spacing: 16) { + CreateSetupTokenView( + selectedMerchantIntegration: DemoSettings.merchantIntegration, + vaultViewModel: paypalVaultViewModel, + paymentSourceType: PaymentSourceType.paypal(usageType: "MERCHANT") + ) + SetupTokenResultView(vaultViewModel: paypalVaultViewModel) + if let url = paypalVaultViewModel.state.setupToken?.paypalURL { + Button("Vault PayPal") { + Task { + await paypalVaultViewModel.vault(url: url) + } + } + .buttonStyle(RoundedBlueButtonStyle()) + .padding() + } + PayPalVaultResultView(viewModel: paypalVaultViewModel) + if let paypalVaultResult = paypalVaultViewModel.state.paypalVaultToken { + CreatePaymentTokenView( + vaultViewModel: paypalVaultViewModel, + selectedMerchantIntegration: DemoSettings.merchantIntegration, + setupToken: paypalVaultResult.tokenID + ) + } + PaymentTokenResultView(vaultViewModel: paypalVaultViewModel) + switch paypalVaultViewModel.state.paymentTokenResponse { + case .loaded, .error: + VStack { + Button("Reset") { + paypalVaultViewModel.resetState() + } + .foregroundColor(.black) + .padding() + .frame(maxWidth: .infinity) + .background(.gray) + .cornerRadius(10) + } + .padding(5) + default: + EmptyView() + } + Text("") + .id("bottomView") + .frame(maxWidth: .infinity, alignment: .top) + .padding(.horizontal, 10) + .onChange(of: paypalVaultViewModel.state) { _ in + withAnimation { + scrollView.scrollTo("bottomView") + } + } + } + } + } + } +} diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift index 35f48af36..1d8456713 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift @@ -1,10 +1,13 @@ import SwiftUI import PaymentButtons +import PayPalWebPayments struct PayPalWebButtonsView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel + @State private var selectedFundingSource: PayPalWebCheckoutFundingSource = .paypal + var body: some View { VStack { VStack(alignment: .center, spacing: 40) { @@ -14,36 +17,43 @@ struct PayPalWebButtonsView: View { .frame(maxWidth: .infinity, maxHeight: 40) PayPalCreditButton.Representable(color: .gold, edges: .softEdges, size: .expanded) { payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit) + VStack(alignment: .center, spacing: 16) { + HStack { + Text("Checkout with PayPal") + .font(.system(size: 20)) + Spacer() } - .frame(maxWidth: .infinity, maxHeight: 40) - PayPalPayLaterButton.Representable(color: .silver, edges: .rounded, size: .full) { - payPalWebViewModel.paymentButtonTapped(funding: .paylater) + .frame(maxWidth: .infinity) + .font(.headline) + Picker("Funding Source", selection: $selectedFundingSource) { + Text("PayPal").tag(PayPalWebCheckoutFundingSource.paypal) + Text("PayPal Credit").tag(PayPalWebCheckoutFundingSource.paylater) + Text("Pay Later").tag(PayPalWebCheckoutFundingSource.paypalCredit) + } + .pickerStyle(SegmentedPickerStyle()) + + switch selectedFundingSource { + case .paypalCredit: + PayPalCreditButton.Representable(color: .black, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit) + } + case .paylater: + PayPalPayLaterButton.Representable(color: .silver, edges: .softEdges, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paylater) + } + case .paypal: + PayPalButton.Representable(color: .blue, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paypal) + } } - .frame(maxWidth: .infinity, maxHeight: 40) } + .frame(height: 150) .padding(20) - .padding() .background( RoundedRectangle(cornerRadius: 10) .stroke(.gray, lineWidth: 2) .padding(5) ) - - if payPalWebViewModel.checkoutResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .approved) - NavigationLink { - PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel) - .navigationTitle("Complete Transaction") - } label: { - Text("Complete Transaction") - } - .navigationViewStyle(StackNavigationViewStyle()) - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) - } - Spacer() } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift index 0af1754d6..ba995324d 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift @@ -13,6 +13,9 @@ struct PayPalWebCreateOrderView: View { Text("Create an Order") .font(.system(size: 20)) Spacer() + Button("Reset") { + payPalWebViewModel.resetState() + } } .frame(maxWidth: .infinity) .font(.headline) @@ -37,7 +40,7 @@ struct PayPalWebCreateOrderView: View { } } .buttonStyle(RoundedBlueButtonStyle()) - if payPalWebViewModel.state == .loading { + if payPalWebViewModel.state == .loading && payPalWebViewModel.checkoutResult == nil { CircularProgressView() } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift index 88a03e406..3b47ac991 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift @@ -6,20 +6,31 @@ struct PayPalWebPaymentsView: View { var body: some View { ScrollView { - VStack(spacing: 16) { - PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel) - if payPalWebViewModel.createOrderResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .created) - NavigationLink { + ScrollViewReader { scrollView in + VStack(spacing: 16) { + PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel) + + if payPalWebViewModel.orderID != nil { PayPalWebButtonsView(payPalWebViewModel: payPalWebViewModel) - .navigationTitle("Checkout with PayPal") - } label: { - Text("Checkout with PayPal") } - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) + + PayPalWebResultView(payPalWebViewModel: payPalWebViewModel) + + if payPalWebViewModel.checkoutResult != nil { + PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel) + .padding(.bottom, 20) + .id("bottomView") + .onAppear { + withAnimation { + scrollView.scrollTo("bottomView") + } + } + } + } + .onChange(of: payPalWebViewModel.state) { _ in + withAnimation { + scrollView.scrollTo("bottomView") + } } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift index 85cd823c0..bb3bfa5ac 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift @@ -1,26 +1,57 @@ import SwiftUI -enum OrderStatus { - case created - case approved - case completed - case error -} - struct PayPalWebResultView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel - var status: OrderStatus - var body: some View { switch payPalWebViewModel.state { case .idle, .loading: EmptyView() case .success: - PayPalWebStatusView(status: status, payPalWebViewModel: payPalWebViewModel) + successView case .error(let errorMessage): ErrorView(errorMessage: errorMessage) } } + + var successView: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Order Details") + .font(.system(size: 20)) + Spacer() + } + if let orderID = payPalWebViewModel.orderID { + LabelViewText("Order ID:", bodyText: orderID) + } + + if let status = payPalWebViewModel.order?.status { + LabelViewText("Status:", bodyText: status) + } + + if let payerID = payPalWebViewModel.checkoutResult?.payerID { + LabelViewText("Payer ID:", bodyText: payerID) + } + + if let emailAddress = payPalWebViewModel.order?.paymentSource?.paypal?.emailAddress { + LabelViewText("Email:", bodyText: emailAddress) + } + + if let vaultID = payPalWebViewModel.order?.paymentSource?.paypal?.attributes?.vault.id { + LabelViewText("Payment Token:", bodyText: vaultID) + } + + if let customerID = payPalWebViewModel.order?.paymentSource?.paypal?.attributes?.vault.customer.id { + LabelViewText("Customer ID:", bodyText: customerID) + } + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(.gray, lineWidth: 2) + .padding(5) + ) + } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift deleted file mode 100644 index 2ec377e39..000000000 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift +++ /dev/null @@ -1,76 +0,0 @@ -import SwiftUI - -struct PayPalWebStatusView: View { - - var status: OrderStatus - var payPalWebViewModel: PayPalWebViewModel - - var body: some View { - VStack(spacing: 16) { - switch status { - case .created: - HStack { - Text("Order Created") - .font(.system(size: 20)) - Spacer() - } - if let order = payPalWebViewModel.createOrderResult { - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Status", weight: .bold) - LeadingText("\(order.status)") - } - case .approved: - HStack { - Text("Order Approved") - .font(.system(size: 20)) - Spacer() - } - if let order = payPalWebViewModel.createOrderResult { - LeadingText("Intent", weight: .bold) - LeadingText("\(payPalWebViewModel.intent)") - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Payer ID", weight: .bold) - LeadingText("\(payPalWebViewModel.checkoutResult?.payerID ?? "")") - } - case .completed: - if let order = payPalWebViewModel.transactionResult { - HStack { - Text("Order \(payPalWebViewModel.intent.rawValue.capitalized)d") - .font(.system(size: 20)) - Spacer() - } - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Status", weight: .bold) - LeadingText("\(order.status)") - - if let emailAddress = order.paymentSource?.paypal?.emailAddress { - LeadingText("Email", weight: .bold) - LeadingText("\(emailAddress)") - } - - if let vaultID = order.paymentSource?.paypal?.attributes?.vault.id { - LeadingText("Vault ID / Payment Token", weight: .bold) - LeadingText("\(vaultID)") - } - - if let customerID = order.paymentSource?.paypal?.attributes?.vault.customer.id { - LeadingText("Customer ID", weight: .bold) - LeadingText("\(customerID)") - } - } - default: - Text("") - } - } - .frame(maxWidth: .infinity) - .padding() - .background( - RoundedRectangle(cornerRadius: 10) - .stroke(.gray, lineWidth: 2) - .padding(5) - ) - } -} diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift index 88b075434..e8b0a132e 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift @@ -5,41 +5,23 @@ struct PayPalWebTransactionView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel var body: some View { - ScrollView { - ScrollViewReader { scrollView in - VStack { - PayPalWebStatusView(status: .approved, payPalWebViewModel: payPalWebViewModel) - ZStack { - Button("\(payPalWebViewModel.intent.rawValue.capitalized) Order") { - Task { - do { - try await payPalWebViewModel.completeTransaction() - } catch { - print("Error capturing order: \(error.localizedDescription)") - } - } + VStack { + ZStack { + Button("\(payPalWebViewModel.intent.rawValue.capitalized) Order") { + Task { + do { + try await payPalWebViewModel.completeTransaction() + } catch { + print("Error capturing order: \(error.localizedDescription)") } - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - - if payPalWebViewModel.state == .loading { - CircularProgressView() - } - } - - if payPalWebViewModel.transactionResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .completed) - .id("bottomView") - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) } } - .onChange(of: payPalWebViewModel.transactionResult) { _ in - withAnimation { - scrollView.scrollTo("bottomView") - } + .buttonStyle(RoundedBlueButtonStyle()) + .padding() + + if payPalWebViewModel.state == .loading { + CircularProgressView() } - Spacer() } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift index 4b2f4b0be..224c435ba 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift @@ -6,12 +6,14 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { @Published var state: CurrentState = .idle @Published var intent: Intent = .authorize - @Published var createOrderResult: Order? - @Published var transactionResult: Order? + @Published var order: Order? @Published var checkoutResult: PayPalWebCheckoutResult? var payPalWebCheckoutClient: PayPalWebCheckoutClient? - var orderID: String? + + var orderID: String? { + order?.id + } let configManager = CoreConfigManager(domain: "PayPalWeb Payments") @@ -46,11 +48,7 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { selectedMerchantIntegration: DemoSettings.merchantIntegration ) - self.orderID = order.id - - DispatchQueue.main.async { - self.createOrderResult = order - } + updateOrder(order) updateState(.success) print("✅ fetched orderID: \(order.id) with status: \(order.status)") } catch { @@ -73,18 +71,23 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { let payPalRequest = PayPalWebCheckoutRequest(orderID: orderID, fundingSource: funding) payPalWebCheckoutClient.start(request: payPalRequest) } + updateState(.success) } catch { print("Error starting PayPalWebCheckoutClient") - state = .error(message: error.localizedDescription) + updateState(.error(message: error.localizedDescription)) } } } - func getPayPalClient() async throws -> PayPalWebCheckoutClient { + func getPayPalClient() async throws -> PayPalWebCheckoutClient? { do { let config = try await configManager.getCoreConfig() let payPalClient = PayPalWebCheckoutClient(config: config) return payPalClient + } catch { + updateState(.error(message: error.localizedDescription)) + print("❌ failed to create PayPalWebCheckoutClient with error: \(error.localizedDescription)") + return nil } } @@ -94,9 +97,7 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { if let orderID { let order = try await DemoMerchantAPI.sharedService.completeOrder(intent: intent, orderID: orderID) - DispatchQueue.main.async { - self.transactionResult = order - } + updateOrder(order) updateState(.success) } } catch { @@ -105,6 +106,18 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { } } + func resetState() { + updateState(.idle) + order = nil + checkoutResult = nil + } + + private func updateOrder(_ order: Order) { + DispatchQueue.main.async { + self.order = order + } + } + private func updateState(_ state: CurrentState) { DispatchQueue.main.async { self.state = state diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift b/Demo/Demo/SwiftUIComponents/VaultViews/CreatePaymentTokenView.swift similarity index 71% rename from Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift rename to Demo/Demo/SwiftUIComponents/VaultViews/CreatePaymentTokenView.swift index 68a0eb736..ded38c87a 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift +++ b/Demo/Demo/SwiftUIComponents/VaultViews/CreatePaymentTokenView.swift @@ -5,10 +5,10 @@ struct CreatePaymentTokenView: View { let selectedMerchantIntegration: MerchantIntegration let setupToken: String - @ObservedObject var cardVaultViewModel: CardVaultViewModel + @ObservedObject var vaultViewModel: VaultViewModel - public init(cardVaultViewModel: CardVaultViewModel, selectedMerchantIntegration: MerchantIntegration, setupToken: String) { - self.cardVaultViewModel = cardVaultViewModel + public init(vaultViewModel: VaultViewModel, selectedMerchantIntegration: MerchantIntegration, setupToken: String) { + self.vaultViewModel = vaultViewModel self.selectedMerchantIntegration = selectedMerchantIntegration self.setupToken = setupToken } @@ -26,17 +26,17 @@ struct CreatePaymentTokenView: View { Button("Create Payment Token") { Task { do { - try await cardVaultViewModel.getPaymentToken( + try await vaultViewModel.getPaymentToken( setupToken: setupToken, selectedMerchantIntegration: selectedMerchantIntegration ) } catch { - print("Error in getting setup token. \(error.localizedDescription)") + print("Error in getting payment token. \(error.localizedDescription)") } } } .buttonStyle(RoundedBlueButtonStyle()) - if case .loading = cardVaultViewModel.state.paymentTokenResponse { + if case .loading = vaultViewModel.state.paymentTokenResponse { CircularProgressView() } } diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift b/Demo/Demo/SwiftUIComponents/VaultViews/CreateSetupTokenView.swift similarity index 75% rename from Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift rename to Demo/Demo/SwiftUIComponents/VaultViews/CreateSetupTokenView.swift index a3189e0da..94b86c97d 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift +++ b/Demo/Demo/SwiftUIComponents/VaultViews/CreateSetupTokenView.swift @@ -5,11 +5,14 @@ struct CreateSetupTokenView: View { let selectedMerchantIntegration: MerchantIntegration @State private var vaultCustomerID: String = "" - @ObservedObject var cardVaultViewModel: CardVaultViewModel + @ObservedObject var vaultViewModel: VaultViewModel - public init(selectedMerchantIntegration: MerchantIntegration, cardVaultViewModel: CardVaultViewModel) { + let paymentSourceType: PaymentSourceType + + public init(selectedMerchantIntegration: MerchantIntegration, vaultViewModel: VaultViewModel, paymentSourceType: PaymentSourceType) { self.selectedMerchantIntegration = selectedMerchantIntegration - self.cardVaultViewModel = cardVaultViewModel + self.vaultViewModel = vaultViewModel + self.paymentSourceType = paymentSourceType } var body: some View { @@ -26,9 +29,10 @@ struct CreateSetupTokenView: View { Button("Create Setup Token") { Task { do { - try await cardVaultViewModel.getSetupToken( + try await vaultViewModel.getSetupToken( customerID: vaultCustomerID.isEmpty ? nil : vaultCustomerID, - selectedMerchantIntegration: selectedMerchantIntegration + selectedMerchantIntegration: selectedMerchantIntegration, + paymentSourceType: paymentSourceType ) } catch { print("Error in getting setup token. \(error.localizedDescription)") @@ -36,7 +40,7 @@ struct CreateSetupTokenView: View { } } .buttonStyle(RoundedBlueButtonStyle()) - if case .loading = cardVaultViewModel.state.setupTokenResponse { + if case .loading = vaultViewModel.state.setupTokenResponse { CircularProgressView() } } diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift b/Demo/Demo/SwiftUIComponents/VaultViews/PaymentTokenResultView.swift similarity index 63% rename from Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift rename to Demo/Demo/SwiftUIComponents/VaultViews/PaymentTokenResultView.swift index 705a63ab7..24c7e1bfc 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift +++ b/Demo/Demo/SwiftUIComponents/VaultViews/PaymentTokenResultView.swift @@ -2,10 +2,10 @@ import SwiftUI struct PaymentTokenResultView: View { - @ObservedObject var cardVaultViewModel: CardVaultViewModel + @ObservedObject var vaultViewModel: VaultViewModel var body: some View { - switch cardVaultViewModel.state.paymentTokenResponse { + switch vaultViewModel.state.paymentTokenResponse { case .idle, .loading: EmptyView() case .loaded(let paymentTokenResponse): @@ -26,10 +26,15 @@ struct PaymentTokenResultView: View { LeadingText("\(paymentTokenResponse.id)") LeadingText("Customer ID", weight: .bold) LeadingText("\(paymentTokenResponse.customer.id)") - LeadingText("Card Brand", weight: .bold) - LeadingText("\(paymentTokenResponse.paymentSource.card.brand ?? "")") - LeadingText("Card Last 4", weight: .bold) - LeadingText("\(paymentTokenResponse.paymentSource.card.lastDigits)") + if let card = paymentTokenResponse.paymentSource.card { + LeadingText("Card Brand", weight: .bold) + LeadingText("\(card.brand ?? "")") + LeadingText("Card Last 4", weight: .bold) + LeadingText("\(card.lastDigits)") + } else if let paypal = paymentTokenResponse.paymentSource.paypal { + LeadingText("Email", weight: .bold) + LeadingText("\(paypal.emailAddress)") + } } .frame(maxWidth: .infinity) .padding() diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift b/Demo/Demo/SwiftUIComponents/VaultViews/SetupTokenResultView.swift similarity index 80% rename from Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift rename to Demo/Demo/SwiftUIComponents/VaultViews/SetupTokenResultView.swift index c7db50e8a..6659d52d4 100644 --- a/Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift +++ b/Demo/Demo/SwiftUIComponents/VaultViews/SetupTokenResultView.swift @@ -2,10 +2,10 @@ import SwiftUI struct SetupTokenResultView: View { - @ObservedObject var cardVaultViewModel: CardVaultViewModel + @ObservedObject var vaultViewModel: VaultViewModel var body: some View { - switch cardVaultViewModel.state.setupTokenResponse { + switch vaultViewModel.state.setupTokenResponse { case .idle, .loading: EmptyView() case .loaded(let setupTokenResponse): @@ -28,6 +28,10 @@ struct SetupTokenResultView: View { LeadingText("\(setupTokenResponse.customer?.id ?? "")") LeadingText("Status", weight: .bold) LeadingText("\(setupTokenResponse.status)") + if let url = setupTokenResponse.paypalURL { + LeadingText("PayPalURL", weight: .bold) + LeadingText("\(url)") + } } .frame(maxWidth: .infinity) .padding() diff --git a/Demo/Demo/ViewModels/CardVaultViewModel.swift b/Demo/Demo/ViewModels/CardVaultViewModel.swift index a08022369..d00cba736 100644 --- a/Demo/Demo/ViewModels/CardVaultViewModel.swift +++ b/Demo/Demo/ViewModels/CardVaultViewModel.swift @@ -1,63 +1,11 @@ -import UIKit +import Foundation import CardPayments import CorePayments -class CardVaultViewModel: ObservableObject, CardVaultDelegate { - - @Published var state = CardVaultState() +class CardVaultViewModel: VaultViewModel, CardVaultDelegate { let configManager = CoreConfigManager(domain: "Card Vault") - func getSetupToken( - customerID: String? = nil, - selectedMerchantIntegration: MerchantIntegration - ) async throws { - do { - DispatchQueue.main.async { - self.state.setupTokenResponse = .loading - } - let setupTokenResult = try await DemoMerchantAPI.sharedService.getSetupToken( - customerID: customerID, - selectedMerchantIntegration: selectedMerchantIntegration - ) - DispatchQueue.main.async { - self.state.setupTokenResponse = .loaded(setupTokenResult) - } - } catch { - DispatchQueue.main.async { - self.state.setupTokenResponse = .error(message: error.localizedDescription) - } - throw error - } - } - - func resetState() { - state = CardVaultState() - } - - func getPaymentToken( - setupToken: String, - selectedMerchantIntegration: MerchantIntegration - ) async throws { - do { - DispatchQueue.main.async { - self.state.paymentTokenResponse = .loading - } - let paymentTokenResult = try await DemoMerchantAPI.sharedService.getPaymentToken( - setupToken: setupToken, - selectedMerchantIntegration: selectedMerchantIntegration - ) - DispatchQueue.main.async { - self.state.paymentTokenResponse = .loaded(paymentTokenResult) - } - } catch { - DispatchQueue.main.async { - self.state.paymentTokenResponse = .error(message: error.localizedDescription) - } - throw error - } - } - func vault(card: Card, setupToken: String) async { DispatchQueue.main.async { self.state.updateSetupTokenResponse = .loading @@ -69,7 +17,7 @@ class CardVaultViewModel: ObservableObject, CardVaultDelegate { let cardVaultRequest = CardVaultRequest(card: card, setupTokenID: setupToken) cardClient.vault(cardVaultRequest) } catch { - state.updateSetupTokenResponse = .error(message: error.localizedDescription) + self.state.updateSetupTokenResponse = .error(message: error.localizedDescription) print("failed in updating setup token. \(error.localizedDescription)") } } @@ -86,7 +34,7 @@ class CardVaultViewModel: ObservableObject, CardVaultDelegate { func setUpTokenSuccessResult(vaultResult: CardPayments.CardVaultResult) { DispatchQueue.main.async { self.state.updateSetupTokenResponse = .loaded( - CardVaultState.UpdateSetupTokenResult(id: vaultResult.setupTokenID, status: vaultResult.status) + UpdateSetupTokenResult(id: vaultResult.setupTokenID, status: vaultResult.status) ) } } diff --git a/Demo/Demo/ViewModels/PayPalVaultViewModel.swift b/Demo/Demo/ViewModels/PayPalVaultViewModel.swift new file mode 100644 index 000000000..ebe0e8237 --- /dev/null +++ b/Demo/Demo/ViewModels/PayPalVaultViewModel.swift @@ -0,0 +1,50 @@ +import UIKit +import PayPalWebPayments +import CorePayments + +class PayPalVaultViewModel: VaultViewModel, PayPalVaultDelegate { + + let configManager = CoreConfigManager(domain: "PayPal Vault") + + func vault(url: String) async { + guard let paypalUrl = URL(string: url) else { + return + } + DispatchQueue.main.async { + self.state.paypalVaultTokenResponse = .loading + } + do { + let config = try await configManager.getCoreConfig() + let paypalClient = PayPalWebCheckoutClient(config: config) + paypalClient.vaultDelegate = self + paypalClient.vault(url: paypalUrl) + } catch { + print("Error in vaulting PayPal Payment") + } + } + + // MARK: - PayPalVault Delegate + + func paypal( + _ paypalWebClient: PayPalWebPayments.PayPalWebCheckoutClient, + didFinishWithVaultResult paypalVaultResult: PayPalVaultResult + ) { + DispatchQueue.main.async { + self.state.paypalVaultTokenResponse = .loaded(paypalVaultResult) + } + } + + func paypal( + _ paypalWebClient: PayPalWebPayments.PayPalWebCheckoutClient, + didFinishWithVaultError vaultError: CorePayments.CoreSDKError + ) { + + DispatchQueue.main.async { + self.state.paypalVaultTokenResponse = .error(message: vaultError.localizedDescription) + } + } + + func paypalDidCancel(_ payPalWebClient: PayPalWebCheckoutClient) { + print("PayPal Checkout Canceled") + } +} diff --git a/Demo/Demo/ViewModels/PayPalViewModel.swift b/Demo/Demo/ViewModels/PayPalViewModel.swift index 29242b8b4..1b16f03ef 100644 --- a/Demo/Demo/ViewModels/PayPalViewModel.swift +++ b/Demo/Demo/ViewModels/PayPalViewModel.swift @@ -1,4 +1,4 @@ -import UIKit +import Foundation import PayPalNativePayments import CorePayments diff --git a/Demo/Demo/ViewModels/CardVaultState.swift b/Demo/Demo/ViewModels/VaultState.swift similarity index 64% rename from Demo/Demo/ViewModels/CardVaultState.swift rename to Demo/Demo/ViewModels/VaultState.swift index 21bb946a5..933ea725f 100644 --- a/Demo/Demo/ViewModels/CardVaultState.swift +++ b/Demo/Demo/ViewModels/VaultState.swift @@ -1,17 +1,21 @@ import Foundation import CardPayments +import PayPalWebPayments -struct CardVaultState: Equatable { +struct UpdateSetupTokenResult: Decodable, Equatable { - struct UpdateSetupTokenResult: Decodable, Equatable { + var id: String + var status: String +} - var id: String - var status: String - } +struct VaultState: Equatable { var setupToken: SetUpTokenResponse? - var updateSetupToken: UpdateSetupTokenResult? var paymentToken: PaymentTokenResponse? + // result for card vault + var updateSetupToken: UpdateSetupTokenResult? + // result for paypal vault + var paypalVaultToken: PayPalVaultResult? var setupTokenResponse: LoadingState = .idle { didSet { @@ -21,6 +25,15 @@ struct CardVaultState: Equatable { } } + var paymentTokenResponse: LoadingState = .idle { + didSet { + if case .loaded(let value) = paymentTokenResponse { + paymentToken = value + } + } + } + + // response from Card Vault var updateSetupTokenResponse: LoadingState = .idle { didSet { if case .loaded(let value) = updateSetupTokenResponse { @@ -28,11 +41,12 @@ struct CardVaultState: Equatable { } } } - - var paymentTokenResponse: LoadingState = .idle { + + // response from PayPal Vault + var paypalVaultTokenResponse: LoadingState = .idle { didSet { - if case .loaded(let value) = paymentTokenResponse { - paymentToken = value + if case .loaded(let value) = paypalVaultTokenResponse { + paypalVaultToken = value } } } diff --git a/Demo/Demo/ViewModels/VaultViewModel.swift b/Demo/Demo/ViewModels/VaultViewModel.swift new file mode 100644 index 000000000..bd17796e5 --- /dev/null +++ b/Demo/Demo/ViewModels/VaultViewModel.swift @@ -0,0 +1,58 @@ +import SwiftUI + +class VaultViewModel: ObservableObject { + + @Published var state = VaultState() + + func getSetupToken( + customerID: String? = nil, + selectedMerchantIntegration: MerchantIntegration, + paymentSourceType: PaymentSourceType + ) async throws { + do { + DispatchQueue.main.async { + self.state.setupTokenResponse = .loading + } + let setupTokenResult = try await DemoMerchantAPI.sharedService.getSetupToken( + customerID: customerID, + selectedMerchantIntegration: selectedMerchantIntegration, + paymentSourceType: paymentSourceType + ) + DispatchQueue.main.async { + self.state.setupTokenResponse = .loaded(setupTokenResult) + } + } catch { + DispatchQueue.main.async { + self.state.setupTokenResponse = .error(message: error.localizedDescription) + } + throw error + } + } + + func resetState() { + state = VaultState() + } + + func getPaymentToken( + setupToken: String, + selectedMerchantIntegration: MerchantIntegration + ) async throws { + do { + DispatchQueue.main.async { + self.state.paymentTokenResponse = .loading + } + let paymentTokenResult = try await DemoMerchantAPI.sharedService.getPaymentToken( + setupToken: setupToken, + selectedMerchantIntegration: selectedMerchantIntegration + ) + DispatchQueue.main.async { + self.state.paymentTokenResponse = .loaded(paymentTokenResult) + } + } catch { + DispatchQueue.main.async { + self.state.paymentTokenResponse = .error(message: error.localizedDescription) + } + throw error + } + } +} diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index b083c1a6b..0c85a45f8 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 3B109B3C2A85CC6200D8135F /* MockCardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */; }; 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */; }; 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */; }; + 3B3C51122B20C962009125FE /* PayPalVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3C51112B20C962009125FE /* PayPalVaultDelegate.swift */; }; + 3B3C51182B2221C9009125FE /* MockPayPalVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3C51172B2221C9009125FE /* MockPayPalVaultDelegate.swift */; }; + 3B3C511E2B2395B5009125FE /* PayPalVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3C511D2B2395B5009125FE /* PayPalVaultResult.swift */; }; 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; }; 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; }; @@ -213,6 +216,9 @@ 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultResult.swift; sourceTree = ""; }; + 3B3C51112B20C962009125FE /* PayPalVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultDelegate.swift; sourceTree = ""; }; + 3B3C51172B2221C9009125FE /* MockPayPalVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPayPalVaultDelegate.swift; sourceTree = ""; }; + 3B3C511D2B2395B5009125FE /* PayPalVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultResult.swift; sourceTree = ""; }; 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateVaultVariables.swift; sourceTree = ""; }; 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailingJSONEncoder.swift; sourceTree = ""; }; 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResponse.swift; sourceTree = ""; }; @@ -642,6 +648,7 @@ isa = PBXGroup; children = ( BCE8A7F227EA531C00AC301B /* MockPayPalWebDelegate.swift */, + 3B3C51172B2221C9009125FE /* MockPayPalVaultDelegate.swift */, ); path = Mocks; sourceTree = ""; @@ -716,6 +723,8 @@ BE4F784527EB629100FF4C0E /* PayPalWebCheckoutRequest.swift */, BE4F784727EB629100FF4C0E /* PayPalWebCheckoutResult.swift */, CB51FF7127FCB947001A97F5 /* PayPalWebCheckoutFundingSource.swift */, + 3B3C51112B20C962009125FE /* PayPalVaultDelegate.swift */, + 3B3C511D2B2395B5009125FE /* PayPalVaultResult.swift */, ); name = PayPalWebPayments; path = Sources/PayPalWebPayments; @@ -1401,8 +1410,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3B3C511E2B2395B5009125FE /* PayPalVaultResult.swift in Sources */, BE4F785327EB656400FF4C0E /* Environment+PayPalWebCheckout.swift in Sources */, BE4F785427EB656400FF4C0E /* PayPalWebCheckoutClient.swift in Sources */, + 3B3C51122B20C962009125FE /* PayPalVaultDelegate.swift in Sources */, BE4F785527EB656400FF4C0E /* PayPalWebCheckoutClientError.swift in Sources */, BE4F785627EB656400FF4C0E /* PayPalWebCheckoutDelegate.swift in Sources */, BE4F785727EB656400FF4C0E /* PayPalWebCheckoutRequest.swift in Sources */, @@ -1415,6 +1426,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 3B3C51182B2221C9009125FE /* MockPayPalVaultDelegate.swift in Sources */, BCE8A7FB27EA5B1D00AC301B /* Environment+PayPalWebCheckout_Tests.swift in Sources */, BCE8A7F927EA555800AC301B /* PayPalWebCheckoutClient_Tests.swift in Sources */, BCE8A7F327EA531C00AC301B /* MockPayPalWebDelegate.swift in Sources */, diff --git a/Sources/PayPalWebPayments/PayPalVaultDelegate.swift b/Sources/PayPalWebPayments/PayPalVaultDelegate.swift new file mode 100644 index 000000000..f189b31fa --- /dev/null +++ b/Sources/PayPalWebPayments/PayPalVaultDelegate.swift @@ -0,0 +1,25 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// PayPalVault delegate to vault results from PayPalWebCheckoutClient +public protocol PayPalVaultDelegate: AnyObject { + + /// Notify that the PayPal vault flow finished with a successful result + /// - Parameters: + /// - client: the PayPalWebCheckoutClient associated with delegate + /// - didFinishWithResult: the successful result from the flow + func paypal(_ paypalWebClient: PayPalWebCheckoutClient, didFinishWithVaultResult paypalVaultResult: PayPalVaultResult) + + /// Notify that an error occurred in the PayPal vault flow + /// - Parameters: + /// - client: the PayPalWebCheckoutClien associated with delegate + /// - didFinishWithError: the error returned by the PayPal vault flow + func paypal(_ paypalWebClient: PayPalWebCheckoutClient, didFinishWithVaultError vaultError: CoreSDKError) + + /// Notify that a cancellation occurred in the PayPal vault flow + /// - Parameters: + /// - client: the PayPalWebCheckoutClien associated with delegate + func paypalDidCancel(_ paypalWebClient: PayPalWebCheckoutClient) +} diff --git a/Sources/PayPalWebPayments/PayPalVaultResult.swift b/Sources/PayPalWebPayments/PayPalVaultResult.swift new file mode 100644 index 000000000..5846b4a11 --- /dev/null +++ b/Sources/PayPalWebPayments/PayPalVaultResult.swift @@ -0,0 +1,11 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// The result of a vault without purchase flow. +public struct PayPalVaultResult: Decodable, Equatable { + + public let tokenID: String + public let approvalSessionID: String +} diff --git a/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift b/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift index 5a59d7035..387a43f10 100644 --- a/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift +++ b/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift @@ -6,6 +6,7 @@ import CorePayments public class PayPalWebCheckoutClient: NSObject { + public weak var vaultDelegate: PayPalVaultDelegate? public weak var delegate: PayPalWebCheckoutDelegate? let config: CoreConfig private let webAuthenticationSession: WebAuthenticationSession @@ -96,6 +97,51 @@ public class PayPalWebCheckoutClient: NSObject { return checkoutURLComponents?.url } + /// Starts a web session for vaulting PayPal Payment Method + /// After setupToken successfullly attaches a payment method, you will need to create a payment token with the setup token + /// - Parameters: + /// - url: URL created from string value from setupToken API + /// - Returns: PayPalVaultResult + /// - Throws: PayPalSDK error if vaulting could not complete successfully + public func vault(url: URL) { + webAuthenticationSession.start( + url: url, + context: self, + sessionDidDisplay: { [weak self] didDisplay in + if didDisplay { + self?.analyticsService?.sendEvent("paypal-vault:browser-presentation:succeeded") + } else { + self?.analyticsService?.sendEvent("paypal-vault:browser-presentation:failed") + } + }, + sessionDidComplete: { url, error in + if let error = error { + switch error { + case ASWebAuthenticationSessionError.canceledLogin: + self.notifyVaultCancellation() + return + default: + self.notifyVaultFailure(with: PayPalWebCheckoutClientError.webSessionError(error)) + return + } + } + + if let url = url { + guard let tokenID = self.getQueryStringParameter(url: url.absoluteString, param: "approval_token_id"), + let approvalSessionID = self.getQueryStringParameter(url: url.absoluteString, param: "approval_session_id"), + !tokenID.isEmpty, !approvalSessionID.isEmpty + else { + self.notifyVaultFailure(with: PayPalWebCheckoutClientError.payPalVaultResponseError) + return + } + + let paypalVaultResult = PayPalVaultResult(tokenID: tokenID, approvalSessionID: approvalSessionID) + self.notifyVaultSuccess(for: paypalVaultResult) + } + } + ) + } + private func getQueryStringParameter(url: String, param: String) -> String? { guard let url = URLComponents(string: url) else { return nil } return url.queryItems?.first { $0.name == param }?.value @@ -116,6 +162,19 @@ public class PayPalWebCheckoutClient: NSObject { analyticsService?.sendEvent("paypal-web-payments:browser-login:canceled") delegate?.payPalDidCancel(self) } + + private func notifyVaultSuccess(for result: PayPalVaultResult) { + vaultDelegate?.paypal(self, didFinishWithVaultResult: result) + } + + private func notifyVaultFailure(with error: CoreSDKError) { + analyticsService?.sendEvent("paypal-web-payments:failed") + vaultDelegate?.paypal(self, didFinishWithVaultError: error) + } + + private func notifyVaultCancellation() { + vaultDelegate?.paypalDidCancel(self) + } } // MARK: - ASWebAuthenticationPresentationContextProviding conformance diff --git a/Sources/PayPalWebPayments/PayPalWebCheckoutClientError.swift b/Sources/PayPalWebPayments/PayPalWebCheckoutClientError.swift index 1bcdc1e9b..ccfd943fe 100644 --- a/Sources/PayPalWebPayments/PayPalWebCheckoutClientError.swift +++ b/Sources/PayPalWebPayments/PayPalWebCheckoutClientError.swift @@ -20,6 +20,9 @@ enum PayPalWebCheckoutClientError { /// 3. Result did not contain the expected data. case malformedResultError + + /// 4. Vault result did not return a token id + case paypalVaultResponseError } static let webSessionError: (Error) -> CoreSDKError = { error in @@ -41,4 +44,10 @@ enum PayPalWebCheckoutClientError { domain: domain, errorDescription: "Result did not contain the expected data." ) + + static let payPalVaultResponseError = CoreSDKError( + code: Code.paypalVaultResponseError.rawValue, + domain: domain, + errorDescription: "Error parsing paypal vault response" + ) } diff --git a/UnitTests/PayPalWebPaymentsTests/Mocks/MockPayPalVaultDelegate.swift b/UnitTests/PayPalWebPaymentsTests/Mocks/MockPayPalVaultDelegate.swift new file mode 100644 index 000000000..7b8fc6ea3 --- /dev/null +++ b/UnitTests/PayPalWebPaymentsTests/Mocks/MockPayPalVaultDelegate.swift @@ -0,0 +1,37 @@ +@testable import CorePayments +@testable import PayPalWebPayments + +class MockPayPalVaultDelegate: PayPalVaultDelegate { + + private var success: ((PayPalWebCheckoutClient, PayPalVaultResult) -> Void)? + private var failure: ((PayPalWebCheckoutClient, CoreSDKError) -> Void)? + private var cancel: ((PayPalWebCheckoutClient) -> Void)? + + required init( + success: ((PayPalWebCheckoutClient, PayPalVaultResult) -> Void)? = nil, + error: ((PayPalWebCheckoutClient, CoreSDKError) -> Void)? = nil, + cancel: ((PayPalWebCheckoutClient) -> Void)? = nil + ) { + self.success = success + self.failure = error + self.cancel = cancel + } + + func paypal( + _ paypalWebClient: PayPalWebPayments.PayPalWebCheckoutClient, + didFinishWithVaultResult paypalVaultResult: PayPalVaultResult + ) { + success?(paypalWebClient, paypalVaultResult) + } + + func paypal( + _ paypalWebClient: PayPalWebPayments.PayPalWebCheckoutClient, + didFinishWithVaultError vaultError: CorePayments.CoreSDKError + ) { + failure?(paypalWebClient, vaultError) + } + + func paypalDidCancel(_ paypalWebClient: PayPalWebPayments.PayPalWebCheckoutClient) { + cancel?(paypalWebClient) + } +} diff --git a/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift b/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift index d148f13a2..e89948031 100644 --- a/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift +++ b/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift @@ -10,7 +10,7 @@ class PayPalClient_Tests: XCTestCase { var mockWebAuthenticationSession: MockWebAuthenticationSession! var payPalClient: PayPalWebCheckoutClient! var mockNetworkingClient: MockNetworkingClient! - + override func setUp() { super.setUp() config = CoreConfig(clientID: "testClientID", environment: .sandbox) @@ -24,6 +24,103 @@ class PayPalClient_Tests: XCTestCase { ) } + func testVault_whenSuccessUrl_ReturnsVaultToken() { + + mockWebAuthenticationSession.cannedResponseURL = URL(string: "sdk.ios.paypal://vault/success?approval_token_id=fakeTokenID&approval_session_id=fakeSessionID") + + let expectation = expectation(description: "vault(url:) completed") + + let url = URL(string: "https://sandbox.paypal.com/vault") + let expectedTokenIDResult = "fakeTokenID" + let expectedSessionIDResult = "fakeSessionID" + let mockVaultDelegate = MockPayPalVaultDelegate(success: {_, result in + XCTAssertEqual(expectedTokenIDResult, result.tokenID) + XCTAssertEqual(expectedSessionIDResult, result.approvalSessionID) + expectation.fulfill() + }, error: {_, _ in + XCTFail("Invoked error() callback. Should invoke success().") + }) + payPalClient.vaultDelegate = mockVaultDelegate + payPalClient.vault(url: url!) + + waitForExpectations(timeout: 10) + } + + func testVault_whenWebSession_cancelled() { + + mockWebAuthenticationSession.cannedErrorResponse = ASWebAuthenticationSessionError( + .canceledLogin, + userInfo: ["Description": "Mock cancellation error description."] + ) + + let expectation = expectation(description: "vault(url:) completed") + + let url = URL(string: "https://sandbox.paypal.com/vault") + let mockVaultDelegate = MockPayPalVaultDelegate(success: {_, _ in + XCTFail("Invoked success callback. Should invoke cancel().") + }, error: {_, _ in + XCTFail("Invoked error() callback. Should invoke success().") + }, cancel: { _ in + XCTAssert(true) + expectation.fulfill() + }) + payPalClient.vaultDelegate = mockVaultDelegate + payPalClient.vault(url: url!) + + waitForExpectations(timeout: 10) + } + + func testVault_whenWebSession_returnsDefaultError() { + + let expectedError = CoreSDKError( + code: PayPalWebCheckoutClientError.Code.webSessionError.rawValue, + domain: PayPalWebCheckoutClientError.domain, + errorDescription: PayPalWebCheckoutClientError.payPalVaultResponseError.errorDescription + ) + mockWebAuthenticationSession.cannedErrorResponse = expectedError + + let expectation = expectation(description: "vault(url:) completed") + + let url = URL(string: "https://sandbox.paypal.com/vault") + let mockVaultDelegate = MockPayPalVaultDelegate(success: {_, _ in + XCTFail("Invoked success callback. Should invoke error().") + }, error: {_, vaultError in + XCTAssertEqual(vaultError.code, expectedError.code) + expectation.fulfill() + }, cancel: { _ in + XCTFail("Invoked cancel callback. Should invoke error().") + }) + payPalClient.vaultDelegate = mockVaultDelegate + payPalClient.vault(url: url!) + + waitForExpectations(timeout: 10) + } + + func testVault_whenSuccessUrl_missingToken_returnsError() { + + mockWebAuthenticationSession.cannedResponseURL = URL(string: "sdk.ios.paypal://vault/success?approval_token_id=&approval_session_id=fakeSessionID") + + let expectation = expectation(description: "vault(url:) completed") + + let url = URL(string: "https://sandbox.paypal.com/vault") + let expectedError = CoreSDKError( + code: PayPalWebCheckoutClientError.payPalVaultResponseError.code, + domain: PayPalWebCheckoutClientError.domain, + errorDescription: PayPalWebCheckoutClientError.payPalVaultResponseError.errorDescription + ) + + let mockVaultDelegate = MockPayPalVaultDelegate(success: {_, _ in + XCTFail("Invoked success() callback. Should invoke error().") + }, error: {_, vaultError in + XCTAssertEqual(vaultError.code, expectedError.code) + expectation.fulfill() + }) + payPalClient.vaultDelegate = mockVaultDelegate + payPalClient.vault(url: url!) + + waitForExpectations(timeout: 10) + } + func testStart_whenNativeSDKOnCancelCalled_returnsCancellationError() { let request = PayPalWebCheckoutRequest(orderID: "1234") let delegate = MockPayPalWebDelegate()