From 96381080ea0a26f4829458920a6f8bfcdc9e1711 Mon Sep 17 00:00:00 2001 From: John Woo Date: Mon, 12 Aug 2024 12:28:59 -0700 Subject: [PATCH] Stripe SDK 23.29.1 --- CHANGELOG.md | 1412 +++++++ MIGRATING.md | 308 ++ Package.swift | 157 + README.md | 158 +- .../Stripe Tests-Debug.xcconfig | 8 + .../Stripe Tests-Release.xcconfig | 8 + .../BuildConfigurations/Stripe-Debug.xcconfig | 7 + .../Stripe-Release.xcconfig | 7 + Stripe/Stripe.xcodeproj/project.pbxproj | 2293 ++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/StripeiOS.xcscheme | 81 + .../xcschemes/StripeiOSTestHostApp.xcscheme | 89 + Stripe/StripeiOS/Docs.docc/Stripe.md | 3 + Stripe/StripeiOS/Info.plist | 24 + Stripe/StripeiOS/PrivacyInfo.xcprivacy | 45 + .../bg-BG.lproj/Localizable.strings | 39 + .../ca-ES.lproj/Localizable.strings | 39 + .../cs-CZ.lproj/Localizable.strings | 39 + .../da.lproj/Localizable.strings | 39 + .../de.lproj/Localizable.strings | 39 + .../el-GR.lproj/Localizable.strings | 39 + .../en-GB.lproj/Localizable.strings | 39 + .../en.lproj/Localizable.strings | 60 + .../es-419.lproj/Localizable.strings | 39 + .../es.lproj/Localizable.strings | 39 + .../et-EE.lproj/Localizable.strings | 39 + .../fi.lproj/Localizable.strings | 39 + .../fil.lproj/Localizable.strings | 39 + .../fr-CA.lproj/Localizable.strings | 39 + .../fr.lproj/Localizable.strings | 39 + .../hr.lproj/Localizable.strings | 39 + .../hu.lproj/Localizable.strings | 39 + .../id.lproj/Localizable.strings | 39 + .../it.lproj/Localizable.strings | 39 + .../ja.lproj/Localizable.strings | 39 + .../ko.lproj/Localizable.strings | 39 + .../lt-LT.lproj/Localizable.strings | 39 + .../lv-LV.lproj/Localizable.strings | 39 + .../ms-MY.lproj/Localizable.strings | 39 + .../mt.lproj/Localizable.strings | 39 + .../nb.lproj/Localizable.strings | 39 + .../nl.lproj/Localizable.strings | 39 + .../nn-NO.lproj/Localizable.strings | 39 + .../pl-PL.lproj/Localizable.strings | 39 + .../pt-BR.lproj/Localizable.strings | 39 + .../pt-PT.lproj/Localizable.strings | 39 + .../ro-RO.lproj/Localizable.strings | 39 + .../ru.lproj/Localizable.strings | 39 + .../sk-SK.lproj/Localizable.strings | 39 + .../sl-SI.lproj/Localizable.strings | 39 + .../sv.lproj/Localizable.strings | 39 + .../tk.lproj/Localizable.strings | 0 .../tr.lproj/Localizable.strings | 39 + .../vi.lproj/Localizable.strings | 39 + .../zh-HK.lproj/Localizable.strings | 39 + .../zh-Hans.lproj/Localizable.strings | 39 + .../zh-Hant.lproj/Localizable.strings | 39 + .../StripeiOS.xcassets/Cards/Contents.json | 6 + .../Contents.json | 23 + .../stp_card_form_amex_cvc.png | Bin 0 -> 1795 bytes .../stp_card_form_amex_cvc@2x.png | Bin 0 -> 2975 bytes .../stp_card_form_amex_cvc@3x.png | Bin 0 -> 4550 bytes .../stp_card_form_back.imageset/Contents.json | 23 + .../stp_card_form_back.png | Bin 0 -> 1949 bytes .../stp_card_form_back@2x.png | Bin 0 -> 3027 bytes .../stp_card_form_back@3x.png | Bin 0 -> 4810 bytes .../Contents.json | 23 + .../stp_card_form_front.png | Bin 0 -> 964 bytes .../stp_card_form_front@2x.png | Bin 0 -> 2424 bytes .../stp_card_form_front@3x.png | Bin 0 -> 3414 bytes .../StripeiOS.xcassets/Contents.json | 6 + .../StripeiOS.xcassets/FPX/Contents.json | 6 + .../Contents.json | 23 + .../stp_bank_fpx_affin_bank.png | Bin 0 -> 627 bytes .../stp_bank_fpx_affin_bank@2x.png | Bin 0 -> 1236 bytes .../stp_bank_fpx_affin_bank@3x.png | Bin 0 -> 1869 bytes .../Contents.json | 23 + .../stp_bank_fpx_alliance_bank.png | Bin 0 -> 514 bytes .../stp_bank_fpx_alliance_bank@2x.png | Bin 0 -> 806 bytes .../stp_bank_fpx_alliance_bank@3x.png | Bin 0 -> 1067 bytes .../Contents.json | 23 + .../stp_bank_fpx_ambank.png | Bin 0 -> 585 bytes .../stp_bank_fpx_ambank@2x.png | Bin 0 -> 1097 bytes .../stp_bank_fpx_ambank@3x.png | Bin 0 -> 1638 bytes .../Contents.json | 23 + .../stp_bank_fpx_bank_islam.png | Bin 0 -> 463 bytes .../stp_bank_fpx_bank_islam@2x.png | Bin 0 -> 992 bytes .../stp_bank_fpx_bank_islam@3x.png | Bin 0 -> 1488 bytes .../Contents.json | 23 + .../stp_bank_fpx_bank_muamalat.png | Bin 0 -> 718 bytes .../stp_bank_fpx_bank_muamalat@2x.png | Bin 0 -> 1812 bytes .../stp_bank_fpx_bank_muamalat@3x.png | Bin 0 -> 2204 bytes .../Contents.json | 23 + .../stp_bank_fpx_bank_rakyat.png | Bin 0 -> 595 bytes .../stp_bank_fpx_bank_rakyat@2x.png | Bin 0 -> 1098 bytes .../stp_bank_fpx_bank_rakyat@3x.png | Bin 0 -> 1583 bytes .../stp_bank_fpx_bsn.imageset/Contents.json | 23 + .../stp_bank_fpx_bsn.png | Bin 0 -> 885 bytes .../stp_bank_fpx_bsn@2x.png | Bin 0 -> 1874 bytes .../stp_bank_fpx_bsn@3x.png | Bin 0 -> 2235 bytes .../stp_bank_fpx_cimb.imageset/Contents.json | 23 + .../stp_bank_fpx_cimb.png | Bin 0 -> 305 bytes .../stp_bank_fpx_cimb@2x.png | Bin 0 -> 462 bytes .../stp_bank_fpx_cimb@3x.png | Bin 0 -> 622 bytes .../Contents.json | 23 + .../stp_bank_fpx_hong_leong_bank.png | Bin 0 -> 923 bytes .../stp_bank_fpx_hong_leong_bank@2x.png | Bin 0 -> 1835 bytes .../stp_bank_fpx_hong_leong_bank@3x.png | Bin 0 -> 2647 bytes .../stp_bank_fpx_hsbc.imageset/Contents.json | 23 + .../stp_bank_fpx_hsbc.png | Bin 0 -> 411 bytes .../stp_bank_fpx_hsbc@2x.png | Bin 0 -> 785 bytes .../stp_bank_fpx_hsbc@3x.png | Bin 0 -> 1205 bytes .../stp_bank_fpx_kfh.imageset/Contents.json | 23 + .../stp_bank_fpx_kfh.png | Bin 0 -> 1088 bytes .../stp_bank_fpx_kfh@2x.png | Bin 0 -> 1882 bytes .../stp_bank_fpx_kfh@3x.png | Bin 0 -> 2637 bytes .../Contents.json | 23 + .../stp_bank_fpx_maybank2e.png | Bin 0 -> 1376 bytes .../stp_bank_fpx_maybank2e@2x.png | Bin 0 -> 2377 bytes .../stp_bank_fpx_maybank2e@3x.png | Bin 0 -> 4570 bytes .../Contents.json | 23 + .../stp_bank_fpx_maybank2u.png | Bin 0 -> 1376 bytes .../stp_bank_fpx_maybank2u@2x.png | Bin 0 -> 2377 bytes .../stp_bank_fpx_maybank2u@3x.png | Bin 0 -> 4570 bytes .../stp_bank_fpx_ocbc.imageset/Contents.json | 23 + .../stp_bank_fpx_ocbc.png | Bin 0 -> 880 bytes .../stp_bank_fpx_ocbc@2x.png | Bin 0 -> 1770 bytes .../stp_bank_fpx_ocbc@3x.png | Bin 0 -> 2054 bytes .../Contents.json | 23 + .../stp_bank_fpx_public_bank.png | Bin 0 -> 505 bytes .../stp_bank_fpx_public_bank@2x.png | Bin 0 -> 927 bytes .../stp_bank_fpx_public_bank@3x.png | Bin 0 -> 1231 bytes .../stp_bank_fpx_rhb.imageset/Contents.json | 23 + .../stp_bank_fpx_rhb.png | Bin 0 -> 681 bytes .../stp_bank_fpx_rhb@2x.png | Bin 0 -> 1299 bytes .../stp_bank_fpx_rhb@3x.png | Bin 0 -> 1876 bytes .../Contents.json | 23 + .../stp_bank_fpx_standard_chartered.png | Bin 0 -> 890 bytes .../stp_bank_fpx_standard_chartered@2x.png | Bin 0 -> 1807 bytes .../stp_bank_fpx_standard_chartered@3x.png | Bin 0 -> 2596 bytes .../stp_bank_fpx_uob.imageset/Contents.json | 23 + .../stp_bank_fpx_uob.png | Bin 0 -> 323 bytes .../stp_bank_fpx_uob@2x.png | Bin 0 -> 560 bytes .../stp_bank_fpx_uob@3x.png | Bin 0 -> 757 bytes .../stp_fpx_big_logo.imageset/Contents.json | 23 + .../stp_fpx_big_logo.png | Bin 0 -> 3536 bytes .../stp_fpx_big_logo@2x.png | Bin 0 -> 7650 bytes .../stp_fpx_big_logo@3x.png | Bin 0 -> 12473 bytes .../FPX/stp_fpx_logo.imageset/Contents.json | 23 + .../stp_fpx_logo.imageset/stp_fpx_logo.png | Bin 0 -> 564 bytes .../stp_fpx_logo.imageset/stp_fpx_logo@2x.png | Bin 0 -> 1092 bytes .../stp_fpx_logo.imageset/stp_fpx_logo@3x.png | Bin 0 -> 1607 bytes .../stp_icon_add.imageset/Contents.json | 23 + .../stp_icon_add.imageset/stp_icon_add.png | Bin 0 -> 105 bytes .../stp_icon_add.imageset/stp_icon_add@2x.png | Bin 0 -> 131 bytes .../stp_icon_add.imageset/stp_icon_add@3x.png | Bin 0 -> 153 bytes .../stp_icon_bank.imageset/Contents.json | 23 + .../stp_icon_bank.imageset/stp_icon_bank.png | Bin 0 -> 323 bytes .../stp_icon_bank@2x.png | Bin 0 -> 492 bytes .../stp_icon_bank@3x.png | Bin 0 -> 638 bytes .../stp_icon_checkmark.imageset/Contents.json | 23 + .../stp_icon_checkmark.png | Bin 0 -> 283 bytes .../stp_icon_checkmark@2x.png | Bin 0 -> 491 bytes .../stp_icon_checkmark@3x.png | Bin 0 -> 701 bytes .../stp_shipping_form.imageset/Contents.json | 23 + .../stp_shipping_form.png | Bin 0 -> 2583 bytes .../stp_shipping_form@2x.png | Bin 0 -> 6000 bytes .../stp_shipping_form@3x.png | Bin 0 -> 9692 bytes .../Enums+CustomStringConvertible.swift | 63 + ...PKAddPaymentPassRequest+Stripe_Error.swift | 30 + ...rizationViewController+Stripe_Blocks.swift | 187 + .../Source/STPAPIClient+BasicUI.swift | 178 + .../STPAPIClient+PushProvisioning.swift | 40 + .../Source/STPAddCardViewController.swift | 1001 +++++ .../StripeiOS/Source/STPAddress+BasicUI.swift | 212 ++ .../Source/STPAddressFieldTableViewCell.swift | 509 +++ .../Source/STPAddressViewModel.swift | 434 +++ .../Source/STPAnalyticsClient+BasicUI.swift | 160 + .../Source/STPAnalyticsClient+Payments.swift | 27 + .../Source/STPApplePayContextDelegate.swift | 98 + .../Source/STPApplePayPaymentOption.swift | 51 + .../Source/STPBackendAPIAdapter.swift | 86 + .../STPBankSelectionTableViewCell.swift | 124 + .../STPBankSelectionViewController.swift | 300 ++ .../STPBasicUIAnalyticsSerializer.swift | 84 + Stripe/StripeiOS/Source/STPBlocks.swift | 44 + Stripe/StripeiOS/Source/STPCameraView.swift | 77 + Stripe/StripeiOS/Source/STPCard+BasicUI.swift | 30 + Stripe/StripeiOS/Source/STPCardScanner.swift | 488 +++ .../Source/STPCardScannerTableViewCell.swift | 67 + .../Source/STPCardValidationState.swift | 9 + .../Source/STPCoreScrollViewController.swift | 62 + .../Source/STPCoreTableViewController.swift | 54 + .../Source/STPCoreViewController.swift | 162 + .../StripeiOS/Source/STPCustomerContext.swift | 419 +++ Stripe/StripeiOS/Source/STPEphemeralKey.swift | 104 + .../Source/STPEphemeralKeyManager.swift | 179 + .../Source/STPEphemeralKeyProvider.swift | 74 + .../Source/STPFPXBankStatusResponse.swift | 44 + .../STPFakeAddPaymentPassViewController.swift | 279 ++ Stripe/StripeiOS/Source/STPImageLibrary.swift | 94 + ...PIntentActionLinkAuthenticateAccount.swift | 33 + .../StripeiOS/Source/STPLocalizedString.swift | 16 + .../STPPaymentActivityIndicatorView.swift | 135 + .../Source/STPPaymentCardTextFieldCell.swift | 91 + .../Source/STPPaymentConfiguration.swift | 244 ++ .../StripeiOS/Source/STPPaymentContext.swift | 1266 +++++++ .../Source/STPPaymentContextAmountModel.swift | 96 + .../STPPaymentIntentParams+BasicUI.swift | 22 + .../Source/STPPaymentMethod+BasicUI.swift | 94 + .../STPPaymentMethodParams+BasicUI.swift | 57 + .../StripeiOS/Source/STPPaymentOption.swift | 36 + .../STPPaymentOptionTableViewCell.swift | 326 ++ .../Source/STPPaymentOptionTuple.swift | 88 + ...PaymentOptionsInternalViewController.swift | 593 +++ .../STPPaymentOptionsViewController.swift | 662 ++++ .../StripeiOS/Source/STPPaymentResult.swift | 43 + .../Source/STPPinManagementService.swift | 144 + .../Source/STPPushProvisioningContext.swift | 144 + .../Source/STPPushProvisioningDetails.swift | 97 + .../STPPushProvisioningDetailsParams.swift | 73 + .../Source/STPSectionHeaderView.swift | 161 + .../STPShippingAddressViewController.swift | 674 ++++ .../STPShippingMethodTableViewCell.swift | 147 + .../STPShippingMethodsViewController.swift | 211 ++ .../StripeiOS/Source/STPSource+BasicUI.swift | 74 + Stripe/StripeiOS/Source/STPTheme.swift | 212 ++ .../StripeiOS/Source/STPUserInformation.swift | 42 + .../StripeiOS/Source/String+Localized.swift | 22 + Stripe/StripeiOS/Source/Stripe+Exports.swift | 13 + .../Source/StripeBundleLocator.swift | 20 + .../Source/UIBarButtonItem+Stripe.swift | 54 + .../Source/UINavigationBar+Stripe_Theme.swift | 142 + ...vigationController+Stripe_Completion.swift | 61 + .../UITableViewCell+Stripe_Borders.swift | 103 + .../UIToolbar+Stripe_InputAccessory.swift | 38 + .../Source/UIView+Stripe_FirstResponder.swift | 24 + .../Source/UIView+Stripe_SafeAreaBounds.swift | 24 + ...ewController+Stripe_KeyboardAvoiding.swift | 166 + ...ontroller+Stripe_NavigationItemProxy.swift | 38 + ...ntroller+Stripe_ParentViewController.swift | 50 + .../Source/UserDefaults+Stripe.swift | 29 + Stripe/StripeiOS/Stripe-umbrella.h | 11 + Stripe/StripeiOS/Stripe.modulemap | 6 + Stripe/StripeiOSAppHostedTests/Info.plist | 22 + Stripe/StripeiOSTestHostApp/AppDelegate.swift | 18 + Stripe/StripeiOSTestHostApp/Info.plist | 66 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Resources/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Resources/Base.lproj/Main.storyboard | 24 + .../StripeiOSTestHostApp/ViewController.swift | 18 + Stripe/StripeiOSTests.xctestplan | 45 + Stripe/StripeiOSTests/APIRequestTest.swift | 294 ++ ...erpayPriceBreakdownViewSnapshotTests.swift | 52 + .../StripeiOSTests/AnalyticsHelperTests.swift | 60 + ...oCompleteViewControllerSnapshotTests.swift | 144 + .../StripeiOSTests/CardExpiryDateTests.swift | 81 + .../CircularButtonSnapshotTests.swift | 62 + .../ConfirmButtonSnapshotTests.swift | 113 + .../StripeiOSTests/ConfirmButtonTests.swift | 91 + .../StripeiOSTests/ConsumerSessionTests.swift | 214 ++ .../StripeiOSTests/CustomerAdapterTests.swift | 288 ++ .../Error+PaymentSheetTests.swift | 54 + ...hotTestCase+STPViewControllerLoading.swift | 79 + .../StripeiOSTests/FormSpecProviderTest.swift | 252 ++ .../FraudDetectionDataTest.swift | 31 + Stripe/StripeiOSTests/HostedSurfaceTest.swift | 83 + Stripe/StripeiOSTests/ImageTest.swift | 27 + Stripe/StripeiOSTests/Info.plist | 24 + ...LinkInlineSignupElementSnapshotTests.swift | 170 + .../LinkLegalTermsViewSnapshotTests.swift | 102 + .../LinkSignupViewModelTests.swift | 216 ++ .../MKPlacemark+PaymentSheetTests.swift | 209 ++ .../StripeiOSTests/NSArray+StripeTest.swift | 67 + .../NSDecimalNumber+StripeTest.swift | 103 + .../NSDictionary+StripeTest.swift | 254 ++ .../NSLocale+STPSwizzling.swift | 96 + .../StripeiOSTests/NSString+StripeTest.swift | 104 + .../NSURLComponents_StripeTest.swift | 40 + .../OneTimeCodeTextFieldSnapshotTests.swift | 63 + .../OneTimeCodeTextFieldTests.swift | 327 ++ .../OperationDebouncerTests.swift | 45 + .../StripeiOSTests/PKPayment+StripeTest.swift | 36 + .../PayWithLinkButtonSnapshotTests.swift | 102 + .../StripeiOSTests/PaymentAnalyticTest.swift | 30 + .../PaymentTypeCellSnapshotTests.swift | 76 + .../Resources/Images.xcassets/Contents.json | 6 + .../MockFiles/paymentIntentResponse.json | 53 + .../Resources/stp_test_upload_image.jpeg | Bin 0 -> 3340 bytes .../RotatingCardBrandsViewSnapshotTests.swift | 32 + .../RotatingCardBrandsViewTests.swift | 42 + .../STPAPIClient+LinkAccountSessionTest.swift | 23 + .../STPAPIClientNetworkBridgeTest.swift | 412 +++ .../STPAPIClientStubbedTest.swift | 311 ++ Stripe/StripeiOSTests/STPAPIClientTest.swift | 149 + .../STPAPISettingsObjCBridgeTest.m | 84 + .../STPAUBECSDebitFormViewSnapshotTests.swift | 115 + .../STPAUBECSFormViewModelTests.swift | 584 +++ ...wControllerLocalizationSnapshotTests.swift | 101 + .../STPAddCardViewControllerTest.swift | 290 ++ Stripe/StripeiOSTests/STPAddressTests.swift | 523 +++ .../STPAddressViewModelTest.swift | 205 ++ .../STPAnalyticsClientPaymentsTest.swift | 247 ++ .../STPApplePayContextFunctionalTest.swift | 362 ++ ...PApplePayContextFunctionalTestExtras.swift | 44 + .../STPApplePayContextTest.swift | 202 + .../STPApplePayFunctionalTest.swift | 107 + .../STPApplePayPaymentOptionTest.swift | 40 + Stripe/StripeiOSTests/STPApplePayTest.swift | 94 + ...BECSDebitAccountNumberValidatorTests.swift | 240 ++ .../STPBSBNumberValidatorTests.swift | 92 + .../STPBankAccountFunctionalTest.swift | 62 + .../STPBankAccountParamsTest.swift | 106 + .../StripeiOSTests/STPBankAccountTest.swift | 124 + Stripe/StripeiOSTests/STPBinRangeTest.swift | 190 + Stripe/StripeiOSTests/STPBlocks.h | 255 ++ .../STPCardBINMetadataTests.swift | 55 + Stripe/StripeiOSTests/STPCardBrandTest.swift | 95 + ...PCardCVCInputTextFieldFormatterTests.swift | 52 + ...TPCardCVCInputTextFieldSnapshotTests.swift | 57 + .../STPCardCVCInputTextFieldTests.swift | 34 + ...PCardCVCInputTextFieldValidatorTests.swift | 54 + ...rdExpiryInputTextFieldFormatterTests.swift | 64 + ...ardExpiryInputTextFieldSnapshotTests.swift | 52 + ...rdExpiryInputTextFieldValidatorTests.swift | 139 + .../STPCardFormViewSnapshotTests.swift | 160 + .../StripeiOSTests/STPCardFormViewTests.swift | 283 ++ .../STPCardFunctionalTest.swift | 146 + ...rdNumberInputTextFieldFormatterTests.swift | 73 + ...ardNumberInputTextFieldSnapshotTests.swift | 57 + ...rdNumberInputTextFieldValidatorTests.swift | 207 ++ Stripe/StripeiOSTests/STPCardParamsTest.swift | 173 + Stripe/StripeiOSTests/STPCardTest.swift | 270 ++ .../StripeiOSTests/STPCardValidatorTest.swift | 480 +++ Stripe/StripeiOSTests/STPCertTest.swift | 68 + .../STPConfirmCardOptionsTest.swift | 31 + .../STPConfirmPaymentMethodOptionsTest.swift | 34 + .../STPConnectAccountAddressTest.swift | 33 + .../STPConnectAccountFunctionalTest.swift | 85 + .../STPConnectAccountParamsTest.swift | 53 + ...CountryPickerInputFieldSnapshotTests.swift | 27 + .../STPCustomerContextTest.swift | 671 ++++ Stripe/StripeiOSTests/STPCustomerTest.swift | 63 + Stripe/StripeiOSTests/STPE2ETest.swift | 156 + .../STPEphemeralKeyManagerTest.swift | 155 + .../StripeiOSTests/STPEphemeralKeyTest.swift | 32 + Stripe/StripeiOSTests/STPErrorBridgeTest.m | 37 + .../StripeiOSTests/STPFPXBankBrandTest.swift | 104 + .../STPFileFunctionalTest.swift | 85 + Stripe/StripeiOSTests/STPFileTest.swift | 99 + ...ingPlaceholderTextFieldSnapshotTests.swift | 439 +++ .../StripeiOSTests/STPFormEncoderTest.swift | 210 ++ .../StripeiOSTests/STPFormTextFieldTest.swift | 62 + .../STPFormViewSnapshotTests.swift | 161 + ...GenericInputPickerFieldSnapshotTests.swift | 79 + ...GenericInputPickerFieldValidatorTest.swift | 45 + ...TPGenericInputTextFieldSnapshotTests.swift | 42 + .../StripeiOSTests/STPImageLibraryTest.swift | 367 ++ .../STPInputTextFieldFormatterTests.swift | 81 + .../STPInputTextFieldValidatorTests.swift | 64 + ...IntentActionAlipayHandleRedirectTest.swift | 55 + ...ntActionMultibancoDisplayDetailsTest.swift | 44 + ...PIntentActionPayNowDisplayQrCodeTest.swift | 37 + ...tentActionPromptPayDisplayQrCodeTest.swift | 38 + .../StripeiOSTests/STPIntentActionTest.swift | 136 + .../STPIntentActionTypeTest.swift | 48 + ...tentActionWeChatPayRedirectToAppTest.swift | 44 + ...abeledFormTextFieldViewSnapshotTests.swift | 24 + ...dMultiFormTextFieldViewSnapshotTests.swift | 41 + ...PMandateCustomerAcceptanceParamsTest.swift | 44 + .../STPMandateDataParamsTest.swift | 42 + .../STPMandateOnlineParamsTest.swift | 40 + Stripe/StripeiOSTests/STPMocks.h | 34 + Stripe/StripeiOSTests/STPMocks.m | 55 + ...PNumericDigitInputTextFormatterTests.swift | 157 + .../STPNumericStringValidatorTests.swift | 49 + .../StripeiOSTests/STPPIIFunctionalTest.swift | 46 + .../STPPaymentCardTextFieldKVOTest.m | 83 + .../STPPaymentCardTextFieldTest.swift | 1291 +++++++ .../STPPaymentCardTextFieldTestsSwift.swift | 67 + ...STPPaymentCardTextFieldViewModelTest.swift | 112 + .../STPPaymentConfigurationTest.m | 141 + .../STPPaymentContextApplePayTest.swift | 204 ++ .../STPPaymentContextSnapshotTests.swift | 84 + .../STPPaymentHandlerFunctionalTest.m | 136 + .../STPPaymentHandlerFunctionalTest.swift | 413 +++ .../STPPaymentHandlerRefreshTests.swift | 151 + ...aymentHandlerStubbedMockedFilesTests.swift | 453 +++ .../STPPaymentHandlerTests.swift | 287 ++ .../STPPaymentIntentEnumsTest.swift | 220 ++ .../STPPaymentIntentFunctionalTest.swift | 1529 ++++++++ ...STPPaymentIntentLastPaymentErrorTest.swift | 60 + .../STPPaymentIntentParamsTest.swift | 215 ++ .../StripeiOSTests/STPPaymentIntentTest.swift | 162 + ...PPaymentMethodAUBECSDebitParamsTests.swift | 56 + .../STPPaymentMethodAUBECSDebitTests.swift | 85 + .../STPPaymentMethodAddressTest.swift | 34 + .../STPPaymentMethodAffirmParamsTest.swift | 43 + .../STPPaymentMethodAffirmTests.swift | 47 + ...mentMethodAfterpayClearpayParamsTest.swift | 59 + ...STPPaymentMethodAfterpayClearpayTest.swift | 38 + .../STPPaymentMethodAlmaParamsTests.swift | 45 + .../STPPaymentMethodAlmaTests.swift | 41 + ...STPPaymentMethodAmazonPayParamsTests.swift | 45 + .../STPPaymentMethodAmazonPayTests.swift | 41 + .../STPPaymentMethodBacsDebitTest.swift | 36 + ...TPPaymentMethodBancontactParamsTests.swift | 50 + .../STPPaymentMethodBancontactTests.swift | 45 + .../STPPaymentMethodBillieParamsTests.swift | 44 + .../STPPaymentMethodBillieTests.swift | 39 + .../STPPaymentMethodBillingDetailsTest.swift | 34 + ...aymentMethodBillingDetailsTests+Link.swift | 41 + .../STPPaymentMethodBoletoParamsTests.swift | 72 + .../STPPaymentMethodBoletoTests.swift | 54 + .../STPPaymentMethodCardChecksTest.swift | 45 + .../STPPaymentMethodCardParamsTest.swift | 99 + .../STPPaymentMethodCardTest.swift | 93 + ...aymentMethodCardWalletMasterpassTest.swift | 21 + .../STPPaymentMethodCardWalletTest.swift | 40 + ...mentMethodCardWalletVisaCheckoutTest.swift | 20 + .../STPPaymentMethodCashAppParamsTests.swift | 45 + .../STPPaymentMethodCashAppTests.swift | 41 + .../STPPaymentMethodEPSParamsTests.swift | 51 + .../STPPaymentMethodEPSTests.swift | 44 + .../STPPaymentMethodFPXTest.swift | 35 + .../STPPaymentMethodFunctionalTest.swift | 355 ++ .../STPPaymentMethodGiropayParamsTests.swift | 50 + .../STPPaymentMethodGiropayTests.swift | 44 + .../STPPaymentMethodGrabPayParamsTest.swift | 51 + .../STPPaymentMethodKlarnaParamsTests.swift | 72 + .../STPPaymentMethodKlarnaTests.swift | 47 + ...STPPaymentMethodMobilePayParamsTests.swift | 43 + .../STPPaymentMethodMobilePayTests.swift | 39 + ...TPPaymentMethodMultibancoParamsTests.swift | 47 + .../STPPaymentMethodMultibancoTests.swift | 41 + ...STPPaymentMethodNetBankingParamsTest.swift | 47 + .../STPPaymentMethodNetBankingTests.swift | 45 + .../STPPaymentMethodOXXOParamsTests.swift | 53 + .../STPPaymentMethodOXXOTests.swift | 38 + .../STPPaymentMethodOptionsTest.swift | 133 + .../STPPaymentMethodParamsTest.swift | 33 + .../STPPaymentMethodPayPalParamsTests.swift | 50 + .../STPPaymentMethodPayPalTests.swift | 37 + ...TPPaymentMethodPrzelewy24ParamsTests.swift | 51 + .../STPPaymentMethodPrzelewy24Tests.swift | 44 + ...TPPaymentMethodRevolutPayParamsTests.swift | 43 + .../STPPaymentMethodRevolutPayTests.swift | 39 + .../STPPaymentMethodSEPADebitTest.swift | 39 + .../STPPaymentMethodSatispayTests.swift | 40 + .../STPPaymentMethodSofortParamsTests.swift | 52 + .../STPPaymentMethodSofortTests.swift | 44 + .../STPPaymentMethodSunbitTests.swift | 39 + .../STPPaymentMethodSwishParamsTests.swift | 40 + .../STPPaymentMethodSwishTests.swift | 42 + .../StripeiOSTests/STPPaymentMethodTest.swift | 167 + ...TPPaymentMethodThreeDSecureUsageTest.swift | 27 + .../STPPaymentMethodUPIParamsTest.swift | 52 + .../STPPaymentMethodUPITests.swift | 45 + ...MethodUSBankAccountParamsStubbedTest.swift | 302 ++ ...PaymentMethodUSBankAccountParamsTest.swift | 235 ++ .../STPPaymentMethodUSBankAccountTest.swift | 53 + .../STPPaymentMethodiDEALTest.swift | 37 + ...wControllerLocalizationSnapshotTests.swift | 102 + .../STPPaymentOptionsViewControllerTest.swift | 351 ++ .../STPPhoneNumberValidatorTest.swift | 137 + ...TPPinManagementServiceFunctionalTest.swift | 270 ++ ...stalCodeInputTextFieldFormatterTests.swift | 58 + ...ostalCodeInputTextFieldSnapshotTests.swift | 73 + .../STPPostalCodeInputTextFieldTests.swift | 69 + ...stalCodeInputTextFieldValidatorTests.swift | 79 + .../STPPostalCodeValidatorTest.swift | 103 + ...ushProvisioningDetailsFunctionalTest.swift | 73 + .../STPRadarSessionFunctionalTest.swift | 57 + .../STPRedirectContextTest.swift | 625 ++++ .../STPSetupIntentConfirmParamsTest.swift | 156 + .../STPSetupIntentFunctionalTest.swift | 262 ++ .../STPSetupIntentLastSetupErrorTest.swift | 32 + .../StripeiOSTests/STPSetupIntentTest.swift | 104 + ...wControllerLocalizationSnapshotTests.swift | 113 + ...STPShippingAddressViewControllerTest.swift | 114 + ...wControllerLocalizationSnapshotTests.swift | 76 + .../STPSourceCardDetailsTest.swift | 116 + .../STPSourceFunctionalTest.swift | 603 +++ .../StripeiOSTests/STPSourceOwnerTest.swift | 57 + .../StripeiOSTests/STPSourceParamsTest.swift | 317 ++ .../STPSourceReceiverTest.swift | 48 + .../STPSourceRedirectTest.swift | 100 + .../STPSourceSEPADebitDetailsTest.swift | 49 + Stripe/StripeiOSTests/STPSourceTest.swift | 495 +++ .../STPSourceVerificationTest.swift | 92 + ...PStackViewWithSeparatorSnapshotTests.swift | 214 ++ .../StripeiOSTests/STPStringUtilsTest.swift | 142 + Stripe/StripeiOSTests/STPSwiftFixtures.swift | 107 + .../STPTextFieldDelegateProxyTests.swift | 37 + .../STPThreeDSButtonCustomizationTest.swift | 40 + .../STPThreeDSFooterCustomizationTest.swift | 45 + .../STPThreeDSLabelCustomizationTest.swift | 37 + ...hreeDSNavigationBarCustomizationTest.swift | 48 + ...STPThreeDSSelectionCustomizationTest.swift | 42 + ...STPThreeDSTextFieldCustomizationTest.swift | 48 + .../STPThreeDSUICustomizationTest.swift | 137 + Stripe/StripeiOSTests/STPTokenTest.swift | 67 + ...PUIVCStripeParentViewControllerTests.swift | 39 + .../STPViewWithSeparatorSnapshotTests.swift | 28 + .../ServerErrorMapperTest.swift | 94 + Stripe/StripeiOSTests/StripeErrorTest.swift | 273 ++ Stripe/StripeiOSTests/StripeTests-Prefix.pch | 21 + .../StripeiOS Tests-Bridging-Header.h | 7 + .../TextFieldElement+IBANTest.swift | 140 + .../UINavigationBar+StripeTest.m | 52 + .../UserDefaults+StripeTest.swift | 33 + .../WalletHeaderViewSnapshotTests.swift | 185 + .../Project-Debug.xcconfig | 15 + .../Project-Release.xcconfig | 12 + .../Project-Shared.xcconfig | 63 + .../Stripe3DS2-Debug.xcconfig | 12 + .../Stripe3DS2-Release.xcconfig | 12 + .../Stripe3DS2-Shared.xcconfig | 23 + .../Stripe3DS2DemoUI-Debug.xcconfig | 12 + .../Stripe3DS2DemoUI-Release.xcconfig | 12 + .../Stripe3DS2DemoUI-Shared.xcconfig | 13 + .../Stripe3DS2DemoUITests-Debug.xcconfig | 12 + .../Stripe3DS2DemoUITests-Release.xcconfig | 12 + .../Stripe3DS2DemoUITests-Shared.xcconfig | 13 + .../Stripe3DS2Tests-Debug.xcconfig | 12 + .../Stripe3DS2Tests-Release.xcconfig | 12 + .../Stripe3DS2Tests-Shared.xcconfig | 12 + .../Stripe3DS2.xcodeproj/project.pbxproj | 1750 +++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/Stripe3DS2.xcscheme | 86 + .../xcschemes/Stripe3DS2DemoUI.xcscheme | 107 + Stripe3DS2/Stripe3DS2/Info.plist | 22 + Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.h | 20 + Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.m | 35 + .../Stripe3DS2/NSDictionary+DecodingHelpers.h | 47 + .../Stripe3DS2/NSDictionary+DecodingHelpers.m | 147 + Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.h | 32 + Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.m | 38 + .../NSLayoutConstraint+LayoutSupport.h | 53 + .../NSLayoutConstraint+LayoutSupport.m | 30 + .../Stripe3DS2/NSString+EmptyChecking.h | 19 + .../Stripe3DS2/NSString+EmptyChecking.m | 25 + Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.h | 21 + Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.m | 54 + Stripe3DS2/Stripe3DS2/PrivacyInfo.xcprivacy | 33 + .../Resources/CertificateFiles/amex.der | Bin 0 -> 1237 bytes .../CertificateFiles/cartes-bancaires.der | Bin 0 -> 1212 bytes .../Resources/CertificateFiles/discover.der | Bin 0 -> 1118 bytes .../Resources/CertificateFiles/ec_test.der | Bin 0 -> 525 bytes .../Resources/CertificateFiles/mastercard.der | Bin 0 -> 1465 bytes .../Resources/CertificateFiles/ul-test.der | Bin 0 -> 1578 bytes .../Resources/CertificateFiles/visa.der | Bin 0 -> 1458 bytes .../Chevron.imageset/Chevron@1x.png | Bin 0 -> 311 bytes .../Chevron.imageset/Chevron@2x.png | Bin 0 -> 798 bytes .../Chevron.imageset/Chevron@3x.png | Bin 0 -> 1035 bytes .../Chevron.imageset/Contents.json | 23 + .../Stripe3DS2.xcassets/Contents.json | 6 + .../amex-logo.imageset/Contents.json | 12 + .../american-express@1x.pdf | Bin 0 -> 6261 bytes .../Contents.json | 12 + .../cartes-bancaires-logo.png | Bin 0 -> 14407 bytes .../discover-logo.imageset/Contents.json | 12 + .../discover-logo.imageset/discover@1x.png | Bin 0 -> 3220 bytes .../error.imageset/Contents.json | 23 + .../error.imageset/error@1x.png | Bin 0 -> 791 bytes .../error.imageset/error@2x.png | Bin 0 -> 1879 bytes .../error.imageset/error@3x.png | Bin 0 -> 1020 bytes .../mastercard-logo.imageset/Contents.json | 12 + .../mastercard@1x.pdf | Bin 0 -> 4307 bytes .../visa-logo.imageset/Contents.json | 12 + .../visa-logo.imageset/visa@1x.pdf | Bin 0 -> 1927 bytes .../visa-white-logo.imageset/Contents.json | 12 + .../visa-white@1x.pdf | Bin 0 -> 41739 bytes .../Resources/bg-BG.lproj/Localizable.strings | 45 + .../Resources/ca-ES.lproj/Localizable.strings | 45 + .../Resources/cs-CZ.lproj/Localizable.strings | 45 + .../Resources/da.lproj/Localizable.strings | 39 + .../Resources/de.lproj/Localizable.strings | 39 + .../Resources/el-GR.lproj/Localizable.strings | 45 + .../Resources/en-GB.lproj/Localizable.strings | 39 + .../Resources/en.lproj/Localizable.strings | 45 + .../es-419.lproj/Localizable.strings | 39 + .../Resources/es.lproj/Localizable.strings | 39 + .../Resources/et-EE.lproj/Localizable.strings | 45 + .../Resources/fi.lproj/Localizable.strings | 39 + .../Resources/fil.lproj/Localizable.strings | 45 + .../Resources/fr-CA.lproj/Localizable.strings | 39 + .../Resources/fr.lproj/Localizable.strings | 39 + .../Resources/hr.lproj/Localizable.strings | 45 + .../Resources/hu.lproj/Localizable.strings | 39 + .../Resources/id.lproj/Localizable.strings | 45 + .../Resources/it.lproj/Localizable.strings | 39 + .../Resources/ja.lproj/Localizable.strings | 39 + .../Resources/ko.lproj/Localizable.strings | 39 + .../Resources/lt-LT.lproj/Localizable.strings | 45 + .../Resources/lv-LV.lproj/Localizable.strings | 45 + .../Resources/ms-MY.lproj/Localizable.strings | 45 + .../Resources/mt.lproj/Localizable.strings | 39 + .../Resources/nb.lproj/Localizable.strings | 39 + .../Resources/nl.lproj/Localizable.strings | 39 + .../Resources/nn-NO.lproj/Localizable.strings | 39 + .../Resources/pl-PL.lproj/Localizable.strings | 45 + .../Resources/pt-BR.lproj/Localizable.strings | 39 + .../Resources/pt-PT.lproj/Localizable.strings | 39 + .../Resources/ro-RO.lproj/Localizable.strings | 45 + .../Resources/ru.lproj/Localizable.strings | 39 + .../Resources/sk-SK.lproj/Localizable.strings | 45 + .../Resources/sl-SI.lproj/Localizable.strings | 45 + .../Resources/sv.lproj/Localizable.strings | 39 + .../Resources/tr.lproj/Localizable.strings | 39 + .../Resources/vi.lproj/Localizable.strings | 45 + .../Resources/zh-HK.lproj/Localizable.strings | 39 + .../zh-Hans.lproj/Localizable.strings | 39 + .../zh-Hant.lproj/Localizable.strings | 39 + .../Stripe3DS2/STDSACSNetworkingManager.h | 30 + .../Stripe3DS2/STDSACSNetworkingManager.m | 169 + .../STDSAuthenticationResponseObject.h | 20 + .../STDSAuthenticationResponseObject.m | 100 + Stripe3DS2/Stripe3DS2/STDSBrandingView.h | 23 + Stripe3DS2/Stripe3DS2/STDSBrandingView.m | 133 + Stripe3DS2/Stripe3DS2/STDSBundleLocator.h | 15 + Stripe3DS2/Stripe3DS2/STDSBundleLocator.m | 109 + .../Stripe3DS2/STDSChallengeInformationView.h | 25 + .../Stripe3DS2/STDSChallengeInformationView.m | 137 + .../STDSChallengeRequestParameters.h | 138 + .../STDSChallengeRequestParameters.m | 105 + Stripe3DS2/Stripe3DS2/STDSChallengeResponse.h | 145 + .../Stripe3DS2/STDSChallengeResponseImage.h | 27 + .../STDSChallengeResponseImageObject.h | 23 + .../STDSChallengeResponseImageObject.m | 53 + .../STDSChallengeResponseMessageExtension.h | 30 + ...SChallengeResponseMessageExtensionObject.h | 21 + ...SChallengeResponseMessageExtensionObject.m | 72 + .../Stripe3DS2/STDSChallengeResponseObject.h | 49 + .../Stripe3DS2/STDSChallengeResponseObject.m | 321 ++ .../STDSChallengeResponseSelectionInfo.h | 24 + ...STDSChallengeResponseSelectionInfoObject.h | 21 + ...STDSChallengeResponseSelectionInfoObject.m | 46 + .../STDSChallengeResponseViewController.h | 80 + .../STDSChallengeResponseViewController.m | 595 +++ .../Stripe3DS2/STDSChallengeSelectionView.h | 35 + .../Stripe3DS2/STDSChallengeSelectionView.m | 255 ++ Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.h | 19 + Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.m | 54 + Stripe3DS2/Stripe3DS2/STDSDeviceInformation.h | 21 + Stripe3DS2/Stripe3DS2/STDSDeviceInformation.m | 30 + .../Stripe3DS2/STDSDeviceInformationManager.h | 23 + .../Stripe3DS2/STDSDeviceInformationManager.m | 65 + .../STDSDeviceInformationParameter+Private.h | 76 + .../STDSDeviceInformationParameter.h | 24 + .../STDSDeviceInformationParameter.m | 455 +++ Stripe3DS2/Stripe3DS2/STDSDirectoryServer.h | 132 + .../STDSDirectoryServerCertificate+Internal.h | 22 + .../STDSDirectoryServerCertificate.h | 43 + .../STDSDirectoryServerCertificate.m | 326 ++ .../Stripe3DS2/STDSEllipticCurvePoint.h | 26 + .../Stripe3DS2/STDSEllipticCurvePoint.m | 90 + .../Stripe3DS2/STDSEphemeralKeyPair+Testing.h | 19 + Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.h | 39 + Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.m | 108 + .../Stripe3DS2/STDSErrorMessage+Internal.h | 40 + .../Stripe3DS2/STDSErrorMessage+Internal.m | 91 + .../Stripe3DS2/STDSException+Internal.h | 19 + .../STDSExpandableInformationView.h | 23 + .../STDSExpandableInformationView.m | 150 + Stripe3DS2/Stripe3DS2/STDSIPAddress.h | 15 + Stripe3DS2/Stripe3DS2/STDSIPAddress.m | 43 + Stripe3DS2/Stripe3DS2/STDSImageLoader.h | 36 + Stripe3DS2/Stripe3DS2/STDSImageLoader.m | 50 + Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.h | 19 + Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.m | 41 + Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.h | 45 + Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.m | 407 +++ Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.h | 41 + Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.m | 88 + Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.h | 19 + Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.m | 31 + Stripe3DS2/Stripe3DS2/STDSLocalizedString.h | 18 + Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.h | 19 + Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.m | 21 + Stripe3DS2/Stripe3DS2/STDSProcessingView.h | 26 + Stripe3DS2/Stripe3DS2/STDSProcessingView.m | 98 + .../Stripe3DS2/STDSProgressViewController.h | 23 + .../Stripe3DS2/STDSProgressViewController.m | 58 + Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.h | 49 + Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.m | 366 ++ Stripe3DS2/Stripe3DS2/STDSSelectionButton.h | 26 + Stripe3DS2/Stripe3DS2/STDSSelectionButton.m | 167 + Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.h | 19 + Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.m | 25 + Stripe3DS2/Stripe3DS2/STDSSpacerView.h | 20 + Stripe3DS2/Stripe3DS2/STDSSpacerView.m | 34 + Stripe3DS2/Stripe3DS2/STDSStackView.h | 56 + Stripe3DS2/Stripe3DS2/STDSStackView.m | 164 + .../STDSSynchronousLocationManager.h | 26 + .../STDSSynchronousLocationManager.m | 123 + Stripe3DS2/Stripe3DS2/STDSTextChallengeView.h | 26 + Stripe3DS2/Stripe3DS2/STDSTextChallengeView.m | 128 + .../STDSThreeDSProtocolVersion+Private.h | 53 + .../Stripe3DS2/STDSThreeDSProtocolVersion.m | 15 + .../Stripe3DS2/STDSTransaction+Private.h | 41 + Stripe3DS2/Stripe3DS2/STDSVisionSupport.h | 21 + Stripe3DS2/Stripe3DS2/STDSWebView.h | 22 + Stripe3DS2/Stripe3DS2/STDSWebView.m | 37 + Stripe3DS2/Stripe3DS2/STDSWhitelistView.h | 25 + Stripe3DS2/Stripe3DS2/STDSWhitelistView.m | 103 + .../Stripe3DS2/Stripe3DS2-Bridging-Header.h | 12 + .../UIButton+CustomInitialization.h | 21 + .../UIButton+CustomInitialization.m | 69 + Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.h | 21 + Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.m | 24 + .../Stripe3DS2/UIColor+ThirteenSupport.h | 24 + .../Stripe3DS2/UIColor+ThirteenSupport.m | 33 + Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.h | 23 + Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.m | 41 + Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.h | 24 + Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.m | 35 + .../Stripe3DS2/UIViewController+Stripe3DS2.h | 21 + .../Stripe3DS2/UIViewController+Stripe3DS2.m | 49 + .../include/STDSAlreadyInitializedException.h | 22 + .../include/STDSAlreadyInitializedException.m | 17 + .../STDSAuthenticationRequestParameters.h | 68 + .../STDSAuthenticationRequestParameters.m | 63 + .../include/STDSAuthenticationResponse.h | 115 + .../include/STDSButtonCustomization.h | 83 + .../include/STDSButtonCustomization.m | 69 + .../include/STDSChallengeParameters.h | 61 + .../include/STDSChallengeParameters.m | 31 + .../include/STDSChallengeStatusReceiver.h | 67 + .../Stripe3DS2/include/STDSCompletionEvent.h | 40 + .../Stripe3DS2/include/STDSCompletionEvent.m | 26 + .../Stripe3DS2/include/STDSConfigParameters.h | 96 + .../Stripe3DS2/include/STDSConfigParameters.m | 113 + .../Stripe3DS2/include/STDSCustomization.h | 25 + .../Stripe3DS2/include/STDSCustomization.m | 25 + .../Stripe3DS2/include/STDSErrorMessage.h | 103 + .../Stripe3DS2/include/STDSErrorMessage.m | 94 + Stripe3DS2/Stripe3DS2/include/STDSException.h | 25 + Stripe3DS2/Stripe3DS2/include/STDSException.m | 27 + .../include/STDSFooterCustomization.h | 41 + .../include/STDSFooterCustomization.m | 45 + .../include/STDSInvalidInputException.h | 21 + .../include/STDSInvalidInputException.m | 17 + .../Stripe3DS2/include/STDSJSONDecodable.h | 33 + .../Stripe3DS2/include/STDSJSONEncodable.h | 22 + .../Stripe3DS2/include/STDSJSONEncoder.h | 27 + .../Stripe3DS2/include/STDSJSONEncoder.m | 51 + .../include/STDSLabelCustomization.h | 31 + .../include/STDSLabelCustomization.m | 43 + .../include/STDSNavigationBarCustomization.h | 59 + .../include/STDSNavigationBarCustomization.m | 44 + .../include/STDSNotInitializedException.h | 23 + .../include/STDSNotInitializedException.m | 17 + .../include/STDSProtocolErrorEvent.h | 42 + .../include/STDSProtocolErrorEvent.m | 28 + .../include/STDSRuntimeErrorEvent.h | 53 + .../include/STDSRuntimeErrorEvent.m | 37 + .../Stripe3DS2/include/STDSRuntimeException.h | 21 + .../Stripe3DS2/include/STDSRuntimeException.m | 17 + .../include/STDSSelectionCustomization.h | 48 + .../include/STDSSelectionCustomization.m | 64 + .../Stripe3DS2/include/STDSStripe3DS2Error.h | 68 + .../Stripe3DS2/include/STDSStripe3DS2Error.m | 15 + .../Stripe3DS2/include/STDSSwiftTryCatch.h | 50 + .../Stripe3DS2/include/STDSSwiftTryCatch.m | 59 + .../include/STDSTextFieldCustomization.h | 47 + .../include/STDSTextFieldCustomization.m | 50 + .../Stripe3DS2/include/STDSThreeDS2Service.h | 84 + .../Stripe3DS2/include/STDSThreeDS2Service.m | 191 + .../include/STDSThreeDSProtocolVersion.h | 15 + .../Stripe3DS2/include/STDSTransaction.h | 94 + .../Stripe3DS2/include/STDSTransaction.m | 531 +++ .../Stripe3DS2/include/STDSUICustomization.h | 109 + .../Stripe3DS2/include/STDSUICustomization.m | 83 + Stripe3DS2/Stripe3DS2/include/STDSWarning.h | 69 + Stripe3DS2/Stripe3DS2/include/STDSWarning.m | 30 + .../Stripe3DS2/include/Stripe3DS2-Prefix.pch | 14 + Stripe3DS2/Stripe3DS2/include/Stripe3DS2.h | 52 + Stripe3DS2/Stripe3DS2DemoUI/Info.plist | 45 + .../Stripe3DS2DemoUI/Resources/150-issuer.png | Bin 0 -> 1135 bytes .../Resources/150-payment.png | Bin 0 -> 1245 bytes .../Stripe3DS2DemoUI/Resources/300-issuer.png | Bin 0 -> 2876 bytes .../Resources/300-payment.png | Bin 0 -> 2828 bytes .../Stripe3DS2DemoUI/Resources/450-issuer.png | Bin 0 -> 4455 bytes .../Resources/450-payment.png | Bin 0 -> 4337 bytes .../Resources/acs_challenge.html | 178 + .../Stripe3DS2DemoUI/Sources/AppDelegate.h | 15 + .../Stripe3DS2DemoUI/Sources/AppDelegate.m | 36 + .../STDSChallengeResponseObject+TestObjects.h | 23 + .../STDSChallengeResponseObject+TestObjects.m | 198 + .../Sources/STDSDemoViewController.h | 20 + .../Sources/STDSDemoViewController.m | 225 ++ Stripe3DS2/Stripe3DS2DemoUI/Sources/main.m | 16 + Stripe3DS2/Stripe3DS2DemoUITests/Info.plist | 24 + ...lengeResponseViewControllerSnapshotTests.m | 117 + Stripe3DS2/Stripe3DS2Resources/Info.plist | 26 + Stripe3DS2/Stripe3DS2Tests/Info.plist | 22 + Stripe3DS2/Stripe3DS2Tests/JSON/ARes.json | 16 + Stripe3DS2/Stripe3DS2Tests/JSON/CRes.json | 31 + .../Stripe3DS2Tests/JSON/ErrorMessage.json | 13 + .../NSDictionary+DecodingHelpersTest.m | 312 ++ .../NSString+EmptyCheckingTests.m | 32 + .../STDSACSNetworkingManagerTest.m | 54 + .../STDSAuthenticationRequestParametersTest.m | 41 + .../STDSAuthenticationResponseTests.m | 32 + .../STDSBase64URLEncodingTests.m | 59 + .../STDSChallengeParametersTests.m | 56 + .../STDSChallengeRequestParametersTest.m | 71 + .../STDSChallengeResponseObjectTest.m | 79 + .../STDSConfigParametersTests.m | 67 + .../STDSDeviceInformationManagerTests.m | 33 + .../STDSDeviceInformationParameterTests.m | 214 ++ .../STDSDirectoryServerCertificateTests.m | 811 ++++ .../STDSEllipticCurvePointTests.m | 42 + .../STDSEphemeralKeyPairTests.m | 41 + .../Stripe3DS2Tests/STDSErrorMessageTest.m | 61 + .../Stripe3DS2Tests/STDSJSONEncoderTest.m | 201 + .../STDSJSONWebEncryptionTests.m | 119 + .../STDSJSONWebSignatureTests.m | 78 + .../STDSSecTypeUtilitiesTests.m | 142 + .../STDSSynchronousLocationManagerTests.m | 28 + .../Stripe3DS2Tests/STDSTestJSONUtils.h | 19 + .../Stripe3DS2Tests/STDSTestJSONUtils.m | 54 + .../STDSThreeDS2ServiceTests.m | 62 + .../Stripe3DS2Tests/STDSTransactionTest.m | 157 + .../STDSUICustomizationTests.m | 249 ++ Stripe3DS2/Stripe3DS2Tests/STDSWarningTests.m | 26 + Stripe3DS2/exported_symbols.txt | 13 + StripeApplePay/README.md | 36 + .../StripeApplePay.xcodeproj/project.pbxproj | 618 ++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripeApplePay.xcscheme | 95 + .../Docs.docc/StripeApplePay.md | 3 + StripeApplePay/StripeApplePay/Info.plist | 22 + .../STPAPIClient+ApplePay.swift | 44 + .../STPApplePayContext+LegacySupport.swift | 55 + .../ApplePayContext/STPApplePayContext.swift | 824 +++++ .../StripeApplePay/Source/Blocks.swift | 18 + .../Extensions/BillingDetails+ApplePay.swift | 101 + .../Source/Extensions/PKContact+Stripe.swift | 26 + .../Source/Extensions/PKPayment+Stripe.swift | 32 + .../PaymentsCore/API/Models/Address.swift | 43 + .../API/Models/BillingDetails.swift | 67 + .../PaymentsCore/API/Models/CardBrand.swift | 33 + .../API/Models/PaymentIntent.swift | 149 + .../API/Models/PaymentIntentParams.swift | 101 + .../API/Models/PaymentMethod.swift | 176 + .../API/Models/PaymentMethodParams.swift | 73 + .../PaymentsCore/API/Models/SetupIntent.swift | 58 + .../API/Models/SetupIntentParams.swift | 66 + .../API/Models/ShippingDetails.swift | 93 + .../PaymentsCore/API/Models/Token.swift | 130 + .../PaymentsCore/API/PaymentIntent+API.swift | 85 + .../PaymentsCore/API/PaymentMethod+API.swift | 61 + .../PaymentsCore/API/SetupIntent+API.swift | 84 + .../Source/PaymentsCore/API/Token+API.swift | 92 + .../STPAnalyticsClient+Payments.swift | 27 + .../STPAnalyticsClient+PaymentsAPI.swift | 67 + .../STPAPIClient+PaymentsCore.swift | 20 + .../Source/StripeCore+Import.swift | 10 + .../StripeApplePay/StripeApplePay.h | 18 + StripeApplePay/StripeApplePayTests/Info.plist | 22 + .../STPTelemetryClientFunctionalTest.swift | 67 + .../PaymentsCore/STPTelemetryClientTest.swift | 75 + .../STPAnalyticsClient+ApplePayTest.swift | 29 + .../STPPaymentMethodFunctionalTest.swift | 97 + .../TelemetryInjectionTest.swift | 95 + .../project.pbxproj | 640 ++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripeCameraCore.xcscheme | 95 + StripeCameraCore/StripeCameraCore/Info.plist | 22 + .../Source/CameraExifMetadata.swift | 46 + .../Categories/CGRect+StripeCameraCore.swift | 80 + .../CVPixelBuffer+StripeCameraCore.swift | 18 + ...UIDeviceOrientation+StripeCameraCore.swift | 26 + .../Source/Categories/UIImage+Buffer.swift | 118 + .../Coordinators/AppSettingsHelper.swift | 43 + .../CameraPermissionsManager.swift | 85 + .../Source/Coordinators/CameraSession.swift | 508 +++ .../MockSimulatorCameraSession.swift | 220 ++ .../Source/Coordinators/Torch.swift | 53 + .../Source/Views/CameraPreviewView.swift | 81 + .../StripeCameraCore/StripeCameraCore.h | 18 + .../StripeCameraCoreTestUtils/Info.plist | 22 + .../Mocks/MockAppSettingsHelper.swift | 21 + .../Mocks/MockCameraPermissionsManager.swift | 41 + .../Mocks/MockTestCameraSession.swift | 180 + .../StripeCameraCoreTestUtils.h | 18 + .../StripeCameraCoreTests/Info.plist | 22 + .../CGRect_StripeCameraCoreTest.swift | 68 + .../StripeCardScan-Debug.xcconfig | 7 + .../StripeCardScan-Release.xcconfig | 7 + StripeCardScan/README.md | 87 + .../StripeCardScan.xcodeproj/project.pbxproj | 1263 +++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripeCardScan.xcscheme | 104 + .../Docs.docc/StripeCardScan.md | 3 + StripeCardScan/StripeCardScan/Info.plist | 22 + .../SSDOcr.mlmodelc/analytics/coremldata.bin | Bin 0 -> 438 bytes .../SSDOcr.mlmodelc/coremldata.bin | Bin 0 -> 589 bytes .../SSDOcr.mlmodelc/metadata.json | 92 + .../SSDOcr.mlmodelc/model.espresso.net | 2489 +++++++++++++ .../SSDOcr.mlmodelc/model.espresso.shape | 661 ++++ .../SSDOcr.mlmodelc/model.espresso.weights | Bin 0 -> 1653056 bytes .../SSDOcr.mlmodelc/model/coremldata.bin | Bin 0 -> 168 bytes .../neural_network_optionals/coremldata.bin | Bin 0 -> 40 bytes .../UxModel.mlmodelc/analytics/coremldata.bin | Bin 0 -> 439 bytes .../UxModel.mlmodelc/coremldata.bin | Bin 0 -> 189 bytes .../UxModel.mlmodelc/metadata.json | 66 + .../UxModel.mlmodelc/model.espresso.net | 3252 +++++++++++++++++ .../UxModel.mlmodelc/model.espresso.shape | 1066 ++++++ .../UxModel.mlmodelc/model.espresso.weights | Bin 0 -> 1381120 bytes .../UxModel.mlmodelc/model/coremldata.bin | Bin 0 -> 208 bytes .../neural_network_optionals/coremldata.bin | Bin 0 -> 40 bytes .../bg-BG.lproj/Localizable.strings | 9 + .../ca-ES.lproj/Localizable.strings | 9 + .../cs-CZ.lproj/Localizable.strings | 9 + .../da.lproj/Localizable.strings | 9 + .../de.lproj/Localizable.strings | 9 + .../el-GR.lproj/Localizable.strings | 9 + .../en-GB.lproj/Localizable.strings | 9 + .../en.lproj/Localizable.strings | 15 + .../es-419.lproj/Localizable.strings | 9 + .../es.lproj/Localizable.strings | 9 + .../et-EE.lproj/Localizable.strings | 9 + .../fi.lproj/Localizable.strings | 9 + .../fil.lproj/Localizable.strings | 9 + .../fr-CA.lproj/Localizable.strings | 9 + .../fr.lproj/Localizable.strings | 9 + .../hr.lproj/Localizable.strings | 9 + .../hu.lproj/Localizable.strings | 9 + .../id.lproj/Localizable.strings | 9 + .../it.lproj/Localizable.strings | 9 + .../ja.lproj/Localizable.strings | 9 + .../ko.lproj/Localizable.strings | 9 + .../lt-LT.lproj/Localizable.strings | 9 + .../lv-LV.lproj/Localizable.strings | 9 + .../ms-MY.lproj/Localizable.strings | 9 + .../mt.lproj/Localizable.strings | 9 + .../nb.lproj/Localizable.strings | 9 + .../nl.lproj/Localizable.strings | 9 + .../nn-NO.lproj/Localizable.strings | 9 + .../pl-PL.lproj/Localizable.strings | 9 + .../pt-BR.lproj/Localizable.strings | 9 + .../pt-PT.lproj/Localizable.strings | 9 + .../ro-RO.lproj/Localizable.strings | 9 + .../ru.lproj/Localizable.strings | 9 + .../sk-SK.lproj/Localizable.strings | 9 + .../sl-SI.lproj/Localizable.strings | 9 + .../sv.lproj/Localizable.strings | 9 + .../tr.lproj/Localizable.strings | 9 + .../vi.lproj/Localizable.strings | 9 + .../zh-HK.lproj/Localizable.strings | 9 + .../zh-Hans.lproj/Localizable.strings | 9 + .../zh-Hant.lproj/Localizable.strings | 9 + .../Source/CardScan/AppleOcr/AppleOcr.swift | 79 + .../CardScan/CardUtils/CardNetwork.swift | 34 + .../Source/CardScan/CardUtils/CardType.swift | 33 + .../CardScan/CardUtils/CreditCardUtils.swift | 390 ++ .../Source/CardScan/CardUtils/Expiry.swift | 20 + .../CreditCardOcr/AppleCreditCardOcr.swift | 96 + .../CreditCardOcrImplementation.swift | 42 + .../CreditCardOcrPrediction.swift | 191 + .../CreditCardOcr/CreditCardOcrResult.swift | 62 + .../CreditCardOcr/ErrorCorrection.swift | 95 + .../CreditCardOcr/MachineLearningResult.swift | 24 + .../CreditCardOcr/MainLoopStateMachine.swift | 119 + .../CardScan/CreditCardOcr/NonNameWords.swift | 43 + .../CardScan/CreditCardOcr/OcrMainLoop.swift | 311 ++ .../CardScan/CreditCardOcr/OcrObject.swift | 32 + .../CreditCardOcr/SSDCreditCardOcr.swift | 50 + .../CardScan/Extensions/Array+utils.swift | 16 + .../CardScan/Extensions/CGRectExtension.swift | 32 + .../CardScan/Extensions/CGrect+utils.swift | 18 + .../CreditCardOcrPrediction+expiry.swift | 14 + .../CardScan/Extensions/Image+utils.swift | 270 ++ .../Extensions/UIImage+pixelBuffer.swift | 248 ++ .../CardScan/MLModels/AsyncModelLoading.swift | 66 + .../CardScan/MLModels/SSDOcr+Utils.swift | 19 + .../Source/CardScan/MLModels/SSDOcr.swift | 320 ++ .../CardScan/MLModels/UxModel+Utils.swift | 19 + .../Source/CardScan/MLModels/UxModel.swift | 306 ++ .../MLRuntime/ActiveStateComputation.swift | 91 + .../Source/CardScan/MLRuntime/AppState.swift | 22 + .../CardScan/MLRuntime/DetectedAllBoxes.swift | 18 + .../MLRuntime/DetectedAllOcrBoxes.swift | 23 + .../CardScan/MLRuntime/DetectedBox.swift | 62 + .../CardScan/MLRuntime/DetectedSSDBox.swift | 51 + .../MLRuntime/DetectedSSDOcrBox.swift | 41 + .../Source/CardScan/MLRuntime/NMS.swift | 75 + .../Source/CardScan/MLRuntime/OcrDD.swift | 27 + .../CardScan/MLRuntime/OcrDDUtils.swift | 177 + .../CardScan/MLRuntime/OcrPriorsGen.swift | 163 + .../MLRuntime/PostDetectionAlgorithm.swift | 221 ++ .../CardScan/MLRuntime/PredictionAPI.swift | 78 + .../CardScan/MLRuntime/PredictionResult.swift | 84 + .../MLRuntime/PredictionUtilOcr.swift | 68 + .../CardScan/MLRuntime/SSDOcrDetect.swift | 207 ++ .../MLRuntime/SSDOcrOutputExtensions.swift | 176 + .../Source/CardScan/MLRuntime/SoftNMS.swift | 80 + .../Source/CardScan/UI/BlurView.swift | 40 + .../Source/CardScan/UI/CardScanSheet.swift | 86 + .../Source/CardScan/UI/CornerView.swift | 72 + .../CardScan/UI/InterfaceOrientation.swift | 56 + .../Source/CardScan/UI/PreviewView.swift | 90 + .../CardScan/UI/ScanBaseViewController.swift | 582 +++ .../CardScan/UI/ScanConfiguration.swift | 11 + .../CardScan/UI/ScanEventsProtocol.swift | 26 + .../Source/CardScan/UI/ScanStats.swift | 84 + .../UI/SimpleScanViewController.swift | 542 +++ .../Source/CardScan/UI/Torch.swift | 48 + .../Source/CardScan/UI/VideoFeed.swift | 244 ++ .../Source/CardScan/Utils/AppInfoUtils.swift | 43 + .../Utils/AtomicPropertyWrapper.swift | 43 + .../Source/CardScan/Utils/DeviceUtils.swift | 82 + ...CardImageVerificationDetailsResponse.swift | 68 + .../ScanStatsPayload+Common.swift | 43 + .../ScanStatsPayload+Tasks.swift | 21 + .../Scan Analytics/ScanStatsPayload.swift | 62 + .../Api/Models/VerificationFramesData.swift | 46 + .../CardVerify/Api/Models/VerifyFrames.swift | 15 + .../STPAPIClient+CardImageVerification.swift | 95 + .../Source/CardVerify/Bouncer.swift | 22 + .../CancellationReason.swift | 18 + .../CardImageVerificationController.swift | 160 + .../CardImageVerificationIntent.swift | 16 + .../CardImageVerificationSheet.swift | 127 + ...dImageVerificationSheetConfiguration.swift | 43 + .../CardScanSheetError.swift | 35 + .../ScanAnalyticsManager+Helpers.swift | 20 + .../ScanAnalyticsManager+Managers.swift | 51 + .../ScanAnalyticsManager+Tasks.swift | 80 + .../ScanAnalyticsManager.swift | 160 + .../Card Image Verification/ScannedCard.swift | 40 + .../ScannedCardImageData+Verification.swift | 137 + .../ScannedCardImageData.swift | 96 + .../StripeCore+Import.swift | 9 + .../Source/CardVerify/CardBase.swift | 46 + .../Source/CardVerify/CardScanFraudData.swift | 180 + .../Source/CardVerify/CardScanMisc.swift | 41 + .../CardVerify/CardVerifyFraudData.swift | 75 + .../CardVerify/CardVerifyStateMachine.swift | 325 ++ .../Source/CardVerify/FadeInAnimation.swift | 67 + .../Source/CardVerify/FrameData.swift | 40 + .../Helpers/EndToEndTestDataSource.swift | 48 + .../Helpers/STPLocalizedString.swift | 16 + .../CardVerify/Helpers/String+Localized.swift | 46 + .../Source/CardVerify/PaymentCard.swift | 76 + .../SimpleScanViewController+Verify.swift | 24 + .../StripeCardScanBundleLocator.swift | 20 + .../Source/CardVerify/UxAnalyzer.swift | 131 + .../Source/CardVerify/UxAndOcrMainLoop.swift | 17 + .../VerifyCardAddViewController.swift | 202 + .../CardVerify/VerifyCardViewController.swift | 310 ++ .../Source/CardVerify/ZoomedInCGImage.swift | 161 + .../StripeCardScan/StripeCardScan.h | 18 + .../Helpers/CardScanMockData.swift | 37 + .../Helpers/Data+Sha256.swift | 26 + .../Helpers/ImageHelpers.swift | 40 + .../Helpers/ScannedCardDetails.swift | 32 + .../Helpers/String+Sha256.swift | 19 + StripeCardScan/StripeCardScanTests/Info.plist | 22 + .../CardImageVerification_CardAdd_200.json | 31 + .../CardImageVerification_CardSet_200.json | 32 + .../Resources/synthetic_test_image.jpg | Bin 0 -> 988407 bytes ...PAPIClient+CardImageVerificationTest.swift | 383 ++ .../ScanStatsPayloadAPIBindingsTests.swift | 357 ++ .../VerifyFramesAPIBindingsTests.swift | 71 + ...CardImageVerificationControllerTests.swift | 170 + ...ImageVerificationDetailsResponseTest.swift | 81 + .../Unit/ImageCompressionTests.swift | 133 + .../Unit/ML Models/UxModelTests.swift | 48 + .../Unit/ScanAnalyticsManagerTests.swift | 165 + .../Unit/StrictModeFramesTest.swift | 159 + .../Unit/StringResourceTests.swift | 27 + .../StripeCore.xcodeproj/project.pbxproj | 1307 +++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripeCore.xcscheme | 102 + StripeCore/StripeCore/Info.plist | 22 + StripeCore/StripeCore/PrivacyInfo.xcprivacy | 33 + .../bg-BG.lproj/Localizable.strings | 31 + .../ca-ES.lproj/Localizable.strings | 31 + .../cs-CZ.lproj/Localizable.strings | 31 + .../da.lproj/Localizable.strings | 31 + .../de.lproj/Localizable.strings | 31 + .../el-GR.lproj/Localizable.strings | 31 + .../en-GB.lproj/Localizable.strings | 31 + .../en.lproj/Localizable.strings | 48 + .../es-419.lproj/Localizable.strings | 31 + .../es.lproj/Localizable.strings | 31 + .../et-EE.lproj/Localizable.strings | 31 + .../fi.lproj/Localizable.strings | 31 + .../fil.lproj/Localizable.strings | 31 + .../fr-CA.lproj/Localizable.strings | 31 + .../fr.lproj/Localizable.strings | 31 + .../hr.lproj/Localizable.strings | 31 + .../hu.lproj/Localizable.strings | 31 + .../id.lproj/Localizable.strings | 31 + .../it.lproj/Localizable.strings | 31 + .../ja.lproj/Localizable.strings | 31 + .../ko.lproj/Localizable.strings | 31 + .../lt-LT.lproj/Localizable.strings | 31 + .../lv-LV.lproj/Localizable.strings | 31 + .../ms-MY.lproj/Localizable.strings | 31 + .../mt.lproj/Localizable.strings | 31 + .../nb.lproj/Localizable.strings | 31 + .../nl.lproj/Localizable.strings | 31 + .../nn-NO.lproj/Localizable.strings | 31 + .../pl-PL.lproj/Localizable.strings | 31 + .../pt-BR.lproj/Localizable.strings | 31 + .../pt-PT.lproj/Localizable.strings | 31 + .../ro-RO.lproj/Localizable.strings | 31 + .../ru.lproj/Localizable.strings | 31 + .../sk-SK.lproj/Localizable.strings | 31 + .../sl-SI.lproj/Localizable.strings | 31 + .../sv.lproj/Localizable.strings | 31 + .../tr.lproj/Localizable.strings | 31 + .../vi.lproj/Localizable.strings | 31 + .../zh-HK.lproj/Localizable.strings | 31 + .../zh-Hans.lproj/Localizable.strings | 31 + .../zh-Hant.lproj/Localizable.strings | 31 + .../API Bindings/Models/EmptyResponse.swift | 14 + .../API Bindings/Models/StripeFile.swift | 45 + .../STPAPIClient+FileUpload.swift | 234 ++ .../Source/API Bindings/STPAPIClient.swift | 554 +++ .../Source/API Bindings/STPAppInfo.swift | 45 + .../STPMultipartFormDataEncoder.swift | 38 + .../STPMultipartFormDataPart.swift | 63 + .../Source/API Bindings/StripeAPI.swift | 197 + .../StripeAPIConfiguration+Version.swift | 19 + .../API Bindings/StripeAPIConfiguration.swift | 21 + .../Source/API Bindings/StripeError.swift | 86 + .../API Bindings/StripeServiceError.swift | 75 + .../Source/Analytics/Analytic.swift | 32 + .../Analytics/AnalyticLoggableError.swift | 140 + .../Analytics/AnalyticLoggableErrorV2.swift | 70 + .../Source/Analytics/AnalyticsClientV2.swift | 153 + .../Source/Analytics/AnalyticsHelper.swift | 54 + .../Source/Analytics/NetworkDetector.swift | 59 + .../Source/Analytics/PluginDetector.swift | 47 + .../Source/Analytics/STPAnalyticEvent.swift | 269 ++ .../Analytics/STPAnalyticsClient+Error.swift | 26 + .../Source/Analytics/STPAnalyticsClient.swift | 158 + .../Categories/Decimal+StripeCore.swift | 15 + .../Source/Categories/Dictionary+Stripe.swift | 96 + .../Enums+CustomStringConvertible.swift | 29 + .../Source/Categories/Locale+StripeCore.swift | 44 + .../Source/Categories/NSArray+Stripe.swift | 31 + .../Categories/NSBundle+Stripe_AppName.swift | 32 + .../NSCharacterSet+StripeCore.swift | 21 + .../Source/Categories/NSError+Stripe.swift | 135 + .../Categories/NSError+StripeCore.swift | 18 + .../NSMutableURLRequest+Stripe.swift | 53 + .../Categories/NSURLComponents+Stripe.swift | 63 + .../Source/Categories/String+StripeCore.swift | 36 + .../Categories/UIImage+StripeCore.swift | 162 + .../Source/Coder/StripeCodable.swift | 177 + .../Source/Coder/StripeJSONDecoder.swift | 712 ++++ .../Source/Coder/StripeJSONEncoder.swift | 564 +++ .../Source/Coder/StripeJSONShared.swift | 37 + .../Source/Coder/UnknownFields.swift | 98 + .../FinancialConnectionsEvent.swift | 151 + .../FinancialConnectionsLinkedBank.swift | 17 + .../FinancialConnectionsSDKInterface.swift | 21 + .../FinancialConnectionsSDKResult.swift | 19 + .../InstantDebitsLinkedBank.swift | 14 + .../StripeCore/Source/Helpers/Async.swift | 158 + .../Helpers/BundleLocatorProtocol.swift | 75 + .../Source/Helpers/FileDownloader.swift | 75 + .../Source/Helpers/InstallMethod.swift | 27 + .../Source/Helpers/PaymentsSDKVariant.swift | 57 + .../Source/Helpers/Result+Extensions.swift | 24 + .../StripeCore/Source/Helpers/STPAssert.swift | 45 + .../Source/Helpers/STPDeviceUtils.swift | 25 + .../Source/Helpers/STPDispatchFunctions.swift | 17 + .../StripeCore/Source/Helpers/STPError.swift | 315 ++ .../Helpers/STPNumericStringValidator.swift | 31 + .../Helpers/STPURLCallbackHandler.swift | 92 + .../Source/Helpers/ServerErrorMapper.swift | 95 + .../Helpers/StripeCoreBundleLocator.swift | 18 + .../Source/Helpers/URLEncoder.swift | 147 + .../Source/Helpers/URLSession+Retry.swift | 42 + .../Localization/STPLocalizationUtils.swift | 97 + .../Localization/STPLocalizedString.swift | 14 + .../Localization/String+Localized.swift | 51 + .../Source/Telemetry/FraudDetectionData.swift | 72 + .../Source/Telemetry/STPTelemetryClient.swift | 221 ++ .../Telemetry/UserDefaults+PaymentsCore.swift | 42 + .../UI/UIActivityIndicatorView+Stripe.swift | 35 + .../StripeCore/Source/UI/UIFont+Stripe.swift | 88 + StripeCore/StripeCore/StripeCore.h | 18 + .../APIStubbedTestCase.swift | 54 + ...alyticsClient+StripeCoreTestingUtils.swift | 18 + .../UIImage+StripeCoreTestingUtils.swift | 30 + .../UIView+StripeCoreTestingUtils.swift | 24 + StripeCore/StripeCoreTestUtils/Info.plist | 22 + .../KeyPathExpectation.swift | 70 + .../Mock Files/File_IdentityDocument.json | 7 + .../Mocks/MockAnalyticsClient.swift | 31 + .../Mocks/MockAnalyticsClientV2.swift | 25 + .../StripeCoreTestUtils/Mocks/MockData.swift | 50 + .../STPSnapshotTestCase.swift | 70 + .../StripeCoreTestUtils/StripeCoreTestUtils.h | 18 + .../StripeCoreTestUtils/TestConstants.swift | 16 + .../URLRequest+StripeTest.swift | 50 + .../XCTestCase+Stripe.swift | 72 + .../STPAPIClient+EmptyResponseTest.swift | 81 + .../STPAPIClient+ErrorResponseTest.swift | 143 + .../API Bindings/StripeCodableTest.swift | 212 ++ .../Analytics/AnalyticLoggableErrorTest.swift | 174 + .../Analytics/AnalyticsClientV2Test.swift | 91 + .../Error_SerializeForLoggingTest.swift | 67 + .../Analytics/STPAnalyticsClientTest.swift | 54 + .../Categories/Dictionary+StripeTests.swift | 37 + .../Categories/NSArray+StripeCoreTest.swift | 27 + .../NSMutableURLRequest+StripeTest.swift | 68 + .../Categories/UIImage+StripeCoreTests.swift | 85 + .../External/TestJSONEncoder.swift | 1756 +++++++++ .../Helpers/URLEncoderTest.swift | 61 + StripeCore/StripeCoreTests/Info.plist | 22 + .../StripeCoreTests/Mock Files/test_image.png | Bin 0 -> 9914 bytes StripeFinancialConnections/README.md | 50 + .../project.pbxproj | 1638 +++++++++ .../contents.xcworkspacedata | 7 + .../StripeFinancialConnections.xcscheme | 95 + .../Docs.docc/StripeFinancialConnections.md | 3 + .../StripeFinancialConnections/Info.plist | 22 + .../PrivacyInfo.xcprivacy | 45 + .../Resources/Images/add@3x.png | Bin 0 -> 490 bytes .../Resources/Images/back_arrow@3x.png | Bin 0 -> 642 bytes .../Resources/Images/brandicon_default@3x.png | Bin 0 -> 1247 bytes .../Resources/Images/bullet@3x.png | Bin 0 -> 327 bytes .../Resources/Images/cancel_circle@3x.png | Bin 0 -> 1208 bytes .../Resources/Images/check@3x.png | Bin 0 -> 460 bytes .../Resources/Images/chevron_down@3x.png | Bin 0 -> 475 bytes .../Resources/Images/close@3x.png | Bin 0 -> 788 bytes .../Resources/Images/generic_error@3x.png | Bin 0 -> 3439 bytes .../Resources/Images/info@3x.png | Bin 0 -> 820 bytes .../Resources/Images/link_logo@3x.png | Bin 0 -> 3548 bytes .../Resources/Images/panel_arrow_right@3x.png | Bin 0 -> 737 bytes .../Resources/Images/person@3x.png | Bin 0 -> 1039 bytes .../Resources/Images/search@3x.png | Bin 0 -> 1425 bytes .../Resources/Images/spinner@3x.png | Bin 0 -> 8428 bytes .../Resources/Images/stripe_logo@3x.png | Bin 0 -> 2889 bytes .../Resources/Images/testmode@3x.png | Bin 0 -> 1571 bytes .../Resources/Images/warning_triangle@3x.png | Bin 0 -> 914 bytes .../bg-BG.lproj/Localizable.strings | 2 + .../ca-ES.lproj/Localizable.strings | 2 + .../cs-CZ.lproj/Localizable.strings | 2 + .../da.lproj/Localizable.strings | 2 + .../de.lproj/Localizable.strings | 2 + .../el-GR.lproj/Localizable.strings | 2 + .../en-GB.lproj/Localizable.strings | 2 + .../en.lproj/Localizable.strings | 1 + .../es-419.lproj/Localizable.strings | 2 + .../es.lproj/Localizable.strings | 2 + .../et-EE.lproj/Localizable.strings | 2 + .../fi.lproj/Localizable.strings | 2 + .../fil.lproj/Localizable.strings | 2 + .../fr-CA.lproj/Localizable.strings | 2 + .../fr.lproj/Localizable.strings | 2 + .../hr.lproj/Localizable.strings | 2 + .../hu.lproj/Localizable.strings | 2 + .../id.lproj/Localizable.strings | 2 + .../it.lproj/Localizable.strings | 2 + .../ja.lproj/Localizable.strings | 2 + .../ko.lproj/Localizable.strings | 2 + .../lt-LT.lproj/Localizable.strings | 2 + .../lv-LV.lproj/Localizable.strings | 2 + .../ms-MY.lproj/Localizable.strings | 2 + .../mt.lproj/Localizable.strings | 2 + .../nb.lproj/Localizable.strings | 2 + .../nl.lproj/Localizable.strings | 2 + .../nn-NO.lproj/Localizable.strings | 2 + .../pl-PL.lproj/Localizable.strings | 2 + .../pt-BR.lproj/Localizable.strings | 2 + .../pt-PT.lproj/Localizable.strings | 2 + .../ro-RO.lproj/Localizable.strings | 2 + .../ru.lproj/Localizable.strings | 2 + .../sk-SK.lproj/Localizable.strings | 2 + .../sl-SI.lproj/Localizable.strings | 2 + .../sv.lproj/Localizable.strings | 2 + .../tr.lproj/Localizable.strings | 2 + .../vi.lproj/Localizable.strings | 2 + .../zh-HK.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + .../API Bindings/APIPollingHelper.swift | 108 + .../Source/API Bindings/APIVersion.swift | 26 + .../FinancialConnectionsAPIClient.swift | 932 +++++ .../Models/BankAccountToken.swift | 40 + .../ConsumerSessionModels.swift | 65 + .../Models/FinancialConnectionsAccount.swift | 138 + .../FinancialConnectionsAuthSession.swift | 58 + .../FinancialConnectionsBulletPoint.swift | 20 + .../Models/FinancialConnectionsConsent.swift | 23 + ...FinancialConnectionsDataAccessNotice.swift | 27 + .../Models/FinancialConnectionsImage.swift | 13 + .../FinancialConnectionsInstitution.swift | 24 + ...tionsInstitutionSearchResultResource.swift | 13 + ...nancialConnectionsLegalDetailsNotice.swift | 27 + .../FinancialConnectionsLinkLoginPane.swift | 15 + ...FinancialConnectionsMixedOAuthParams.swift | 13 + ...ConnectionsNetworkedAccountsResponse.swift | 46 + ...ncialConnectionsNetworkingLinkSignup.swift | 21 + .../FinancialConnectionsOAuthPrepane.swift | 76 + .../FinancialConnectionsPartnerAccount.swift | 41 + ...ialConnectionsPaymentAccountResource.swift | 25 + ...inancialConnectionsPaymentMethodType.swift | 15 + .../Models/FinancialConnectionsSession.swift | 166 + .../FinancialConnectionsSessionManifest.swift | 248 ++ .../FinancialConnectionsSynchronize.swift | 26 + .../Models/FinancialConnectionsTheme.swift | 125 + .../FinancialConnectionsAnalyticsClient.swift | 221 ++ .../FinancialConnectionsSheetAnalytics.swift | 116 + .../Source/Common/ExperimentHelper.swift | 69 + ...ctionsCustomManualEntryRequiredError.swift | 10 + ...ncialConnectionsNavigationController.swift | 217 ++ .../Source/Common/FlowRouter.swift | 139 + .../Source/Common/HostController.swift | 244 ++ .../Source/Common/HostViewController.swift | 165 + .../Source/Common/LoadingView.swift | 104 + ...dalPresentationWrapperViewController.swift | 50 + .../Common/TestModeAutofillBannerView.swift | 163 + ...lConnectionsLinkedBankImplementation.swift | 34 + ...inancialConnectionsSDKImplementation.swift | 103 + ...nstantDebitsLinkedBankImplementation.swift | 27 + .../Source/FinancialConnectionsSheet.swift | 292 ++ .../FinancialConnectionsSheetError.swift | 43 + ...FinancialConnectionsEvent+Extensions.swift | 51 + .../Helpers/FinancialConnectionsFont.swift | 169 + .../Source/Helpers/Helpers.swift | 15 + .../Source/Helpers/Image.swift | 34 + .../Source/Helpers/Locale+Extensions.swift | 33 + .../NSAttributedString+Extensions.swift | 78 + .../Helpers/PaymentAccount+Extensions.swift | 20 + .../Source/Helpers/STPLocalizedString.swift | 16 + .../Source/Helpers/ScreenNativeScale.swift | 16 + .../Source/Helpers/String+Extensions.swift | 133 + .../Source/Helpers/String+Localized.swift | 35 + ...ipeFinancialConnectionsBundleLocator.swift | 18 + .../Source/Helpers/UIColor+Extensions.swift | 194 + .../Helpers/UIViewController+Extensions.swift | 47 + .../AccountPickerAccountLoadErrorView.swift | 157 + .../AccountPickerDataSource.swift | 147 + .../AccountPickerFooterView.swift | 109 + .../AccountPicker/AccountPickerHelpers.swift | 124 + ...ountPickerNoAccountEligibleErrorView.swift | 226 ++ .../AccountPickerSelectionListView.swift | 107 + .../AccountPickerSelectionView.swift | 63 + .../AccountPickerViewController.swift | 553 +++ .../Native/AccountPicker/CheckboxView.swift | 39 + .../RetrieveAccountsLoadingView.swift | 72 + .../AccountNumberRetrievalErrorView.swift | 120 + ...AttachLinkedPaymentAccountDataSource.swift | 63 + ...chLinkedPaymentAccountViewController.swift | 173 + .../Native/Consent/ConsentBodyView.swift | 149 + .../Native/Consent/ConsentDataSource.swift | 48 + .../Native/Consent/ConsentFooterView.swift | 142 + .../Native/Consent/ConsentLogoView.swift | 376 ++ .../Consent/ConsentViewController.swift | 194 + .../Source/Native/Error/ErrorDataSource.swift | 35 + .../Native/Error/ErrorViewController.swift | 221 ++ .../InstitutionCellView.swift | 119 + .../InstitutionDataSource.swift | 70 + .../InstitutionNoResultsView.swift | 133 + .../InstitutionPickerViewController.swift | 509 +++ .../InstitutionSearchBar.swift | 274 ++ .../InstitutionTableFooterView.swift | 129 + .../InstitutionTableLoadingView.swift | 129 + .../InstitutionTableView.swift | 412 +++ .../InstitutionTableViewCell.swift | 158 + .../AccountUpdateRequiredViewController.swift | 79 + .../LinkAccountPickerBodyView.swift | 221 ++ .../LinkAccountPickerDataSource.swift | 91 + .../LinkAccountPickerFooterView.swift | 98 + .../LinkAccountPickerLoadingView.swift | 35 + .../LinkAccountPickerNewAccountRowView.swift | 142 + .../LinkAccountPickerViewController.swift | 564 +++ .../LinkLogin/LinkLoginDataSource.swift | 111 + .../LinkLogin/LinkLoginViewController.swift | 216 ++ .../ManualEntry/ManualEntryDataSource.swift | 50 + .../ManualEntry/ManualEntryErrorView.swift | 30 + .../ManualEntry/ManualEntryFooterView.swift | 61 + .../ManualEntry/ManualEntryFormView.swift | 236 ++ .../ManualEntry/ManualEntryValidator.swift | 121 + .../ManualEntryViewController.swift | 172 + .../Source/Native/NativeFlowController.swift | 1425 ++++++++ .../Source/Native/NativeFlowDataManager.swift | 143 + .../NetworkingLinkLoginWarmupBodyView.swift | 110 + .../NetworkingLinkLoginWarmupDataSource.swift | 43 + ...workingLinkLoginWarmupViewController.swift | 151 + .../EmailTextField.swift | 217 ++ .../LinkSignupFormView.swift | 217 ++ .../NetworkingLinkSignupBodyView.swift | 151 + .../NetworkingLinkSignupDataSource.swift | 84 + .../NetworkingLinkSignupFooterView.swift | 164 + .../NetworkingLinkSignupViewController.swift | 332 ++ .../PhoneCountryCodePickerView.swift | 193 + .../PhoneCountryCodeSelectorView.swift | 206 ++ .../PhoneTextField.swift | 269 ++ ...orkingLinkStepUpVerificationBodyView.swift | 132 + ...kingLinkStepUpVerificationDataSource.swift | 85 + ...LinkStepUpVerificationViewController.swift | 283 ++ ...NetworkingLinkVerificationDataSource.swift | 124 + ...orkingLinkVerificationViewController.swift | 253 ++ ...kingSaveToLinkVerificationDataSource.swift | 114 + ...SaveToLinkVerificationViewController.swift | 236 ++ .../PartnerAuth/PartnerAuthDataSource.swift | 197 + .../PartnerAuthViewController.swift | 824 +++++ .../Native/PartnerAuth/PrepaneImageView.swift | 142 + .../Native/PartnerAuth/PrepaneViews.swift | 271 ++ .../ResetFlow/ResetFlowDataSource.swift | 40 + .../ResetFlow/ResetFlowViewController.swift | 72 + .../Shared/AccountPickerRowLabelView.swift | 95 + .../Native/Shared/AccountPickerRowView.swift | 276 ++ .../Native/Shared/AttributedLabel.swift | 89 + .../Native/Shared/AttributedTextView.swift | 245 ++ .../Native/Shared/AuthFlowHelpers.swift | 92 + .../Native/Shared/BulletPointLabelView.swift | 63 + .../Native/Shared/Button+Extensions.swift | 154 + .../CloseConfirmationViewController.swift | 79 + .../Source/Native/Shared/Constants.swift | 17 + .../DataAccessNoticeViewController.swift | 292 ++ .../Shared/FeedbackGeneratorAdapter.swift | 30 + .../Native/Shared/HitTestStackView.swift | 25 + .../Source/Native/Shared/HitTestView.swift | 25 + .../Native/Shared/InstitutionIconView.swift | 94 + .../LegalDetailsNoticeViewController.swift | 138 + .../Shared/MerchantDataAccessView.swift | 258 ++ .../NetworkingOTPDataSource.swift | 114 + .../NetworkingOTPView/NetworkingOTPView.swift | 288 ++ .../Source/Native/Shared/PaneLayoutView.swift | 230 ++ .../PaneLayoutView+Extensions.swift | 261 ++ .../Native/Shared/RoundedIconView.swift | 133 + .../Native/Shared/RoundedTextField.swift | 756 ++++ .../SFSafariViewController+Extensions.swift | 24 + .../Native/Shared/SheetViewController.swift | 584 +++ .../Source/Native/Shared/ShimmeringView.swift | 40 + .../Source/Native/Shared/SpinnerView.swift | 79 + .../Shared/TimeInterval+Extensions.swift | 16 + .../Native/Shared/UIImage+Extensions.swift | 27 + .../Shared/UIImageView+Extensions.swift | 79 + .../Shared/UITableView+Extensions.swift | 31 + .../UIViewController+KeyboardAvoiding.swift | 179 + .../Native/Success/SuccessDataSource.swift | 51 + .../Native/Success/SuccessFooterView.swift | 62 + .../Success/SuccessViewController.swift | 236 ++ .../TerminalError/TerminalErrorView.swift | 65 + .../TerminalErrorViewController.swift | 55 + .../Source/StripeCore+Import.swift | 9 + .../Web/AuthenticationSessionManager.swift | 156 + .../Source/Web/ContinueStateView.swift | 71 + .../FinancialConnectionsAccountFetcher.swift | 69 + .../FinancialConnectionsSessionFetcher.swift | 68 + ...cialConnectionsWebFlowViewController.swift | 418 +++ .../StripeFinancialConnections.h | 18 + .../APIPollingHelperTests.swift | 342 ++ .../AccountFetcherTests.swift | 142 + .../AccountPickerHelpersTests.swift | 25 + .../AuthFlowHelpersTests.swift | 30 + .../EmptyFinancialConnectionsAPIClient.swift | 213 ++ .../FinancialConnectionsAPIClientTests.swift | 55 + .../FinancialConnectionsAnalyticsTest.swift | 74 + .../FinancialConnectionsSessionTests.swift | 53 + .../FinancialConnectionsSheetTests.swift | 77 + .../FlowRouterTests.swift | 187 + .../Info.plist | 22 + .../ManualEntryValidatorTests.swift | 76 + .../MarkdownBoldAttributedStringTests.swift | 168 + ...alConnectionsSession_both_accounts_la.json | 204 ++ ...ncialConnectionsSession_only_accounts.json | 129 + ...lConnectionsSession_only_both_missing.json | 9 + .../FinancialConnectionsSession_only_la.json | 129 + .../SessionFetcherTests.swift | 108 + .../SoftLinkTests.swift | 19 + .../StringExtensionsTests.swift | 51 + StripeIdentity/README.md | 58 + .../StripeIdentity.xcodeproj/project.pbxproj | 1990 ++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/swiftpm/Package.resolved | 32 + .../xcschemes/StripeIdentity.xcscheme | 102 + .../Docs.docc/StripeIdentity.md | 3 + StripeIdentity/StripeIdentity/Info.plist | 22 + .../Resources/Images/icon_add@3x.png | Bin 0 -> 388 bytes .../Resources/Images/icon_camera@3x.png | Bin 0 -> 1186 bytes .../Images/icon_camera_classic@3x.png | Bin 0 -> 1184 bytes .../Resources/Images/icon_checkmark@3x.png | Bin 0 -> 518 bytes .../Resources/Images/icon_checkmark_92@3x.png | Bin 0 -> 2248 bytes .../Resources/Images/icon_clock@3x.png | Bin 0 -> 651 bytes .../Resources/Images/icon_cloud@3x.png | Bin 0 -> 806 bytes .../icon_create_identity_verification@3x.png | Bin 0 -> 1389 bytes .../Images/icon_dispute_protection@3x.png | Bin 0 -> 1111 bytes .../Resources/Images/icon_document@3x.png | Bin 0 -> 493 bytes .../Resources/Images/icon_ellipsis@3x.png | Bin 0 -> 567 bytes .../Resources/Images/icon_id_front@3x.png | Bin 0 -> 10488 bytes .../Resources/Images/icon_info@3x.png | Bin 0 -> 391 bytes .../Resources/Images/icon_lock@3x.png | Bin 0 -> 918 bytes .../Resources/Images/icon_moved@3x.png | Bin 0 -> 923 bytes .../Resources/Images/icon_phone@3x.png | Bin 0 -> 761 bytes .../Images/icon_selfie_warmup@3x.png | Bin 0 -> 14256 bytes .../Resources/Images/icon_wallet@3x.png | Bin 0 -> 524 bytes .../Resources/Images/icon_warning2@3x.png | Bin 0 -> 905 bytes .../Resources/Images/icon_warning@3x.png | Bin 0 -> 2170 bytes .../Resources/Images/icon_warning_92@3x.png | Bin 0 -> 3705 bytes .../bg-BG.lproj/Localizable.strings | 155 + .../ca-ES.lproj/Localizable.strings | 155 + .../cs-CZ.lproj/Localizable.strings | 155 + .../da.lproj/Localizable.strings | 155 + .../de.lproj/Localizable.strings | 155 + .../el-GR.lproj/Localizable.strings | 155 + .../en-GB.lproj/Localizable.strings | 155 + .../en.lproj/Localizable.strings | 241 ++ .../es-419.lproj/Localizable.strings | 155 + .../es.lproj/Localizable.strings | 155 + .../et-EE.lproj/Localizable.strings | 155 + .../fi.lproj/Localizable.strings | 155 + .../fil.lproj/Localizable.strings | 155 + .../fr-CA.lproj/Localizable.strings | 155 + .../fr.lproj/Localizable.strings | 155 + .../hr.lproj/Localizable.strings | 155 + .../hu.lproj/Localizable.strings | 155 + .../id.lproj/Localizable.strings | 155 + .../it.lproj/Localizable.strings | 155 + .../ja.lproj/Localizable.strings | 155 + .../ko.lproj/Localizable.strings | 155 + .../lt-LT.lproj/Localizable.strings | 155 + .../lv-LV.lproj/Localizable.strings | 155 + .../ms-MY.lproj/Localizable.strings | 155 + .../mt.lproj/Localizable.strings | 155 + .../nb.lproj/Localizable.strings | 155 + .../nl.lproj/Localizable.strings | 155 + .../nn-NO.lproj/Localizable.strings | 155 + .../pl-PL.lproj/Localizable.strings | 155 + .../pt-BR.lproj/Localizable.strings | 155 + .../pt-PT.lproj/Localizable.strings | 155 + .../ro-RO.lproj/Localizable.strings | 155 + .../ru.lproj/Localizable.strings | 155 + .../sk-SK.lproj/Localizable.strings | 155 + .../sl-SI.lproj/Localizable.strings | 155 + .../sv.lproj/Localizable.strings | 155 + .../tr.lproj/Localizable.strings | 155 + .../vi.lproj/Localizable.strings | 155 + .../zh-HK.lproj/Localizable.strings | 155 + .../zh-Hans.lproj/Localizable.strings | 155 + .../zh-Hant.lproj/Localizable.strings | 155 + .../API Bindings/DocumentScanner+API.swift | 45 + .../DocumentType+StripeIdentity.swift | 21 + .../API Bindings/DocumentUploader+API.swift | 65 + .../Source/API Bindings/FaceScanner+API.swift | 28 + .../API Bindings/IdentityAPIClient.swift | 175 + .../API Bindings/Models/DocumentType.swift | 16 + .../Models/TruncatedDecimal.swift | 61 + .../VerificationPage/VerificationPage.swift | 58 + .../VerificationPageFieldType.swift | 30 + .../VerificationPageIconType.swift | 53 + .../VerificationPageRequirements.swift | 17 + ...ficationPageStaticConsentLineContent.swift | 18 + ...nPageStaticContentBottomSheetContent.swift | 19 + ...eStaticContentBottomSheetLineContent.swift | 19 + ...ficationPageStaticContentConsentPage.swift | 22 + ...ageStaticContentCountryNotListedPage.swift | 19 + ...geStaticContentDocumentCaptureModels.swift | 19 + ...PageStaticContentDocumentCapturePage.swift | 31 + ...nPageStaticContentDocumentSelectPage.swift | 20 + ...ificationPageStaticContentExperiment.swift | 19 + ...ationPageStaticContentIndividualPage.swift | 22 + ...geStaticContentIndividualWelcomePage.swift | 21 + ...icationPageStaticContentPhoneOtpPage.swift | 23 + ...icationPageStaticContentSelfieModels.swift | 19 + ...ificationPageStaticContentSelfiePage.swift | 34 + ...erificationPageStaticContentTextPage.swift | 19 + .../VerificationPageData.swift | 42 + ...VerificationPageDataRequirementError.swift | 21 + .../VerificationPageDataRequirements.swift | 18 + .../RequiredInternationalAddress.swift | 20 + .../VerificationPageClearData.swift | 42 + .../VerificationPageCollectedData.swift | 151 + .../VerificationPageDataDob.swift | 17 + ...VerificationPageDataDocumentFileData.swift | 59 + .../VerificationPageDataFace.swift | 48 + .../VerificationPageDataIdNumber.swift | 17 + .../VerificationPageDataName.swift | 16 + .../VerificationPageDataPhone.swift | 16 + .../VerificationPageDataUpdate.swift | 18 + .../API Bindings/SelfieUploader+API.swift | 64 + .../Analytics/IdentityAnalyticsClient.swift | 534 +++ .../Categories/Array+StripeIdentity.swift | 33 + .../Categories/CGImage+StripeIdentity.swift | 119 + .../MLMultiArray+StripeIdentity.swift | 16 + .../Categories/NSAttributedString+HTML.swift | 273 ++ .../TimeInterval+StripeIdentity.swift | 21 + ...INavigationController+StripeIdentity.swift | 61 + .../VNBarcodeSymbology+StripeIdentity.swift | 158 + .../Source/Elements/IdNumberElement.swift | 98 + .../Elements/IdentityElementsFactory.swift | 196 + .../Elements/IdentityTextButtonElement.swift | 54 + .../Elements/IndividualFormElement.swift | 182 + .../Enums+CustomStringConvertible.swift | 7 + .../StripeIdentity/Source/Helpers/Image.swift | 38 + .../Source/Helpers/STPLocalizedString.swift | 16 + .../Source/Helpers/String+Localized.swift | 346 ++ .../Helpers/StripeIdentityBundleLocator.swift | 19 + .../Source/Helpers/UIImage+utils.swift | 22 + .../Source/IdentityVerificationSheet.swift | 283 ++ .../IdentityVerificationSheetError.swift | 65 + .../IdentityTopLevelDestination.swift | 20 + .../DocumentScanner/DocumentScanner.swift | 194 + .../DocumentScannerConfiguration.swift | 43 + .../DocumentScannerOutput.swift | 90 + .../FaceScanner/FaceCaptureData.swift | 53 + .../FaceScanner/FaceScanner.swift | 77 + .../FaceScannerConfiguration.swift | 33 + .../FaceScanner/FaceScannerOutput.swift | 85 + .../ImageScanner/ImageScanner.swift | 80 + .../ImageScanningConcurrencyManager.swift | 176 + .../ImageScanningSession.swift | 380 ++ .../ImageScanningSessionDelegate.swift | 275 ++ .../ImageUploaders/DocumentUploader.swift | 226 ++ .../IdentityImageUploader.swift | 216 ++ .../ImageUploaders/SelfieUploader.swift | 138 + .../VerificationSheetController.swift | 711 ++++ .../VerificationSheetFlowController.swift | 830 +++++ ...VerificationSheetFlowControllerError.swift | 78 + .../Detectors/BarcodeDetector.swift | 102 + .../Detectors/FaceDetector/FaceDetector.swift | 49 + .../FaceDetector/FaceDetectorOutput.swift | 188 + .../Detectors/IDDetector/IDDetector.swift | 48 + .../IDDetector/IDDetectorConstants.swift | 17 + .../IDDetector/IDDetectorOutput.swift | 240 ++ .../Detectors/LaplacianBlurDetector.swift | 91 + .../Detectors/MLDetectorConfiguration.swift | 19 + .../Detectors/MLDetectorMetricsTracker.swift | 94 + .../Detectors/MotionBlurDetector.swift | 99 + .../Detectors/VisionBasedDetector.swift | 142 + .../NativeComponents/DocumentSide.swift | 14 + .../IdentityDataCollecting.swift | 34 + .../IdentityFlowNavigationController.swift | 138 + .../Source/NativeComponents/IdentityUI.swift | 92 + .../ML/Helpers/MLModelLoader.swift | 137 + .../MLModelUnexpectedOutputError.swift | 69 + .../ML/Helpers/NonMaxSuppression.swift | 188 + .../ML/IdentityMLModelLoader.swift | 182 + .../BiometricConsentViewController.swift | 225 ++ .../BottomSheetViewController.swift | 69 + .../CountryNotListedViewController.swift | 105 + .../ViewControllers/DebugViewController.swift | 87 + ...ocumentCaptureViewController+Strings.swift | 97 + .../DocumentCaptureViewController.swift | 606 +++ ...mentFileUploadViewController+Strings.swift | 86 + .../DocumentFileUploadViewController.swift | 638 ++++ .../DocumentWarmupViewController.swift | 48 + .../ViewControllers/ErrorViewController.swift | 225 ++ .../IdentityFlowViewController.swift | 217 ++ .../IndividualViewController.swift | 101 + .../IndividualWelcomeViewController.swift | 90 + .../LoadingViewController.swift | 88 + .../PhoneOtpViewController.swift | 159 + .../SelfieCaptureViewController+Strings.swift | 32 + .../SelfieCaptureViewController.swift | 482 +++ .../SelfieWarmupViewController.swift | 41 + .../SuccessViewController.swift | 80 + .../UIViewController+SafariExtension.swift | 24 + .../Views/BottomAlignedLabel.swift | 144 + .../Views/BottomSheetView.swift | 226 ++ .../Views/CameraPreviewContainerView.swift | 116 + .../Views/CompleteOptionView.swift | 177 + .../Views/ContentCenteringScrollView.swift | 40 + .../NativeComponents/Views/DebugView.swift | 344 ++ .../DocumentCapture/AnimatedBorderView.swift | 171 + .../DocumentCapture/DocumentCaptureView.swift | 76 + .../DocumentScanningView.swift | 204 ++ .../InstructionalDocumentScanningView.swift | 96 + .../Views/DocumentWarmupView.swift | 98 + .../NativeComponents/Views/ErrorView.swift | 99 + .../Views/HeaderIconView.swift | 207 ++ .../NativeComponents/Views/HeaderView.swift | 141 + .../Views/IdentityFlowView.swift | 473 +++ .../Views/IdentityHTMLView/HTMLTextView.swift | 180 + .../HTMLViewWithIconLabels.swift | 227 ++ .../IdentityHTMLView/IconLabelHTMLView.swift | 143 + .../MultilineIconLabelHTMLView.swift | 70 + .../Views/InstructionListView.swift | 100 + .../Views/ListView/ListItemView.swift | 233 ++ .../Views/ListView/ListView.swift | 136 + .../NativeComponents/Views/PhoneOtpView.swift | 169 + .../Views/Selfie/SelfieCaptureView.swift | 106 + .../Views/Selfie/SelfieScanningView.swift | 470 +++ .../Views/Selfie/SelfieWarmupView.swift | 82 + .../Views/ShadowConfiguration.swift | 25 + .../Views/ShadowedCorneredImageView.swift | 80 + .../Source/StripeCore+Import.swift | 10 + .../Source/VerificationClientSecret.swift | 47 + .../Source/VerificationSheetAnalytics.swift | 87 + .../WebWrapper/VerificationFlowWebView.swift | 296 ++ .../VerificationFlowWebViewController.swift | 204 ++ .../WebWrapper/VerifyWebURLHelper.swift | 27 + .../StripeIdentity/StripeIdentity.h | 18 + .../Helpers/DocumentUploaderMock.swift | 57 + .../Helpers/IdentityAPIClientTestMock.swift | 148 + .../IdentityAnalyticsClientTestHelpers.swift | 51 + .../Helpers/IdentityMLModelLoaderMock.swift | 46 + .../Helpers/IdentityMockData.swift | 179 + .../Helpers/ImageScannerMock.swift | 49 + .../ImageScanningConcurrencyManagerMock.swift | 55 + .../MLDetectorMetricsTrackerMock.swift | 39 + .../Helpers/SnapshotTestMockData.swift | 31 + .../VerificationFlowResult+Equatable.swift | 27 + .../VerificationSheetControllerMock.swift | 201 + .../VerificationSheetFlowControllerMock.swift | 114 + StripeIdentity/StripeIdentityTests/Info.plist | 22 + .../Mock Photos/back_drivers_license.jpg | Bin 0 -> 1385618 bytes .../cgimage_stripeidentity_test.png | Bin 0 -> 318214 bytes .../Mock Photos/front_drivers_license.jpg | Bin 0 -> 1524408 bytes .../Mock Files/Mock Photos/header_icon.png | Bin 0 -> 1067 bytes .../VerificationPage_200.json | 239 ++ ...erificationPage_200_no_consent_header.json | 240 ++ .../VerificationPage_200_no_exp.json | 240 ++ .../VerificationPage_200_submitted.json | 240 ++ .../VerificationPage_200_testMode.json | 240 ++ .../VerificationPage_no_selfie.json | 219 ++ ...VerificationPage_require_live_capture.json | 240 ++ .../VerificationPage_type_address.json | 218 ++ ...ficationPage_type_doc_require_address.json | 219 ++ ...icationPage_type_doc_require_idNumber.json | 219 ++ ...type_doc_require_idNumber_and_address.json | 220 ++ .../VerificationPage_type_idNumber.json | 218 ++ .../VerificationPage_type_phone.json | 221 ++ .../VerificationPageData_200.json | 18 + .../VerificationPageData_no_errors.json | 11 + ...rificationPageData_no_errors_needback.json | 12 + .../VerificationPageData_submitted.json | 11 + ...ficationPageData_submitted_not_closed.json | 15 + .../StripeIdentityTests/Mock Files/mock.html | 8 + .../AnimatedBorderViewSnapshotTest.swift | 69 + ...ricConsentViewControllerSnapshotTest.swift | 40 + .../BottomSheetViewSnapshotTest.swift | 32 + .../CGImage_StripeIdentitySnapshotTest.png | Bin 0 -> 402622 bytes .../CGImage_StripeIdentitySnapshotTest.swift | 158 + .../DebugViewControllerSnapshotTest.swift | 20 + .../DocumentScanningViewSnapshotTest.swift | 71 + .../DocumentWarmupViewSnapshotTest.swift | 31 + .../ErrorViewControllerSnapshotTest.swift | 31 + .../Snapshot/ErrorViewSnapshotTest.swift | 55 + .../Snapshot/HeaderIconViewSnapshotTest.swift | 73 + .../Snapshot/HeaderViewSnapshotTest.swift | 135 + .../IdentityFlowViewSnapshotTest.swift | 110 + .../IdentityHTMLViewSnapshotTest.swift | 95 + ...IndividualViewControllerSnapshotTest.swift | 46 + ...ualWelcomeViewControllerSnapshotTest.swift | 27 + .../InstructionListViewSnapshotTest.swift | 71 + ...onalDocumentScanningViewSnapshotTest.swift | 67 + .../Snapshot/ListItemViewSnapshotTest.swift | 145 + .../Snapshot/ListViewSnapshotTest.swift | 102 + .../NSAttributedString_HTMLSnapshotTest.swift | 127 + .../PhoneOtpViewControllerSnapshotTest.swift | 49 + .../SelfieCaptureViewSnapshotTest.swift | 71 + .../SelfieScanningViewSnapshotTest.swift | 120 + .../SelfieWarmupViewSnapshotTest.swift | 22 + .../SuccessViewControllerSnapshotTest.swift | 30 + ...VerificationFlowWebViewSnapshotTests.swift | 88 + .../API Bindings/IdentityAPIClientTest.swift | 308 ++ .../API Bindings/TruncatedDecimalTest.swift | 69 + .../IdentityAnalyticsClientTest.swift | 65 + .../CGImage+StripeIdentityUnitTest.swift | 209 ++ .../IdentityElementsFactoryTest.swift | 104 + .../Unit/Elements/IndividualElementTest.swift | 104 + .../Coordinators/DocumentUploaderTest.swift | 527 +++ .../IdentityImageUploaderTest.swift | 269 ++ .../IdentityVerificationSheetTest.swift | 268 ++ .../VerificationSheetControllerTest.swift | 927 +++++ .../VerificationSheetFlowControllerTest.swift | 536 +++ .../BiometricConsentViewControllerTest.swift | 48 + .../DebugViewControllerTest.swift | 73 + .../DocumentCaptureViewControllerTest.swift | 904 +++++ ...DocumentFileUploadViewControllerTest.swift | 182 + .../DocumentWarmupViewControllerTest.swift | 45 + .../ErrorViewControllerTest.swift | 80 + ...IdentityFlowNavigationControllerTest.swift | 40 + .../IndividualViewControllerTest.swift | 41 + .../IndividualWelcomeViewControllerTest.swift | 37 + .../PhoneOtpViewControllerTest.swift | 100 + .../SelfieWarmupViewControllerTest.swift | 35 + .../Unit/VerificationClientSecretTest.swift | 89 + .../Unit/VerificationSheetAnalyticsTest.swift | 72 + ...erificationFlowWebViewControllerTest.swift | 73 + .../VerificationFlowWebViewTest.swift | 66 + StripePaymentSheet/README.md | 41 + .../project.pbxproj | 2196 +++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripePaymentSheet.xcscheme | 102 + .../Docs.docc/StripePaymentSheet.md | 3 + .../StripePaymentSheet/Info.plist | 22 + .../StripePaymentSheet/PrivacyInfo.xcprivacy | 45 + .../Resources/JSON/form_specs.json | 1147 ++++++ .../bg-BG.lproj/Localizable.strings | 193 + .../ca-ES.lproj/Localizable.strings | 193 + .../cs-CZ.lproj/Localizable.strings | 193 + .../da.lproj/Localizable.strings | 193 + .../de.lproj/Localizable.strings | 193 + .../el-GR.lproj/Localizable.strings | 193 + .../en-GB.lproj/Localizable.strings | 193 + .../en.lproj/Localizable.strings | 295 ++ .../es-419.lproj/Localizable.strings | 193 + .../es.lproj/Localizable.strings | 193 + .../et-EE.lproj/Localizable.strings | 193 + .../fi.lproj/Localizable.strings | 193 + .../fil.lproj/Localizable.strings | 193 + .../fr-CA.lproj/Localizable.strings | 193 + .../fr.lproj/Localizable.strings | 193 + .../hr.lproj/Localizable.strings | 193 + .../hu.lproj/Localizable.strings | 193 + .../id.lproj/Localizable.strings | 193 + .../it.lproj/Localizable.strings | 193 + .../ja.lproj/Localizable.strings | 193 + .../ko.lproj/Localizable.strings | 193 + .../lt-LT.lproj/Localizable.strings | 193 + .../lv-LV.lproj/Localizable.strings | 193 + .../ms-MY.lproj/Localizable.strings | 193 + .../mt.lproj/Localizable.strings | 193 + .../nb.lproj/Localizable.strings | 193 + .../nl.lproj/Localizable.strings | 193 + .../nn-NO.lproj/Localizable.strings | 193 + .../pl-PL.lproj/Localizable.strings | 193 + .../pt-BR.lproj/Localizable.strings | 193 + .../pt-PT.lproj/Localizable.strings | 193 + .../ro-RO.lproj/Localizable.strings | 193 + .../ru.lproj/Localizable.strings | 193 + .../sk-SK.lproj/Localizable.strings | 193 + .../sl-SI.lproj/Localizable.strings | 193 + .../sv.lproj/Localizable.strings | 193 + .../tk.lproj/Localizable.strings | 0 .../tr.lproj/Localizable.strings | 193 + .../vi.lproj/Localizable.strings | 193 + .../zh-HK.lproj/Localizable.strings | 193 + .../zh-Hans.lproj/Localizable.strings | 193 + .../zh-Hant.lproj/Localizable.strings | 193 + .../Carousel/Contents.json | 6 + .../carousel_applepay.imageset/Contents.json | 22 + .../applePayDark.svg | 9 + .../carousel_applepay.svg | 9 + .../carousel_card_amex.imageset/Contents.json | 22 + .../americanExpressDark.svg | 3 + .../americanExpressLight.svg | 3 + .../Contents.json | 22 + .../cartesBancairesDark.svg | 3 + .../cartesBancairesLight.svg | 20 + .../Contents.json | 22 + .../dinersClubDark.svg | 3 + .../dinersClubLight.svg | 3 + .../Contents.json | 22 + .../discoverDark.svg | 19 + .../discoverLight.svg | 19 + .../carousel_card_jcb.imageset/Contents.json | 22 + .../carousel_card_jcb.imageset/jcbDark.svg | 7 + .../carousel_card_jcb.imageset/jcbLight.svg | 7 + .../Contents.json | 22 + .../mastercardDark.svg | 10 + .../mastercardLight.svg | 6 + .../Contents.json | 22 + .../unionPayDark.svg | 18 + .../unionPayLight.svg | 6 + .../Contents.json | 22 + .../unknownDark.svg | 3 + .../unknownLight.svg | 7 + .../carousel_card_visa.imageset/Contents.json | 22 + .../carousel_card_visa.imageset/visaDark.svg | 3 + .../carousel_card_visa.imageset/visaLight.svg | 3 + .../carousel_sepa.imageset/Contents.json | 22 + .../carousel_sepa.imageset/carousel_sepa.svg | 6 + .../carousel_sepa.imageset/sepaDark.svg | 4 + .../StripePaymentSheet.xcassets/Contents.json | 6 + .../Link/Contents.json | 6 + .../Link/InstantDebitIcons/Contents.json | 6 + .../bank_icon_boa.imageset/BrandIcon--boa.svg | 16 + .../bank_icon_boa.imageset/Contents.json | 12 + .../BrandIcon--capitalone.svg | 12 + .../Contents.json | 12 + .../BrandIcon--citibank.svg | 1 + .../bank_icon_citibank.imageset/Contents.json | 12 + .../BrandIcon--compass.svg | 1 + .../bank_icon_compass.imageset/Contents.json | 12 + .../bank_icon_default.imageset/Contents.json | 23 + .../bank_icon_default.png | Bin 0 -> 1034 bytes .../bank_icon_default@2x.png | Bin 0 -> 1579 bytes .../bank_icon_default@3x.png | Bin 0 -> 541 bytes .../BrandIcon--morganchase.svg | 1 + .../Contents.json | 12 + .../BrandIcon--navyfederal.svg | 1 + .../bank_icon_nfcu.imageset/Contents.json | 12 + .../bank_icon_pnc.imageset/BrandIcon--pnc.svg | 1 + .../bank_icon_pnc.imageset/Contents.json | 12 + .../BrandIcon--stripe.svg | 1 + .../bank_icon_stripe.imageset/Contents.json | 12 + .../BrandIcon--suntrust.svg | 1 + .../bank_icon_suntrust.imageset/Contents.json | 12 + .../bank_icon_svb.imageset/BrandIcon--svb.svg | 1 + .../bank_icon_svb.imageset/Contents.json | 12 + .../bank_icon_td.imageset/BrandIcon--td.svg | 1 + .../bank_icon_td.imageset/Contents.json | 12 + .../BrandIcon--usaa.svg | 1 + .../bank_icon_usaa.imageset/Contents.json | 12 + .../BrandIcon--usbank.svg | 1 + .../bank_icon_usbank.imageset/Contents.json | 12 + .../BrandIcon--wellsfargo.svg | 1 + .../Contents.json | 12 + .../Link/link_icon.imageset/Contents.json | 12 + .../Link/link_icon.imageset/Link.svg | 4 + .../Link/link_logo.imageset/Contents.json | 22 + .../Link/link_logo.imageset/Dark.svg | 9 + .../Link/link_logo.imageset/Light.svg | 9 + .../Link/link_logo_bw.imageset/Contents.json | 12 + .../link_logo_bw.imageset/link_logo_bw.svg | 9 + .../link_logo_knockout.imageset/Contents.json | 22 + .../link_logo_knockout.svg | 8 + .../link_logo_knockout_dark.svg | 8 + .../Mandates/Contents.json | 6 + .../bacsdd_logo.imageset/Contents.json | 15 + .../bacsdd_logo.imageset/bacsdd_logo.svg | 3 + .../PaymentMethods/Contents.json | 6 + .../icon-pm-affirm.imageset/Contents.json | 12 + .../icon-pm-affirm.svg | 4 + .../icon-pm-afterpay.imageset/Contents.json | 12 + .../icon-pm-afterpay-mod.svg | 11 + .../icon-pm-alipay.imageset/Contents.json | 12 + .../icon-pm-alipay-mod.svg | 11 + .../Contents.json | 12 + .../icon-pm-bank.svg | 3 + .../icon-pm-bancontact.imageset/Contents.json | 12 + .../icon-pm-bancontact-mod.svg | 15 + .../icon-pm-bank.imageset/Contents.json | 12 + .../icon-pm-bank.imageset/icon-pm-bank.svg | 3 + .../icon-pm-blik.imageset/Contents.json | 12 + .../icon-pm-blik-color.svg | 16 + .../icon-pm-boleto.imageset/Contents.json | 12 + .../icon-pm-boleto.svg | 3 + .../icon-pm-card.imageset/Contents.json | 12 + .../icon-pm-card.imageset/icon-pm-card.svg | 3 + .../icon-pm-cashapp.imageset/Contents.json | 12 + .../icon-pm-cash-app-color.png | Bin 0 -> 1680 bytes .../icon-pm-eps.imageset/Contents.json | 12 + .../icon-pm-eps.imageset/icon-pm-eps-mod.svg | 12 + .../icon-pm-giropay.imageset/Contents.json | 12 + .../icon-pm-giropay-mod.svg | 14 + .../icon-pm-ideal.imageset/Contents.json | 12 + .../icon-pm-ideal-mod.svg | 14 + .../icon-pm-klarna.imageset/Contents.json | 12 + .../icon-pm-klarna-mod.svg | 11 + .../icon-pm-konbini.imageset/Contents.json | 12 + .../icon-pm-konbini-color.svg | 10 + .../icon-pm-oxxo.imageset/Contents.json | 12 + .../icon-pm-oxxo-mod.svg | 7 + .../icon-pm-p24.imageset/Contents.json | 12 + .../icon-pm-p24.imageset/icon-pm-p24-mod.svg | 13 + .../icon-pm-paypal.imageset/Contents.json | 22 + .../icon-pm-paypal-color-dark.svg | 5 + .../icon-pm-paypal-color.svg | 12 + .../icon-pm-revolutpay.imageset/Contents.json | 22 + .../icon-pm-revolutpay@3x.png | Bin 0 -> 7905 bytes .../icon-pm-revolutpay_dark@3x.png | Bin 0 -> 7222 bytes .../icon-pm-sepa.imageset/Contents.json | 12 + .../icon-pm-sepa-mod.svg | 1 + .../icon-pm-swish.imageset/Contents.json | 12 + .../icon-pm-swish@3x.png | Bin 0 -> 10001 bytes .../icon-pm-upi.imageset/Contents.json | 12 + .../icon-pm-upi-color.svg | 31 + .../PaymentSheet/Contents.json | 6 + .../icon_checkmark.imageset/Contents.json | 12 + .../icon_checkmark.pdf | Bin 0 -> 1147 bytes .../icon_chevron_left.imageset/Contents.json | 12 + .../icon_chevron_left.pdf | Bin 0 -> 1147 bytes .../Contents.json | 12 + .../chevronLeft.svg | 3 + .../icon_chevron_right.imageset/Contents.json | 12 + .../chevronRight.svg | 3 + .../icon_edit.imageset/Contents.json | 12 + .../icon_edit.imageset/icon_edit.pdf | Bin 0 -> 1086 bytes .../icon_lock.imageset/Contents.json | 12 + .../icon_lock.imageset/icon_lock.pdf | Bin 0 -> 1753 bytes .../icon_plus.imageset/Contents.json | 12 + .../icon_plus.imageset/icon_plus.pdf | Bin 0 -> 1411 bytes .../icon_x.imageset/Contents.json | 12 + .../PaymentSheet/icon_x.imageset/icon_x.pdf | Bin 0 -> 1399 bytes .../icon_x_standalone.imageset/Contents.json | 12 + .../icon_x_standalone.imageset/cancel.svg | 3 + .../affirm_mark.imageset/Contents.json | 22 + .../affirm_mark.imageset/affirm_mark.svg | 15 + .../affirm_mark.imageset/affirm_mark_dark.svg | 15 + .../afterpay_icon_info.imageset/Contents.json | 12 + .../afterpay_icon_info.imageset/info.svg | 10 + .../afterpay_mark.imageset/Contents.json | 22 + .../afterpay_mark.imageset/afterpay_mark.svg | 17 + .../afterpay_mark_dark.svg | 17 + .../apple_pay_mark.imageset/Contents.json | 12 + .../apple_pay_mark.imageset/applePay.svg | 1 + .../clearpay_mark.imageset/Contents.json | 22 + .../clearpay_mark.imageset/vector (15).svg | 14 + .../clearpay_mark.imageset/vector (16).svg | 14 + .../polling_error_icon.imageset/Contents.json | 23 + .../polling_error_icon.png | Bin 0 -> 992 bytes .../polling_error_icon@2x.png | Bin 0 -> 2425 bytes .../polling_error_icon@3x.png | Bin 0 -> 1131 bytes .../PaymentSheetAnalyticsHelper.swift | 324 ++ .../STPAnalyticsClient+Address.swift | 87 + .../STPAnalyticsClient+CustomerSheet.swift | 55 + .../Analytics/STPAnalyticsClient+LUXE.swift | 29 + .../Source/Categories/Data+SHA256.swift | 26 + .../NSAttributedString+Stripe.swift | 17 + .../STPPaymentMethod+PaymentSheet.swift | 49 + .../STPPaymentMethodParams+PaymentSheet.swift | 39 + .../Source/Categories/String+Localized.swift | 372 ++ .../String+StripePaymentSheet.swift | 19 + .../UIApplication+StripePaymentSheet.swift | 24 + .../Source/Helpers/BoolReference.swift | 20 + .../Source/Helpers/DownloadManager.swift | 165 + .../Source/Helpers/Images.swift | 85 + .../Source/Helpers/IntentStatusPoller.swift | 103 + .../Helpers/PaymentSheetLinkAccount.swift | 309 ++ .../Source/Helpers/STPCameraView.swift | 71 + .../Source/Helpers/STPCardScanner.swift | 491 +++ .../Source/Helpers/STPImageLibrary.swift | 116 + .../Source/Helpers/STPLocalizedString.swift | 13 + .../Source/Helpers/STPStringUtils.swift | 206 ++ .../Helpers/SavedPaymentMethodManager.swift | 75 + .../Helpers/StripePaymentSheet+Exports.swift | 12 + .../StripePaymentSheetBundleLocator.swift | 19 + .../Link/ConsumerSession+LookupResponse.swift | 64 + .../Link/ConsumerSession+PublishableKey.swift | 24 + .../API Bindings/Link/ConsumerSession.swift | 153 + .../API Bindings/Link/PaymentDetails.swift | 36 + .../Link/PaymentDetailsShareResponse.swift | 33 + .../API Bindings/Link/STPAPIClient+Link.swift | 383 ++ .../Link/VerificationSession.swift | 43 + .../STPAPIClient+PaymentSheet.swift | 201 + .../API Bindings/VO/CardExpiryDate.swift | 85 + .../CustomerSession.swift | 100 + .../ElementsCustomer.swift | 42 + .../ExternalPaymentMethod.swift | 40 + .../STPCardBrandChoice.swift | 58 + .../STPElementsSession.swift | 249 ++ .../Internal/Basic UI/SeparatorLabel.swift | 131 + ...sOnlyFinancialConnectionsAuthManager.swift | 179 + .../PayWithLinkWebController.swift | 196 + .../LinkInlineSignupElement.swift | 66 + ...LinkInlineSignupView-CheckboxElement.swift | 70 + .../InlineSignup/LinkInlineSignupView.swift | 273 ++ .../Link/Elements/LinkEmailElement.swift | 114 + .../Link/Extensions/FormElement+Link.swift | 90 + .../Link/Extensions/Intent+Link.swift | 67 + .../Extensions/STPAnalyticsClient+Link.swift | 121 + .../Link/Extensions/UIColor+Link.swift | 109 + .../Source/Internal/Link/LinkUI.swift | 145 + .../Link/Services/LinkAccountService.swift | 86 + .../Link/Utils/LinkPopupURLParser.swift | 72 + .../Link/Utils/LinkURLGenerator.swift | 129 + .../Internal/Link/Utils/Locale+Link.swift | 35 + .../Link/Utils/OperationDebouncer.swift | 63 + .../Verification/LinkAccountContext.swift | 47 + .../Link/Verification/LinkCookieKey.swift | 15 + .../LinkInlineSignupViewModel.swift | 357 ++ .../Link/Views/LinkLegalTermsView.swift | 159 + .../Link/Views/LinkMoreInfoView.swift | 60 + .../AddressViewController+Configuration.swift | 166 + .../AddressViewController.swift | 448 +++ .../BottomSheet/BottomSheetPresentable.swift | 14 + .../BottomSheetPresentationAnimator.swift | 124 + .../BottomSheetPresentationController.swift | 171 + .../BottomSheetTransitioningDelegate.swift | 65 + .../UIViewController+BottomSheet.swift | 59 + .../CustomerAdapter/CustomerAdapter.swift | 255 ++ .../CustomerPaymentOption.swift | 66 + .../UserDefaults+StripePaymentSheet.swift | 50 + .../CustomerSessionAdapter.swift | 135 + ...stomerAddPaymentMethodViewController.swift | 355 ++ ...ymentMethodsCollectionViewController.swift | 558 +++ ...merSavedPaymentMethodsViewController.swift | 930 +++++ .../CustomerSheet/CustomerSheet+API.swift | 74 + ...tomerSheet+PaymentMethodAvailability.swift | 89 + .../CustomerSheet/CustomerSheet+SwiftUI.swift | 114 + .../CustomerSheet/CustomerSheet.swift | 367 ++ .../CustomerSheetConfiguration.swift | 138 + .../CustomerSheetDataSource.swift | 195 + .../CustomerSheet/CustomerSheetError.swift | 17 + .../CardSection/CVCRecollectionElement.swift | 135 + .../CardSection/CardSectionElement.swift | 280 ++ .../CardSectionWithScannerView.swift | 104 + .../Elements/CardSection/HostedSurface.swift | 68 + .../Elements/ConnectionsElement.swift | 26 + .../LinkEnabledPaymentMethodElement.swift | 112 + .../PaymentMethodElement.swift | 66 + .../PaymentMethodElementWrapper.swift | 145 + .../Elements/SimpleMandateElement.swift | 37 + .../TextField/TextFieldElement+Card.swift | 319 ++ .../TextField/TextFieldElement+IBAN.swift | 184 + .../PaymentSheet/Error+PaymentSheet.swift | 32 + .../Source/PaymentSheet/Intent.swift | 121 + .../PaymentSheet/IntentConfirmParams.swift | 202 + .../Link/LinkPaymentController.swift | 364 ++ .../Link/PayWithLinkController.swift | 79 + .../Link/PaymentSheet-LinkConfirmOption.swift | 73 + .../AddPaymentMethodViewController.swift | 207 ++ .../PaymentMethodTypeCollectionView.swift | 340 ++ .../WalletHeaderView.swift | 198 + .../PaymentSheet/PaymentMethodType.swift | 418 +++ .../PaymentSheet/PaymentOption+Images.swift | 227 ++ .../PaymentSheet/PaymentSheet+API.swift | 664 ++++ .../PaymentSheet+DeferredAPI.swift | 167 + ...ymentSheet+PaymentMethodAvailability.swift | 224 ++ .../PaymentSheet/PaymentSheet+SwiftUI.swift | 442 +++ .../Source/PaymentSheet/PaymentSheet.swift | 414 +++ .../PaymentSheet/PaymentSheetAppearance.swift | 214 ++ .../PaymentSheetConfiguration.swift | 529 +++ .../PaymentSheetDeferredValidator.swift | 95 + .../PaymentSheet/PaymentSheetError.swift | 196 + .../PaymentSheetFlowController.swift | 580 +++ .../FormSpec/FormSpec.swift | 191 + .../FormSpec/FormSpecProvider.swift | 96 + .../PaymentSheetFormFactory+BLIK.swift | 45 + .../PaymentSheetFormFactory+Boleto.swift | 42 + .../PaymentSheetFormFactory+Card.swift | 114 + .../PaymentSheetFormFactory+FormSpec.swift | 235 ++ .../PaymentSheetFormFactory+Mandates.swift | 73 + .../PaymentSheetFormFactory+OXXO.swift | 31 + .../PaymentSheetFormFactory+UPI.swift | 53 + .../PaymentSheetFormFactory.swift | 953 +++++ .../PaymentSheetFormFactoryConfig.swift | 96 + .../PaymentSheetIntentConfiguration.swift | 192 + .../PaymentSheet/PaymentSheetLoader.swift | 318 ++ .../STPAnalyticsClient+PaymentSheet.swift | 118 + .../STPApplePayContext+PaymentSheet.swift | 237 ++ ...ntShippingDetailsParams+PaymentSheet.swift | 31 + .../SavedPaymentMethodCollectionView.swift | 436 +++ .../SavedPaymentOptionsViewController.swift | 749 ++++ .../SavedPaymentMethodRowButton.swift | 153 + ...calSavedPaymentMethodsViewController.swift | 351 ++ .../USBankAccount/BankAccountInfoView.swift | 160 + .../InstantDebitsPaymentMethodElement.swift | 206 ++ .../USBankAccountPaymentMethodElement.swift | 251 ++ .../Vertical Main Screen/FormHeaderView.swift | 72 + .../RightAccessoryButton.swift | 160 + .../Vertical Main Screen/RowButton.swift | 278 ++ ...ticalPaymentMethodListViewController.swift | 241 ++ .../AutoComplete/AddressSearchResult.swift | 49 + .../AutoCompleteViewController.swift | 281 ++ .../AutoComplete/String+AutoComplete.swift | 83 + .../BottomSheet3DS2ViewController.swift | 102 + .../BottomSheetViewController.swift | 550 +++ ...ReconfirmationVerticalViewController.swift | 74 + .../CVCReconfirmationViewController.swift | 181 + .../LoadingViewController.swift | 79 + .../PaymentMethodFormViewController.swift | 409 +++ ...entSheetFlowControllerViewController.swift | 617 ++++ .../PaymentSheetVerticalViewController.swift | 838 +++++ .../PaymentSheetViewController.swift | 640 ++++ .../PollingViewController.swift | 359 ++ .../ViewControllers/PollingViewModel.swift | 60 + .../SepaMandateViewController.swift | 80 + .../UpdateCardViewController.swift | 259 ++ .../PaymentSheet/Views/AUBECSMandate.swift | 74 + .../PaymentSheet/Views/AffirmCopyLabel.swift | 47 + .../Views/AfterpayPriceBreakdownView.swift | 179 + .../Views/Appearance+FontScaling.swift | 42 + .../Views/BacsDDMandateView.swift | 175 + .../CVCPaymentMethodInformationView.swift | 109 + .../Views/CVCRecollectionView.swift | 89 + .../PaymentSheet/Views/CardScanButton.swift | 32 + .../PaymentSheet/Views/CardScanningView.swift | 262 ++ .../PaymentSheet/Views/CircularButton.swift | 158 + .../PaymentSheet/Views/ConfirmButton.swift | 663 ++++ .../Views/ManualEntryButton.swift | 38 + .../Views/PayWithLinkButton.swift | 496 +++ .../Views/PaymentMethodTypeImageView.swift | 58 + .../Views/PaymentSheetUIKitAdditions.swift | 174 + .../Views/RotatingCardBrandsView.swift | 206 ++ .../Views/ShadowedRoundedRectangleView.swift | 79 + .../Views/SheetNavigationBar.swift | 183 + .../Views/SheetNavigationButton.swift | 48 + .../Views/SimpleMandateTextView.swift | 75 + .../PaymentSheet/Views/TestModeView.swift | 53 + .../StripePaymentSheet/StripePaymentSheet.h | 18 + .../StripePaymentSheetTests/Info.plist | 22 + ...entMethodViewControllerSnapshotTests.swift | 45 + .../AddressViewControllerSnapshotTests.swift | 132 + .../BacsDDMandateViewSnapshotTests.swift | 29 + ...MethodsCollectionViewControllerTests.swift | 312 ++ .../CustomerSheetConfigurationTests.swift | 63 + ...rSheetPaymentMethodAvailabilityTests.swift | 155 + .../CustomerSheet/CustomerSheetTests.swift | 278 ++ .../CustomerSheet_ConfirmFlowTests.swift | 468 +++ .../CustomerSheetSnapshotTests.swift | 695 ++++ .../PaymentSheet/DownloadManagerTest.swift | 231 ++ .../PaymentSheet/Elements+TestHelpers.swift | 65 + .../FlowControllerStateTests.swift | 48 + .../IntentConfirmParamsTest.swift | 161 + .../PaymentSheet/IntentStatusPollerTest.swift | 105 + .../Link/LinkPopupURLParserTests.swift | 69 + .../Link/LinkURLGeneratorTests.swift | 97 + .../PaymentSheet/LinkStubs.swift | 55 + .../PaymentMethodFormViewControllerTest.swift | 121 + ...entMethodMessagingViewFunctionalTest.swift | 85 + ...mentMethodMessagingViewSnapshotTests.swift | 103 + .../PaymentMethodRowButtonSnapshotTests.swift | 54 + .../PaymentSheet/PaymentSheet+APITest.swift | 1173 ++++++ ...mentSheet+DashboardConfirmParamsTest.swift | 323 ++ .../PaymentSheet+DeferredAPITest.swift | 51 + .../PaymentSheetAddressTests.swift | 311 ++ .../PaymentSheetAnalyticsHelperTest.swift | 287 ++ .../PaymentSheetConfigurationTests.swift | 130 + .../PaymentSheetDeferredValidatorTests.swift | 77 + .../PaymentSheet/PaymentSheetErrorTest.swift | 18 + ...ymentSheetExternalPaymentMethodTests.swift | 182 + ...ontrollerViewControllerSnapshotTests.swift | 121 + .../PaymentSheetFormFactorySnapshotTest.swift | 428 +++ .../PaymentSheetFormFactoryTest.swift | 1944 ++++++++++ .../PaymentSheetGDPRConfirmFlowTests.swift | 662 ++++ .../PaymentSheet/PaymentSheetImageTests.swift | 24 + .../PaymentSheetLPMConfirmFlowTests.swift | 900 +++++ .../PaymentSheetLinkAccountTests.swift | 69 + .../PaymentSheetLoaderStubbedTest.swift | 429 +++ .../PaymentSheet/PaymentSheetLoaderTest.swift | 348 ++ .../PaymentSheetPaymentMethodTypeTest.swift | 556 +++ .../PaymentSheetSnapshotTests.swift | 1314 +++++++ ...etVerticalViewControllerSnapshotTest.swift | 320 ++ ...ymentSheetVerticalViewControllerTest.swift | 85 + ...mentSheetViewControllerSnapshotTests.swift | 84 + .../PaymentSheet/PollingViewTests.swift | 24 + .../RightAccessoryButtonTest.swift | 83 + .../STPAPIClient+PaymentSheetTest.swift | 129 + .../STPApplePayContext+PaymentSheetTest.swift | 93 + .../PaymentSheet/STPCardBrandChoiceTest.swift | 29 + .../PaymentSheet/STPElementsSessionTest.swift | 265 ++ .../STPFixtures+PaymentSheet.swift | 276 ++ .../SavedPaymentMethodManagerTest.swift | 209 ++ ...ntOptionsViewControllerSnapshotTests.swift | 51 + ...vedPaymentOptionsViewControllerTests.swift | 320 ++ ...epaMandateViewControllerSnapshotTest.swift | 32 + .../PaymentSheet/Stubbed/StubbedBackend.swift | 85 + .../TextFieldElement+CardTest.swift | 366 ++ ...pdateCardViewControllerSnapshotTests.swift | 60 + .../UserDefaults+StripePaymentSheetTest.swift | 36 + ...efaults+StripePaymentSheetTest.swift.plist | Bin 0 -> 93 bytes ...MethodListViewControllerSnapshotTest.swift | 82 + ...lPaymentMethodListViewControllerTest.swift | 129 + ...ntMethodsViewControllerSnapshotTests.swift | 62 + ...vedPaymentMethodsViewControllerTests.swift | 216 ++ .../MockFiles/consumers_lookup_200.json | 12 + .../Resources/MockFiles/customers_200.json | 10 + ...erSessionCustomerSheetWithSavedPM_200.json | 163 + ...ions_customerSessionCustomerSheet_200.json | 115 + ...nts_sessions_link_signup_disabled_200.json | 107 + .../elements_sessions_paymentMethod_200.json | 82 + ...ments_sessions_paymentMethod_link_200.json | 106 + ...ts_sessions_paymentMethod_savedPM_200.json | 111 + .../MockFiles/payment_intents_200.json | 53 + .../MockFiles/saved_payment_methods_200.json | 8 + .../saved_payment_methods_withCard_200.json | 53 + .../saved_payment_methods_withSepa_200.json | 39 + .../saved_payment_methods_withUSBank_200.json | 45 + .../MockFiles/setup_intents_200.json | 72 + ...STPAnalyticsClient+PaymentSheetTests.swift | 24 + StripePayments/README.md | 38 + .../StripePayments.xcodeproj/project.pbxproj | 2391 ++++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripePayments.xcscheme | 95 + .../Docs.docc/StripePayments.md | 3 + StripePayments/StripePayments/Info.plist | 22 + .../bg-BG.lproj/Localizable.strings | 63 + .../ca-ES.lproj/Localizable.strings | 63 + .../cs-CZ.lproj/Localizable.strings | 63 + .../da.lproj/Localizable.strings | 63 + .../de.lproj/Localizable.strings | 63 + .../el-GR.lproj/Localizable.strings | 63 + .../en-GB.lproj/Localizable.strings | 63 + .../en.lproj/Localizable.strings | 96 + .../es-419.lproj/Localizable.strings | 63 + .../es.lproj/Localizable.strings | 63 + .../et-EE.lproj/Localizable.strings | 63 + .../fi.lproj/Localizable.strings | 63 + .../fil.lproj/Localizable.strings | 63 + .../fr-CA.lproj/Localizable.strings | 63 + .../fr.lproj/Localizable.strings | 63 + .../hr.lproj/Localizable.strings | 63 + .../hu.lproj/Localizable.strings | 63 + .../id.lproj/Localizable.strings | 63 + .../it.lproj/Localizable.strings | 63 + .../ja.lproj/Localizable.strings | 63 + .../ko.lproj/Localizable.strings | 63 + .../lt-LT.lproj/Localizable.strings | 63 + .../lv-LV.lproj/Localizable.strings | 63 + .../ms-MY.lproj/Localizable.strings | 63 + .../mt.lproj/Localizable.strings | 63 + .../nb.lproj/Localizable.strings | 63 + .../nl.lproj/Localizable.strings | 63 + .../nn-NO.lproj/Localizable.strings | 63 + .../pl-PL.lproj/Localizable.strings | 63 + .../pt-BR.lproj/Localizable.strings | 63 + .../pt-PT.lproj/Localizable.strings | 63 + .../ro-RO.lproj/Localizable.strings | 63 + .../ru.lproj/Localizable.strings | 63 + .../sk-SK.lproj/Localizable.strings | 63 + .../sl-SI.lproj/Localizable.strings | 63 + .../sv.lproj/Localizable.strings | 63 + .../tk.lproj/Localizable.strings | 0 .../tr.lproj/Localizable.strings | 63 + .../vi.lproj/Localizable.strings | 63 + .../zh-HK.lproj/Localizable.strings | 63 + .../zh-Hans.lproj/Localizable.strings | 63 + .../zh-Hant.lproj/Localizable.strings | 63 + .../StripeAPI+Deprecated.swift | 169 + .../StripeApplePay+Import.swift | 9 + .../StripeCore+Import.swift | 10 + .../Models/ACH/LinkAccountSession.swift | 51 + .../ACH/STPCollectBankAccountParams.swift | 55 + .../STPConfirmAlipayOptions.swift | 57 + .../STPConfirmBLIKOptions.swift | 54 + .../STPConfirmCardOptions.swift | 53 + .../STPConfirmKonbiniOptions.swift | 27 + .../STPConfirmPaymentMethodOptions.swift | 71 + .../STPConfirmUSBankAccountOptions.swift | 47 + .../STPConfirmWeChatPayOptions.swift | 60 + .../PaymentIntents/STPPaymentIntent.swift | 350 ++ .../STPPaymentIntentAction.swift | 16 + .../STPPaymentIntentActionRedirectToURL.swift | 19 + .../STPPaymentIntentEnums.swift | 135 + .../STPPaymentIntentLastPaymentError.swift | 174 + .../STPPaymentIntentParams.swift | 288 ++ .../STPPaymentIntentShippingDetails.swift | 90 + ...PPaymentIntentShippingDetailsAddress.swift | 99 + ...ntIntentShippingDetailsAddressParams.swift | 116 + ...TPPaymentIntentShippingDetailsParams.swift | 110 + .../STPPaymentIntentSourceAction.swift | 17 + ...ntIntentSourceActionAuthorizeWithURL.swift | 21 + .../PaymentMethods/STPPaymentMethod.swift | 363 ++ .../STPPaymentMethodAddress.swift | 143 + .../STPPaymentMethodAllowRedisplay.swift | 31 + .../STPPaymentMethodBillingDetails.swift | 133 + .../STPPaymentMethodEnums.swift | 323 ++ .../STPPaymentMethodParams.swift | 1373 +++++++ .../STPPaymentMethodUpdateParams.swift | 48 + .../Types/STPPaymentMethodAUBECSDebit.swift | 64 + .../STPPaymentMethodAUBECSDebitParams.swift | 31 + .../Types/STPPaymentMethodAffirm.swift | 44 + .../Types/STPPaymentMethodAffirmParams.swift | 23 + .../STPPaymentMethodAfterpayClearpay.swift | 43 + ...PPaymentMethodAfterpayClearpayParams.swift | 24 + .../Types/STPPaymentMethodAlipay.swift | 43 + .../Types/STPPaymentMethodAlipayParams.swift | 27 + .../Types/STPPaymentMethodAlma.swift | 42 + .../Types/STPPaymentMethodAlmaParams.swift | 24 + .../Types/STPPaymentMethodAmazonPay.swift | 42 + .../STPPaymentMethodAmazonPayParams.swift | 24 + .../Types/STPPaymentMethodBLIK.swift | 44 + .../Types/STPPaymentMethodBLIKParams.swift | 27 + .../Types/STPPaymentMethodBacsDebit.swift | 54 + .../STPPaymentMethodBacsDebitParams.swift | 33 + .../Types/STPPaymentMethodBancontact.swift | 41 + .../STPPaymentMethodBancontactParams.swift | 22 + .../Types/STPPaymentMethodBillie.swift | 42 + .../Types/STPPaymentMethodBillieParams.swift | 24 + .../Types/STPPaymentMethodBoleto.swift | 56 + .../Types/STPPaymentMethodBoletoParams.swift | 49 + .../Types/STPPaymentMethodCard.swift | 103 + .../Types/STPPaymentMethodCardChecks.swift | 102 + .../Types/STPPaymentMethodCardNetworks.swift | 53 + .../STPPaymentMethodCardNetworksParams.swift | 50 + .../Types/STPPaymentMethodCardParams.swift | 129 + .../Types/STPPaymentMethodCardPresent.swift | 38 + .../Types/STPPaymentMethodCardWallet.swift | 114 + ...STPPaymentMethodCardWalletMasterpass.swift | 66 + ...PPaymentMethodCardWalletVisaCheckout.swift | 47 + .../Types/STPPaymentMethodCashApp.swift | 43 + .../Types/STPPaymentMethodCashAppParams.swift | 24 + .../Types/STPPaymentMethodEPS.swift | 42 + .../Types/STPPaymentMethodEPSParams.swift | 22 + .../Types/STPPaymentMethodFPX.swift | 46 + .../Types/STPPaymentMethodFPXParams.swift | 47 + .../Types/STPPaymentMethodGiropay.swift | 41 + .../Types/STPPaymentMethodGiropayParams.swift | 22 + .../Types/STPPaymentMethodGrabPay.swift | 40 + .../Types/STPPaymentMethodGrabPayParams.swift | 23 + .../Types/STPPaymentMethodKlarna.swift | 45 + .../Types/STPPaymentMethodKlarnaParams.swift | 24 + .../Types/STPPaymentMethodLink.swift | 42 + .../Types/STPPaymentMethodLinkParams.swift | 35 + .../Types/STPPaymentMethodMobilePay.swift | 42 + .../STPPaymentMethodMobilePayParams.swift | 24 + .../Types/STPPaymentMethodMultibanco.swift | 44 + .../STPPaymentMethodMultibancoParams.swift | 23 + .../Types/STPPaymentMethodNetBanking.swift | 50 + .../STPPaymentMethodNetBankingParams.swift | 29 + .../Types/STPPaymentMethodOXXO.swift | 40 + .../Types/STPPaymentMethodOXXOParams.swift | 25 + .../Types/STPPaymentMethodPayPal.swift | 42 + .../Types/STPPaymentMethodPayPalParams.swift | 24 + .../Types/STPPaymentMethodPrzelewy24.swift | 42 + .../STPPaymentMethodPrzelewy24Params.swift | 24 + .../Types/STPPaymentMethodRevolutPay.swift | 40 + .../STPPaymentMethodRevolutPayParams.swift | 21 + .../Types/STPPaymentMethodSEPADebit.swift | 69 + .../STPPaymentMethodSEPADebitParams.swift | 30 + .../Types/STPPaymentMethodSatispay.swift | 42 + .../STPPaymentMethodSatispayParams.swift | 24 + .../Types/STPPaymentMethodSofort.swift | 48 + .../Types/STPPaymentMethodSofortParams.swift | 29 + .../Types/STPPaymentMethodSunbit.swift | 42 + .../Types/STPPaymentMethodSunbitParams.swift | 24 + .../Types/STPPaymentMethodSwish.swift | 39 + .../Types/STPPaymentMethodSwishParams.swift | 24 + .../STPPaymentMethodThreeDSecureUsage.swift | 54 + .../Types/STPPaymentMethodUPI.swift | 50 + .../Types/STPPaymentMethodUPIParams.swift | 29 + .../Types/STPPaymentMethodUSBankAccount.swift | 213 ++ .../STPPaymentMethodUSBankAccountParams.swift | 80 + .../Types/STPPaymentMethodWeChatPay.swift | 42 + .../STPPaymentMethodWeChatPayParams.swift | 24 + .../Types/STPPaymentMethodiDEAL.swift | 51 + .../Types/STPPaymentMethodiDEALParams.swift | 30 + .../Models/STPAPIResponseDecodable.swift | 23 + .../API Bindings/Models/STPAddress.swift | 305 ++ .../API Bindings/Models/STPCardBrand.swift | 103 + .../Models/STPConnectAccountAddress.swift | 82 + .../STPConnectAccountCompanyParams.swift | 107 + .../STPConnectAccountIndividualParams.swift | 246 ++ .../Models/STPConnectAccountParams.swift | 173 + .../API Bindings/Models/STPContactField.swift | 39 + .../API Bindings/Models/STPCustomer.swift | 268 ++ .../API Bindings/Models/STPFPXBankBrand.swift | 333 ++ .../Source/API Bindings/Models/STPFile.swift | 139 + .../Models/STPFormEncodable.swift | 23 + .../Models/STPIssuingCardPin.swift | 55 + .../API Bindings/Models/STPRadarSession.swift | 37 + .../Source/API Bindings/Models/STPToken.swift | 160 + .../API Bindings/Models/STPiDEALBank.swift | 59 + .../Models/SetupIntents/STPSetupIntent.swift | 247 ++ .../STPSetupIntentConfirmParams.swift | 164 + .../SetupIntents/STPSetupIntentEnums.swift | 41 + .../STPSetupIntentLastSetupError.swift | 140 + .../Models/Shared/LinkSettings.swift | 82 + .../Models/Shared/STPIntentAction.swift | 483 +++ .../STPIntentActionAlipayHandleRedirect.swift | 126 + .../STPIntentActionBoletoDisplayDetails.swift | 68 + .../STPIntentActionCashAppRedirectToApp.swift | 65 + ...STPIntentActionKonbiniDisplayDetails.swift | 45 + ...IntentActionMultibancoDisplayDetails.swift | 73 + .../STPIntentActionOXXODisplayDetails.swift | 71 + .../STPIntentActionPayNowDisplayQrCode.swift | 63 + ...TPIntentActionPromptPayDisplayQrCode.swift | 63 + .../Shared/STPIntentActionRedirectToURL.swift | 104 + .../STPIntentActionSwishHandleRedirect.swift | 63 + ...PIntentActionVerifyWithMicrodeposits.swift | 88 + ...TPIntentActionWeChatPayRedirectToApp.swift | 64 + .../STPMandateCustomerAcceptanceParams.swift | 70 + .../Models/Shared/STPMandateDataParams.swift | 52 + .../Shared/STPMandateOnlineParams.swift | 68 + .../Shared/STPPaymentMethodOptions.swift | 157 + .../Models/Sources/STPSource.swift | 331 ++ .../Models/Sources/STPSourceEnums.swift | 100 + .../Models/Sources/STPSourceOwner.swift | 61 + .../Models/Sources/STPSourceParams.swift | 975 +++++ .../Models/Sources/STPSourceProtocol.swift | 18 + .../Models/Sources/STPSourceReceiver.swift | 64 + .../Models/Sources/STPSourceRedirect.swift | 113 + .../Sources/STPSourceVerification.swift | 100 + .../Models/Sources/Types/STPBankAccount.swift | 202 + .../Sources/Types/STPBankAccountParams.swift | 128 + .../Models/Sources/Types/STPCard.swift | 340 ++ .../Models/Sources/Types/STPCardParams.swift | 212 ++ .../Sources/Types/STPKlarnaLineItem.swift | 54 + .../Sources/Types/STPSourceCardDetails.swift | 135 + .../Types/STPSourceKlarnaDetails.swift | 50 + .../Types/STPSourceSEPADebitDetails.swift | 74 + .../Types/STPSourceWeChatPayDetails.swift | 44 + .../API Bindings/STPAPIClient+ApplePay.swift | 211 ++ .../STPAPIClient+LinkAccountSession.swift | 161 + .../API Bindings/STPAPIClient+Payments.swift | 1373 +++++++ .../API Bindings/STPAPIClient+Radar.swift | 51 + .../API Bindings/STPRedirectContext.swift | 626 ++++ .../Captcha/DispatchQueue+Throttle.swift | 84 + .../Source/Captcha/HCaptcha.swift | 271 ++ .../Source/Captcha/HCaptchaConfig.swift | 307 ++ .../Source/Captcha/HCaptchaDebugInfo.swift | 100 + .../Source/Captcha/HCaptchaDecoder.swift | 167 + .../Source/Captcha/HCaptchaError.swift | 91 + .../Source/Captcha/HCaptchaEvent.swift | 53 + .../Source/Captcha/HCaptchaHtml.swift | 165 + .../Source/Captcha/HCaptchaLog.swift | 68 + .../Source/Captcha/HCaptchaResult.swift | 55 + .../Source/Captcha/HCaptchaURLOpener.swift | 39 + ...aWebViewManager+WKNavigationDelegate.swift | 46 + .../Captcha/HCaptchaWebViewManager.swift | 394 ++ .../Source/Captcha/String+Dict.swift | 32 + .../Enums+CustomStringConvertible.swift | 877 +++++ .../Source/Helpers/STPBINController.swift | 431 +++ .../Helpers/STPBankAccountCollector.swift | 696 ++++ .../Source/Helpers/STPBlocks.swift | 122 + .../Source/Helpers/STPCardValidator.swift | 517 +++ .../Source/Helpers/STPLocalizedString.swift | 16 + .../STPPaymentConfirmation+SwiftUI.swift | 146 + .../Helpers/StripePayments+Export.swift | 10 + .../Helpers/StripePaymentsBundleLocator.swift | 19 + .../Internal/API Bindings/APIRequest.swift | 199 + .../STP3DS2AuthenticateResponse.swift | 81 + .../API Bindings/STPEmptyStripeResponse.swift | 30 + .../API Bindings/STPFormEncoder.swift | 97 + .../STPIntentActionUseStripeSDK.swift | 224 ++ .../STPInternalAPIResponseDecodable.swift | 16 + .../STPPaymentMethodListDeserializer.swift | 44 + .../API Bindings/STPSourcePoller.swift | 232 ++ .../Analytics/Analytic+Payments.swift | 57 + .../STPAnalyticsClient+Payments.swift | 382 ++ .../Internal/Categories/NSArray+Stripe.swift | 38 + .../NSDecimalNumber+Stripe_Currency.swift | 57 + .../Categories/NSDictionary+Stripe.swift | 131 + .../Internal/Categories/NSString+Stripe.swift | 73 + .../STPAPIClient+PaymentsCore.swift | 23 + .../Helpers/ConnectionsSDKAvailability.swift | 80 + .../STPPaymentHandlerActionParams.swift | 190 + ...STPAnalyticsClient+STPPaymentHandler.swift | 119 + .../STPAuthenticationContext.swift | 36 + .../PaymentHandler/STPPaymentHandler.swift | 2503 +++++++++++++ .../STPThreeDSButtonCustomization.swift | 126 + .../STPThreeDSCustomizationSettings.swift | 44 + .../STPThreeDSFooterCustomization.swift | 88 + .../STPThreeDSLabelCustomization.swift | 67 + ...STPThreeDSNavigationBarCustomization.swift | 101 + .../STPThreeDSSelectionCustomization.swift | 72 + .../STPThreeDSTextFieldCustomization.swift | 94 + .../STPThreeDSUICustomization.swift | 193 + .../StripePayments/StripePayments.h | 18 + .../StripePaymentsObjcTestUtils/Info.plist | 22 + .../Resources/Mock Files/3DSSource.json | 57 + .../Resources/Mock Files/AlipaySource.json | 34 + .../Mock Files/ApplePayPaymentMethod.json | 30 + .../Mock Files/BacsDebitPaymentMethod.json | 28 + .../Mock Files/BancontactSource.json | 38 + .../Resources/Mock Files/BankAccount.json | 18 + .../Resources/Mock Files/Card.json | 26 + .../Mock Files/CardPaymentMethod.json | 66 + .../Resources/Mock Files/CardSource.json | 33 + .../Resources/Mock Files/Customer.json | 26 + .../Resources/Mock Files/EPSSource.json | 34 + .../Resources/Mock Files/ElementsSession.json | 196 + .../Resources/Mock Files/EphemeralKey.json | 14 + .../Resources/Mock Files/FileUpload.json | 10 + .../Resources/Mock Files/GiropaySource.json | 36 + .../Mock Files/MultibancoSource.json | 42 + .../Resources/Mock Files/P24Source.json | 33 + .../Resources/Mock Files/PaymentIntent.json | 82 + .../Mock Files/SEPADebitPaymentMethod.json | 30 + .../Resources/Mock Files/SEPADebitSource.json | 38 + .../Resources/Mock Files/SetupIntent.json | 71 + .../Resources/Mock Files/SofortSource.json | 39 + .../USBankAccountPaymentMethod.json | 31 + .../Resources/Mock Files/WeChatPaySource.json | 40 + .../Resources/Mock Files/iDEALSource.json | 32 + .../StripePaymentsObjcTestUtils/STPFixtures.h | 215 ++ .../StripePaymentsObjcTestUtils/STPFixtures.m | 350 ++ .../STPTestUtils.h | 49 + .../STPTestUtils.m | 73 + .../STPTestingAPIClient.h | 92 + .../STPTestingAPIClient.m | 178 + .../SWHttpTrafficRecorder.h | 211 ++ .../SWHttpTrafficRecorder.m | 540 +++ .../StripePaymentsObjcTestUtils.h | 19 + .../AppDelegate.swift | 32 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../StripePaymentsTestHostApp/Info.plist | 29 + .../SceneDelegate.swift | 49 + .../ViewController.swift | 12 + .../StripePaymentsTestUtils/Info.plist | 22 + .../test429Backoff/0000_get_status_429.tail | 11 + .../test429Backoff/0001_get_status_429.tail | 11 + .../test429Backoff/0002_get_status_429.tail | 11 + .../test429NoBackoff/0000_get_status_429.tail | 11 + .../testDelete/0000_delete_delete.tail | 40 + .../APIRequestTest/testGet/0000_get_get.tail | 31 + .../testPost/0000_post_post.tail | 43 + .../0000_get_bearer.tail | 15 + ...000_post_v1_consumers_sessions_lookup.tail | 109 + ...000_post_v1_consumers_sessions_lookup.tail | 41 + ...00_post_v1_consumers_accounts_sign_up.tail | 56 + ...001_post_v1_consumers_payment_details.tail | 82 + ...02_post_v1_consumers_sessions_log_out.tail | 47 + ...003_post_v1_consumers_payment_details.tail | 36 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2BFY0qyl6XeWRzP5gQiw.tail | 45 + ...seti_1PiS2BFY0qyl6XeWRzP5gQiw_confirm.tail | 95 + ...intents_seti_1PiS2BFY0qyl6XeWRzP5gQiw.tail | 45 + .../0005_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2EFY0qyl6XeW9Fvr0HAM.tail | 45 + ...seti_1PiS2EFY0qyl6XeW9Fvr0HAM_confirm.tail | 95 + ...intents_seti_1PiS2EFY0qyl6XeW9Fvr0HAM.tail | 45 + .../0005_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2KFY0qyl6XeWaSCEpeqZ.tail | 45 + ...seti_1PiS2KFY0qyl6XeWaSCEpeqZ_confirm.tail | 95 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2IKG6vc7r7YCvXuia3LK.tail | 45 + ...seti_1PiS2IKG6vc7r7YCvXuia3LK_confirm.tail | 95 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2NFY0qyl6XeWBZGcxTE5.tail | 45 + ...seti_1PiS2NFY0qyl6XeWBZGcxTE5_confirm.tail | 81 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2PFY0qyl6XeWUpOO5bwC.tail | 50 + ...qyl6XeWUpOO5bwC_link_account_sessions.tail | 58 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2OFY0qyl6XeWfFtnj0kz.tail | 50 + ...qyl6XeWfFtnj0kz_link_account_sessions.tail | 58 + .../0000_get_content.tail | 23 + .../0000_get_content.tail | 23 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2RFY0qyl6XeW0Li38Ol8.tail | 64 + ...s_pi_3PiS2RFY0qyl6XeW0Li38Ol8_confirm.tail | 194 + .../0004_post_v1_payment_methods.tail | 77 + .../0005_post_create_payment_intent.tail | 18 + .../0006_post_v1_payment_methods.tail | 77 + .../0007_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2VFY0qyl6XeWcg242nPq.tail | 45 + ...seti_1PiS2VFY0qyl6XeWcg242nPq_confirm.tail | 160 + .../0010_post_v1_payment_methods.tail | 77 + .../0011_post_create_setup_intent.tail | 18 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2YFY0qyl6XeW025NyBhO.tail | 64 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2ZFY0qyl6XeW0HOLru0s.tail | 110 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2bFY0qyl6XeW0QGoXuQ6.tail | 64 + ...s_pi_3PiS2bFY0qyl6XeW0QGoXuQ6_confirm.tail | 194 + .../0003_post_create_payment_intent.tail | 18 + .../0004_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2eFY0qyl6XeWjBlYwG8E.tail | 45 + ...seti_1PiS2eFY0qyl6XeWjBlYwG8E_confirm.tail | 160 + .../0007_post_create_setup_intent.tail | 18 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2hFY0qyl6XeW59ADP6Ul.tail | 91 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2iFY0qyl6XeWqVyArXRB.tail | 45 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2jFY0qyl6XeW0whXhj0e.tail | 64 + ...s_pi_3PiS2jFY0qyl6XeW0whXhj0e_confirm.tail | 127 + ...t_intents_pi_3PiS2jFY0qyl6XeW0whXhj0e.tail | 77 + .../0005_post_v1_payment_methods.tail | 77 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2mFY0qyl6XeW1b5TXiWF.tail | 110 + ...t_intents_pi_3PiS2mFY0qyl6XeW1b5TXiWF.tail | 64 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2oFY0qyl6XeW1oMcHfVh.tail | 64 + ...s_pi_3PiS2oFY0qyl6XeW1oMcHfVh_confirm.tail | 132 + ...t_intents_pi_3PiS2oFY0qyl6XeW1oMcHfVh.tail | 82 + .../0005_post_v1_payment_methods.tail | 77 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2rFY0qyl6XeW05Fedut8.tail | 115 + ...t_intents_pi_3PiS2rFY0qyl6XeW05Fedut8.tail | 69 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2tFY0qyl6XeW05MpbWg1.tail | 64 + ...s_pi_3PiS2tFY0qyl6XeW05MpbWg1_confirm.tail | 127 + ...t_intents_pi_3PiS2tFY0qyl6XeW05MpbWg1.tail | 77 + .../0005_post_v1_payment_methods.tail | 77 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS2vFY0qyl6XeW0HK2Qn1f.tail | 110 + ...t_intents_pi_3PiS2vFY0qyl6XeW0HK2Qn1f.tail | 64 + .../0009_post_v1_payment_methods.tail | 77 + .../0010_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS2xFY0qyl6XeW0RJBh9ro.tail | 45 + ...seti_1PiS2xFY0qyl6XeW0RJBh9ro_confirm.tail | 95 + ...intents_seti_1PiS2xFY0qyl6XeW0RJBh9ro.tail | 45 + .../0014_post_v1_payment_methods.tail | 77 + .../0015_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS30FY0qyl6XeW4HMbAKPF.tail | 91 + ...intents_seti_1PiS30FY0qyl6XeW4HMbAKPF.tail | 45 + .../0000_get_v1_elements_sessions.tail | 324 ++ .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS32FY0qyl6XeW1HcyyvIm.tail | 65 + ...s_pi_3PiS32FY0qyl6XeW1HcyyvIm_confirm.tail | 128 + .../0000_get_v1_elements_sessions.tail | 324 ++ .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS35FY0qyl6XeW1YNPLCbq.tail | 111 + .../0000_post_create_payment_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 582 +++ ...s_pi_3PiS36FY0qyl6XeW0l85ktJx_confirm.tail | 130 + ...t_intents_pi_3PiS36FY0qyl6XeW0l85ktJx.tail | 80 + .../0000_post_create_payment_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 388 ++ ...s_pi_3PiS38FY0qyl6XeW0OAa7B47_confirm.tail | 127 + ...t_intents_pi_3PiS38FY0qyl6XeW0OAa7B47.tail | 77 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3FFY0qyl6XeW0zpYxa1Z.tail | 64 + ...s_pi_3PiS3FFY0qyl6XeW0zpYxa1Z_confirm.tail | 127 + .../0003_post_v1_payment_methods.tail | 77 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3IFY0qyl6XeW1R8mJEdf.tail | 64 + ...s_pi_3PiS3IFY0qyl6XeW1R8mJEdf_confirm.tail | 127 + .../0007_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3KFY0qyl6XeW0m1XmnUK.tail | 64 + ...s_pi_3PiS3KFY0qyl6XeW0m1XmnUK_confirm.tail | 132 + .../0010_post_v1_payment_methods.tail | 77 + .../0011_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3MFY0qyl6XeW011ApMz7.tail | 64 + ...s_pi_3PiS3MFY0qyl6XeW011ApMz7_confirm.tail | 132 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3BFY0qyl6XeW06c19ynF.tail | 64 + ...s_pi_3PiS3BFY0qyl6XeW06c19ynF_confirm.tail | 127 + .../0003_post_v1_payment_methods.tail | 77 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS3DFY0qyl6XeW0CVkYoHP.tail | 64 + ...s_pi_3PiS3DFY0qyl6XeW0CVkYoHP_confirm.tail | 127 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS3OFY0qyl6XeWoJKyoMjz.tail | 45 + ...seti_1PiS3OFY0qyl6XeWoJKyoMjz_confirm.tail | 95 + .../0003_post_v1_payment_methods.tail | 77 + .../0004_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS3QFY0qyl6XeWA06BiwA7.tail | 45 + ...seti_1PiS3QFY0qyl6XeWA06BiwA7_confirm.tail | 95 + .../0000_get_v1_elements_sessions.tail | 351 ++ .../0001_get_v1_elements_sessions.tail | 351 ++ .../0002_get_v1_elements_sessions.tail | 359 ++ .../0000_get_v1_elements_sessions.tail | 351 ++ .../0001_get_v1_elements_sessions.tail | 34 + .../0002_get_v1_elements_sessions.tail | 351 ++ .../0000_get_v1_elements_sessions.tail | 351 ++ .../0001_get_v1_elements_sessions.tail | 359 ++ .../0002_get_v1_elements_sessions.tail | 351 ++ .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLiFY0qyl6XeW1WQQJxTb.tail | 64 + ...s_pi_3PjWLiFY0qyl6XeW1WQQJxTb_confirm.tail | 119 + ...t_intents_pi_3PjWLiFY0qyl6XeW1WQQJxTb.tail | 69 + .../0006_get_v1_payment_methods.tail | 81 + .../0007_post_create_ephemeral_key.tail | 18 + .../0008_post_v1_payment_methods.tail | 77 + .../0009_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLlFY0qyl6XeW0LAeRYcp.tail | 64 + ...s_pi_3PjWLlFY0qyl6XeW0LAeRYcp_confirm.tail | 114 + ...t_intents_pi_3PjWLlFY0qyl6XeW0LAeRYcp.tail | 64 + .../0013_get_v1_payment_methods.tail | 34 + .../0014_post_create_ephemeral_key.tail | 18 + .../0015_post_v1_payment_methods.tail | 77 + .../0016_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLoFY0qyl6XeW0iKpD6M5.tail | 64 + ...s_pi_3PjWLoFY0qyl6XeW0iKpD6M5_confirm.tail | 114 + ...t_intents_pi_3PjWLoFY0qyl6XeW0iKpD6M5.tail | 64 + .../0020_get_v1_payment_methods.tail | 34 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLHFY0qyl6XeW0b92TYtH.tail | 64 + ...s_pi_3PjWLHFY0qyl6XeW0b92TYtH_confirm.tail | 119 + ...t_intents_pi_3PjWLHFY0qyl6XeW0b92TYtH.tail | 69 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLKFY0qyl6XeW1gjmiaLT.tail | 64 + ...s_pi_3PjWLKFY0qyl6XeW1gjmiaLT_confirm.tail | 114 + ...t_intents_pi_3PjWLKFY0qyl6XeW1gjmiaLT.tail | 64 + .../0011_get_v1_payment_methods.tail | 34 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWLNFY0qyl6XeW0WKDQbyd.tail | 64 + ...s_pi_3PjWLNFY0qyl6XeW0WKDQbyd_confirm.tail | 114 + ...t_intents_pi_3PjWLNFY0qyl6XeW0WKDQbyd.tail | 64 + .../0017_get_v1_payment_methods.tail | 34 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5WFY0qyl6XeW0TEoZrzq.tail | 64 + ...s_pi_3PiS5WFY0qyl6XeW0TEoZrzq_confirm.tail | 119 + ...t_intents_pi_3PiS5WFY0qyl6XeW0TEoZrzq.tail | 69 + .../0006_get_v1_payment_methods.tail | 81 + .../0007_post_create_ephemeral_key.tail | 18 + .../0008_post_v1_payment_methods.tail | 77 + .../0009_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5ZFY0qyl6XeW0IRPongV.tail | 64 + ...s_pi_3PiS5ZFY0qyl6XeW0IRPongV_confirm.tail | 114 + ...t_intents_pi_3PiS5ZFY0qyl6XeW0IRPongV.tail | 64 + .../0013_get_v1_payment_methods.tail | 81 + .../0014_post_create_ephemeral_key.tail | 18 + .../0015_post_v1_payment_methods.tail | 77 + .../0016_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5dFY0qyl6XeW13nb2Yi8.tail | 64 + ...s_pi_3PiS5dFY0qyl6XeW13nb2Yi8_confirm.tail | 114 + ...t_intents_pi_3PiS5dFY0qyl6XeW13nb2Yi8.tail | 64 + .../0020_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5gFY0qyl6XeW1RMB0Ifn.tail | 115 + ...t_intents_pi_3PiS5gFY0qyl6XeW1RMB0Ifn.tail | 69 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5jFY0qyl6XeW1anpvcKh.tail | 110 + ...t_intents_pi_3PiS5jFY0qyl6XeW1anpvcKh.tail | 64 + .../0011_get_v1_payment_methods.tail | 81 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_v1_payment_methods.tail | 77 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5mFY0qyl6XeW05mcfsYr.tail | 110 + ...t_intents_pi_3PiS5mFY0qyl6XeW05mcfsYr.tail | 64 + .../0017_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5pFY0qyl6XeW0JjoWPFh.tail | 64 + ...s_pi_3PiS5pFY0qyl6XeW0JjoWPFh_confirm.tail | 119 + ...t_intents_pi_3PiS5pFY0qyl6XeW0JjoWPFh.tail | 69 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5tFY0qyl6XeW1GiWT7BV.tail | 64 + ...s_pi_3PiS5tFY0qyl6XeW1GiWT7BV_confirm.tail | 114 + ...t_intents_pi_3PiS5tFY0qyl6XeW1GiWT7BV.tail | 64 + .../0011_get_v1_payment_methods.tail | 81 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS5wFY0qyl6XeW1iavV4Xv.tail | 64 + ...s_pi_3PiS5wFY0qyl6XeW1iavV4Xv_confirm.tail | 114 + ...t_intents_pi_3PiS5wFY0qyl6XeW1iavV4Xv.tail | 64 + .../0017_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWM4FY0qyl6XeW1p4g32Lz.tail | 115 + ...t_intents_pi_3PjWM4FY0qyl6XeW1p4g32Lz.tail | 69 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWM7FY0qyl6XeW0bfoI51q.tail | 110 + ...t_intents_pi_3PjWM7FY0qyl6XeW0bfoI51q.tail | 64 + .../0011_get_v1_payment_methods.tail | 34 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_v1_payment_methods.tail | 77 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PjWMAFY0qyl6XeW1hqdkyKh.tail | 110 + ...t_intents_pi_3PjWMAFY0qyl6XeW1hqdkyKh.tail | 64 + .../0017_get_v1_payment_methods.tail | 34 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS5zFY0qyl6XeW7VSdzoRF.tail | 45 + ...seti_1PiS5zFY0qyl6XeW7VSdzoRF_confirm.tail | 95 + ...intents_seti_1PiS5zFY0qyl6XeW7VSdzoRF.tail | 45 + .../0006_get_v1_payment_methods.tail | 81 + .../0007_post_create_ephemeral_key.tail | 18 + .../0008_post_v1_payment_methods.tail | 77 + .../0009_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS63FY0qyl6XeW1r1MusJ5.tail | 45 + ...seti_1PiS63FY0qyl6XeW1r1MusJ5_confirm.tail | 95 + ...intents_seti_1PiS63FY0qyl6XeW1r1MusJ5.tail | 45 + .../0013_get_v1_payment_methods.tail | 81 + .../0014_post_create_ephemeral_key.tail | 18 + .../0015_post_v1_payment_methods.tail | 77 + .../0016_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS66FY0qyl6XeW9NpSVdmL.tail | 45 + ...seti_1PiS66FY0qyl6XeW9NpSVdmL_confirm.tail | 95 + ...intents_seti_1PiS66FY0qyl6XeW9NpSVdmL.tail | 45 + .../0020_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + .../0002_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS69FY0qyl6XeWf9hJqA5J.tail | 91 + ...intents_seti_1PiS69FY0qyl6XeWf9hJqA5J.tail | 45 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS6CFY0qyl6XeWwl1fntB3.tail | 91 + ...intents_seti_1PiS6CFY0qyl6XeWwl1fntB3.tail | 45 + .../0011_get_v1_payment_methods.tail | 81 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_v1_payment_methods.tail | 77 + .../0014_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS6FFY0qyl6XeWeiWitQvy.tail | 91 + ...intents_seti_1PiS6FFY0qyl6XeWeiWitQvy.tail | 45 + .../0017_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS6IFY0qyl6XeWWT82QhxK.tail | 45 + ...seti_1PiS6IFY0qyl6XeWWT82QhxK_confirm.tail | 95 + ...intents_seti_1PiS6IFY0qyl6XeWWT82QhxK.tail | 45 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS6LFY0qyl6XeWfwqQkcpe.tail | 45 + ...seti_1PiS6LFY0qyl6XeWfwqQkcpe_confirm.tail | 95 + ...intents_seti_1PiS6LFY0qyl6XeWfwqQkcpe.tail | 45 + .../0011_get_v1_payment_methods.tail | 81 + .../0012_post_create_ephemeral_key.tail | 18 + .../0013_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS6OFY0qyl6XeWeafPdyl1.tail | 45 + ...seti_1PiS6OFY0qyl6XeWeafPdyl1_confirm.tail | 95 + ...intents_seti_1PiS6OFY0qyl6XeWeafPdyl1.tail | 45 + .../0017_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj42RFY0qyl6XeW1G4psghK.tail | 64 + ...s_pi_3Pj42RFY0qyl6XeW1G4psghK_confirm.tail | 114 + ...t_intents_pi_3Pj42RFY0qyl6XeW1G4psghK.tail | 64 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj42VFY0qyl6XeW1S6Bvk4O.tail | 64 + ...s_pi_3Pj42VFY0qyl6XeW1S6Bvk4O_confirm.tail | 114 + ...t_intents_pi_3Pj42VFY0qyl6XeW1S6Bvk4O.tail | 64 + .../0012_get_v1_payment_methods.tail | 81 + .../0013_post_create_ephemeral_key.tail | 18 + .../0014_post_v1_payment_methods.tail | 77 + .../0015_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj42YFY0qyl6XeW0TuZwW2K.tail | 64 + ...s_pi_3Pj42YFY0qyl6XeW0TuZwW2K_confirm.tail | 114 + ...t_intents_pi_3Pj42YFY0qyl6XeW0TuZwW2K.tail | 64 + .../0019_get_v1_payment_methods.tail | 81 + .../0020_post_create_ephemeral_key.tail | 18 + .../0021_post_create_setup_intent.tail | 18 + ...intents_seti_1Pj42bFY0qyl6XeWW7izP2aM.tail | 45 + ...seti_1Pj42bFY0qyl6XeWW7izP2aM_confirm.tail | 95 + ...intents_seti_1Pj42bFY0qyl6XeWW7izP2aM.tail | 45 + .../0025_get_v1_payment_methods.tail | 81 + .../0026_post_create_ephemeral_key.tail | 18 + .../0027_post_v1_payment_methods.tail | 77 + .../0028_post_create_setup_intent.tail | 18 + ...intents_seti_1Pj42fFY0qyl6XeWpJZpd8qv.tail | 45 + ...seti_1Pj42fFY0qyl6XeWpJZpd8qv_confirm.tail | 95 + ...intents_seti_1Pj42fFY0qyl6XeWpJZpd8qv.tail | 45 + .../0032_get_v1_payment_methods.tail | 81 + .../0033_post_create_ephemeral_key.tail | 18 + .../0034_post_v1_payment_methods.tail | 77 + .../0035_post_create_setup_intent.tail | 18 + ...intents_seti_1Pj42iFY0qyl6XeWO5zkclKJ.tail | 91 + ...intents_seti_1Pj42iFY0qyl6XeWO5zkclKJ.tail | 45 + .../0038_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4YFY0qyl6XeW00bjI1bV.tail | 64 + ...s_pi_3PiS4YFY0qyl6XeW00bjI1bV_confirm.tail | 114 + ...t_intents_pi_3PiS4YFY0qyl6XeW00bjI1bV.tail | 64 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4cFY0qyl6XeW1SkdNnUV.tail | 64 + ...s_pi_3PiS4cFY0qyl6XeW1SkdNnUV_confirm.tail | 114 + ...t_intents_pi_3PiS4cFY0qyl6XeW1SkdNnUV.tail | 64 + .../0012_get_v1_payment_methods.tail | 81 + .../0013_post_create_ephemeral_key.tail | 18 + .../0014_post_v1_payment_methods.tail | 77 + .../0015_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4fFY0qyl6XeW1bFCizTK.tail | 110 + ...t_intents_pi_3PiS4fFY0qyl6XeW1bFCizTK.tail | 64 + .../0018_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4iFY0qyl6XeW1SY1RjLl.tail | 64 + ...s_pi_3PiS4iFY0qyl6XeW1SY1RjLl_confirm.tail | 114 + ...t_intents_pi_3PiS4iFY0qyl6XeW1SY1RjLl.tail | 64 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4mFY0qyl6XeW18rpc2LS.tail | 64 + ...s_pi_3PiS4mFY0qyl6XeW18rpc2LS_confirm.tail | 114 + ...t_intents_pi_3PiS4mFY0qyl6XeW18rpc2LS.tail | 64 + .../0012_get_v1_payment_methods.tail | 81 + .../0013_post_create_ephemeral_key.tail | 18 + .../0014_post_v1_payment_methods.tail | 77 + .../0015_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4pFY0qyl6XeW0QFV0awd.tail | 110 + ...t_intents_pi_3PiS4pFY0qyl6XeW0QFV0awd.tail | 64 + .../0018_get_v1_payment_methods.tail | 81 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4EFY0qyl6XeW0yOJYEqG.tail | 64 + ...s_pi_3PiS4EFY0qyl6XeW0yOJYEqG_confirm.tail | 119 + ...t_intents_pi_3PiS4EFY0qyl6XeW0yOJYEqG.tail | 69 + .../0005_get_v1_payment_methods.tail | 81 + .../0006_post_create_ephemeral_key.tail | 18 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4HFY0qyl6XeW0xhKArcl.tail | 64 + ...s_pi_3PiS4HFY0qyl6XeW0xhKArcl_confirm.tail | 119 + ...t_intents_pi_3PiS4HFY0qyl6XeW0xhKArcl.tail | 69 + .../0012_get_v1_payment_methods.tail | 81 + .../0013_post_create_ephemeral_key.tail | 18 + .../0014_post_v1_payment_methods.tail | 77 + .../0015_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4LFY0qyl6XeW0IUAqohP.tail | 115 + ...t_intents_pi_3PiS4LFY0qyl6XeW0IUAqohP.tail | 69 + .../0018_get_v1_payment_methods.tail | 81 + .../0019_post_create_ephemeral_key.tail | 18 + .../0020_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4OFY0qyl6XeW1QmNfV3P.tail | 64 + ...s_pi_3PiS4OFY0qyl6XeW1QmNfV3P_confirm.tail | 114 + ...t_intents_pi_3PiS4OFY0qyl6XeW1QmNfV3P.tail | 64 + .../0024_get_v1_payment_methods.tail | 34 + .../0025_post_create_ephemeral_key.tail | 18 + .../0026_post_v1_payment_methods.tail | 77 + .../0027_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4SFY0qyl6XeW1pnsNTSc.tail | 64 + ...s_pi_3PiS4SFY0qyl6XeW1pnsNTSc_confirm.tail | 114 + ...t_intents_pi_3PiS4SFY0qyl6XeW1pnsNTSc.tail | 64 + .../0031_get_v1_payment_methods.tail | 34 + .../0032_post_create_ephemeral_key.tail | 18 + .../0033_post_v1_payment_methods.tail | 77 + .../0034_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS4VFY0qyl6XeW1khrwbQ6.tail | 110 + ...t_intents_pi_3PiS4VFY0qyl6XeW1khrwbQ6.tail | 64 + .../0037_get_v1_payment_methods.tail | 34 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFJF7QokQdxBy1oLWJSGM.tail | 59 + ...s_pi_3PiTFJF7QokQdxBy1oLWJSGM_confirm.tail | 89 + .../0003_post_v1_payment_methods.tail | 57 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFLF7QokQdxBy2skk4jd2.tail | 59 + ...s_pi_3PiTFLF7QokQdxBy2skk4jd2_confirm.tail | 89 + .../0007_post_v1_payment_methods.tail | 57 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFNF7QokQdxBy0o0gue0K.tail | 85 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFOF7QokQdxBy2DeLSlCw.tail | 59 + ...s_pi_3PiTFOF7QokQdxBy2DeLSlCw_confirm.tail | 89 + .../0013_post_v1_payment_methods.tail | 57 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFQF7QokQdxBy1mcIi24C.tail | 59 + ...s_pi_3PiTFQF7QokQdxBy1mcIi24C_confirm.tail | 89 + .../0017_post_v1_payment_methods.tail | 57 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFSF7QokQdxBy2aCZCbRj.tail | 85 + .../0020_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFTF7QokQdxBy85OjWngM.tail | 45 + ...seti_1PiTFTF7QokQdxBy85OjWngM_confirm.tail | 75 + .../0023_post_v1_payment_methods.tail | 57 + .../0024_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFVF7QokQdxByjRz9RlYV.tail | 45 + ...seti_1PiTFVF7QokQdxByjRz9RlYV_confirm.tail | 75 + .../0027_post_v1_payment_methods.tail | 57 + .../0028_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFWF7QokQdxByLZXVizQJ.tail | 71 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEEFY0qyl6XeW0s1Y1qGj.tail | 64 + ...s_pi_3PiTEEFY0qyl6XeW0s1Y1qGj_confirm.tail | 100 + ...t_intents_pi_3PiTEEFY0qyl6XeW0s1Y1qGj.tail | 96 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEGFY0qyl6XeW042Gfi2g.tail | 64 + ...s_pi_3PiTEGFY0qyl6XeW042Gfi2g_confirm.tail | 100 + ...t_intents_pi_3PiTEGFY0qyl6XeW042Gfi2g.tail | 96 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEJFY0qyl6XeW1GTJWEDq.tail | 96 + ...t_intents_pi_3PiTEJFY0qyl6XeW1GTJWEDq.tail | 96 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEKKG6vc7r7YC1tBz8NmF.tail | 59 + ...s_pi_3PiTEKKG6vc7r7YC1tBz8NmF_confirm.tail | 93 + ...t_intents_pi_3PiTEKKG6vc7r7YC1tBz8NmF.tail | 89 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEMKG6vc7r7YC0w1UTQQ7.tail | 59 + ...s_pi_3PiTEMKG6vc7r7YC0w1UTQQ7_confirm.tail | 93 + ...t_intents_pi_3PiTEMKG6vc7r7YC0w1UTQQ7.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEOKG6vc7r7YC1MHc68qb.tail | 89 + ...t_intents_pi_3PiTEOKG6vc7r7YC1MHc68qb.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 64 + ...s_pi_3PiTEPFY0qyl6XeW1MDTgfBY_confirm.tail | 98 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + ...t_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail | 94 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 64 + ...s_pi_3PiTEiFY0qyl6XeW086jB16T_confirm.tail | 98 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + ...t_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail | 94 + .../0019_post_v1_payment_methods.tail | 55 + .../0020_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + ...t_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail | 94 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFwArGMi59tL41WJNey9A.tail | 59 + ...s_pi_3PiTFwArGMi59tL41WJNey9A_confirm.tail | 89 + .../0003_post_v1_payment_methods.tail | 55 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFyArGMi59tL40XEPxieD.tail | 59 + ...s_pi_3PiTFyArGMi59tL40XEPxieD_confirm.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFXGoesj9fw9Q1Xou3zeP.tail | 59 + ...s_pi_3PiTFXGoesj9fw9Q1Xou3zeP_confirm.tail | 89 + .../0003_post_v1_payment_methods.tail | 57 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFZGoesj9fw9Q1ZrT7KZV.tail | 59 + ...s_pi_3PiTFZGoesj9fw9Q1ZrT7KZV_confirm.tail | 89 + .../0007_post_v1_payment_methods.tail | 57 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFbGoesj9fw9Q1Ex7WyLP.tail | 85 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFcGoesj9fw9Q0toFtRG6.tail | 59 + ...s_pi_3PiTFcGoesj9fw9Q0toFtRG6_confirm.tail | 89 + .../0013_post_v1_payment_methods.tail | 57 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFeGoesj9fw9Q1UcMUUqn.tail | 59 + ...s_pi_3PiTFeGoesj9fw9Q1UcMUUqn_confirm.tail | 89 + .../0017_post_v1_payment_methods.tail | 57 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFgGoesj9fw9Q1Adpxm6L.tail | 85 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFhFY0qyl6XeW0jmVo7PO.tail | 64 + ...s_pi_3PiTFhFY0qyl6XeW0jmVo7PO_confirm.tail | 98 + ...t_intents_pi_3PiTFhFY0qyl6XeW0jmVo7PO.tail | 94 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFjFY0qyl6XeW1fcdU0OJ.tail | 64 + ...s_pi_3PiTFjFY0qyl6XeW1fcdU0OJ_confirm.tail | 98 + ...t_intents_pi_3PiTFjFY0qyl6XeW1fcdU0OJ.tail | 94 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFlFY0qyl6XeW02rwAJgQ.tail | 94 + ...t_intents_pi_3PiTFlFY0qyl6XeW02rwAJgQ.tail | 94 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFmFY0qyl6XeW0ktOZq7I.tail | 64 + ...s_pi_3PiTFmFY0qyl6XeW0ktOZq7I_confirm.tail | 98 + ...t_intents_pi_3PiTFmFY0qyl6XeW0ktOZq7I.tail | 94 + .../0017_post_v1_payment_methods.tail | 55 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFoFY0qyl6XeW0IzVfqHs.tail | 64 + ...s_pi_3PiTFoFY0qyl6XeW0IzVfqHs_confirm.tail | 98 + ...t_intents_pi_3PiTFoFY0qyl6XeW0IzVfqHs.tail | 94 + .../0022_post_v1_payment_methods.tail | 55 + .../0023_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFqFY0qyl6XeW0n428V7D.tail | 94 + ...t_intents_pi_3PiTFqFY0qyl6XeW0n428V7D.tail | 94 + .../0026_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFsFY0qyl6XeW7jaV90Md.tail | 45 + ...seti_1PiTFsFY0qyl6XeW7jaV90Md_confirm.tail | 79 + ...intents_seti_1PiTFsFY0qyl6XeW7jaV90Md.tail | 75 + .../0030_post_v1_payment_methods.tail | 55 + .../0031_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFtFY0qyl6XeWBaOXa6Hu.tail | 45 + ...seti_1PiTFtFY0qyl6XeWBaOXa6Hu_confirm.tail | 79 + ...intents_seti_1PiTFtFY0qyl6XeWBaOXa6Hu.tail | 75 + .../0035_post_v1_payment_methods.tail | 55 + .../0036_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTFvFY0qyl6XeWtjYpzRrr.tail | 75 + ...intents_seti_1PiTFvFY0qyl6XeWtjYpzRrr.tail | 75 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pgvu8Goesj9fw9Q0IOK5vPe.tail | 59 + ...s_pi_3Pgvu8Goesj9fw9Q0IOK5vPe_confirm.tail | 92 + ...t_intents_pi_3Pgvu8Goesj9fw9Q0IOK5vPe.tail | 89 + .../0004_post_v1_payment_methods.tail | 54 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PgvuAGoesj9fw9Q0HWarpDa.tail | 59 + ...s_pi_3PgvuAGoesj9fw9Q0HWarpDa_confirm.tail | 92 + ...t_intents_pi_3PgvuAGoesj9fw9Q0HWarpDa.tail | 89 + .../0009_post_v1_payment_methods.tail | 54 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PgvuDGoesj9fw9Q1rBUIspY.tail | 89 + ...t_intents_pi_3PgvuDGoesj9fw9Q1rBUIspY.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTFzJQVROkWvqT2AHpcsRS.tail | 59 + ...s_pi_3PiTFzJQVROkWvqT2AHpcsRS_confirm.tail | 96 + ...t_intents_pi_3PiTFzJQVROkWvqT2AHpcsRS.tail | 92 + .../0004_post_v1_payment_methods.tail | 56 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTG2JQVROkWvqT2PNPBtsB.tail | 59 + ...s_pi_3PiTG2JQVROkWvqT2PNPBtsB_confirm.tail | 96 + ...t_intents_pi_3PiTG2JQVROkWvqT2PNPBtsB.tail | 92 + .../0009_post_v1_payment_methods.tail | 56 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTG4JQVROkWvqT2DcJBbmi.tail | 92 + ...t_intents_pi_3PiTG4JQVROkWvqT2DcJBbmi.tail | 92 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTG5JQVROkWvqT1aduWbdJ.tail | 59 + ...s_pi_3PiTG5JQVROkWvqT1aduWbdJ_confirm.tail | 96 + ...t_intents_pi_3PiTG5JQVROkWvqT1aduWbdJ.tail | 92 + .../0017_post_v1_payment_methods.tail | 56 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTG8JQVROkWvqT1ZjX5xPZ.tail | 59 + ...s_pi_3PiTG8JQVROkWvqT1ZjX5xPZ_confirm.tail | 96 + ...t_intents_pi_3PiTG8JQVROkWvqT1ZjX5xPZ.tail | 92 + .../0022_post_v1_payment_methods.tail | 56 + .../0023_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGAJQVROkWvqT2tTBSBLu.tail | 92 + ...t_intents_pi_3PiTGAJQVROkWvqT2tTBSBLu.tail | 92 + .../0026_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGBJQVROkWvqTTzTGuL9A.tail | 45 + ...seti_1PiTGBJQVROkWvqTTzTGuL9A_confirm.tail | 74 + .../0029_post_v1_payment_methods.tail | 56 + .../0030_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGDJQVROkWvqTentcyB7A.tail | 45 + ...seti_1PiTGDJQVROkWvqTentcyB7A_confirm.tail | 74 + .../0033_post_v1_payment_methods.tail | 56 + .../0034_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGEJQVROkWvqT2JMW6jDA.tail | 70 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGFFY0qyl6XeW0ftVWFhB.tail | 64 + ...s_pi_3PiTGFFY0qyl6XeW0ftVWFhB_confirm.tail | 114 + .../0003_post_v1_payment_methods.tail | 77 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGIFY0qyl6XeW1M7cc3hi.tail | 64 + ...s_pi_3PiTGIFY0qyl6XeW1M7cc3hi_confirm.tail | 114 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGKFY0qyl6XeW1IYJLkNz.tail | 110 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGMFY0qyl6XeW0BsWTvpQ.tail | 64 + ...s_pi_3PiTGMFY0qyl6XeW0BsWTvpQ_confirm.tail | 114 + .../0013_post_v1_payment_methods.tail | 77 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGOFY0qyl6XeW1dFZRTpk.tail | 64 + ...s_pi_3PiTGOFY0qyl6XeW1dFZRTpk_confirm.tail | 114 + .../0017_post_v1_payment_methods.tail | 77 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGRFY0qyl6XeW0qDrruu9.tail | 110 + .../0020_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGSFY0qyl6XeWMTPiPbvt.tail | 45 + ...seti_1PiTGSFY0qyl6XeWMTPiPbvt_confirm.tail | 95 + .../0023_post_v1_payment_methods.tail | 77 + .../0024_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGUFY0qyl6XeWTyqCLhFp.tail | 45 + ...seti_1PiTGUFY0qyl6XeWTyqCLhFp_confirm.tail | 95 + .../0027_post_v1_payment_methods.tail | 77 + .../0028_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGWFY0qyl6XeWx3jWxDK0.tail | 91 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGYFY0qyl6XeW1w5uq2LD.tail | 64 + ...s_pi_3PiTGYFY0qyl6XeW1w5uq2LD_confirm.tail | 114 + .../0003_post_v1_payment_methods.tail | 77 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGaFY0qyl6XeW1UjPiJbG.tail | 64 + ...s_pi_3PiTGaFY0qyl6XeW1UjPiJbG_confirm.tail | 114 + .../0007_post_v1_payment_methods.tail | 77 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGdFY0qyl6XeW1SqUIwh0.tail | 110 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGeFY0qyl6XeW14vbbzHp.tail | 64 + ...s_pi_3PiTGeFY0qyl6XeW14vbbzHp_confirm.tail | 114 + .../0013_post_v1_payment_methods.tail | 77 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGhFY0qyl6XeW1ANdoojK.tail | 64 + ...s_pi_3PiTGhFY0qyl6XeW1ANdoojK_confirm.tail | 114 + .../0017_post_v1_payment_methods.tail | 77 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGkFY0qyl6XeW0nLUKYR4.tail | 110 + .../0020_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGlFY0qyl6XeWDBSwU0Ji.tail | 45 + ...seti_1PiTGlFY0qyl6XeWDBSwU0Ji_confirm.tail | 95 + .../0023_post_v1_payment_methods.tail | 77 + .../0024_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGoFY0qyl6XeWtNO3Gq7J.tail | 45 + ...seti_1PiTGoFY0qyl6XeWtNO3Gq7J_confirm.tail | 95 + .../0027_post_v1_payment_methods.tail | 77 + .../0028_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTGqFY0qyl6XeWqSYQnSMj.tail | 91 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGsFY0qyl6XeW1cydassq.tail | 64 + ...s_pi_3PiTGsFY0qyl6XeW1cydassq_confirm.tail | 104 + ...s_pi_3PiTGsFY0qyl6XeW1cydassq_refresh.tail | 104 + .../0004_post_v1_payment_methods.tail | 56 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGvFY0qyl6XeW07Oz79VA.tail | 64 + ...s_pi_3PiTGvFY0qyl6XeW07Oz79VA_confirm.tail | 104 + ...s_pi_3PiTGvFY0qyl6XeW07Oz79VA_refresh.tail | 104 + .../0009_post_v1_payment_methods.tail | 56 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTGyFY0qyl6XeW0PUH0y6X.tail | 100 + ...s_pi_3PiTGyFY0qyl6XeW0PUH0y6X_refresh.tail | 104 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTH1INV13NEzqK12Jr5dBU.tail | 64 + ...s_pi_3PiTH1INV13NEzqK12Jr5dBU_confirm.tail | 99 + ...t_intents_pi_3PiTH1INV13NEzqK12Jr5dBU.tail | 95 + .../0004_post_v1_payment_methods.tail | 56 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTH3INV13NEzqK1pkJxeaL.tail | 64 + ...s_pi_3PiTH3INV13NEzqK1pkJxeaL_confirm.tail | 99 + ...t_intents_pi_3PiTH3INV13NEzqK1pkJxeaL.tail | 95 + .../0009_post_v1_payment_methods.tail | 56 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTH5INV13NEzqK1SfhebHX.tail | 95 + ...t_intents_pi_3PiTH5INV13NEzqK1SfhebHX.tail | 95 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTH7AOnZToJom10keWm7Vo.tail | 59 + ...s_pi_3PiTH7AOnZToJom10keWm7Vo_confirm.tail | 93 + ...t_intents_pi_3PiTH7AOnZToJom10keWm7Vo.tail | 89 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHAAOnZToJom10ZeKkJvu.tail | 59 + ...s_pi_3PiTHAAOnZToJom10ZeKkJvu_confirm.tail | 93 + ...t_intents_pi_3PiTHAAOnZToJom10ZeKkJvu.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHCAOnZToJom10MAh7KxU.tail | 89 + ...t_intents_pi_3PiTHCAOnZToJom10MAh7KxU.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHTFY0qyl6XeW04DQWUEN.tail | 64 + ...s_pi_3PiTHTFY0qyl6XeW04DQWUEN_confirm.tail | 98 + ...nonce_QZcWOgi94iiGXAWwMbz9dSzJ2xAh5S1.tail | 15 + ...t_intents_pi_3PiTHTFY0qyl6XeW04DQWUEN.tail | 94 + .../0005_post_v1_payment_methods.tail | 55 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHWFY0qyl6XeW0MSNKjMC.tail | 64 + ...s_pi_3PiTHWFY0qyl6XeW0MSNKjMC_confirm.tail | 98 + ...nonce_QZcW5TtpMtB8PKkvoqj5BVNruPM7C5S.tail | 15 + ...t_intents_pi_3PiTHWFY0qyl6XeW0MSNKjMC.tail | 94 + .../0011_post_v1_payment_methods.tail | 55 + .../0012_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHZFY0qyl6XeW1NaLbUlA.tail | 94 + ...nonce_QZcWUmwEYQRrYKqJmZd4KaL5NKsND5I.tail | 15 + ...t_intents_pi_3PiTHZFY0qyl6XeW1NaLbUlA.tail | 94 + .../0016_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHbFY0qyl6XeW0ViaYHn2.tail | 64 + ...s_pi_3PiTHbFY0qyl6XeW0ViaYHn2_confirm.tail | 98 + ...nonce_QZcWXqGqpErtQo8FfcwxefSINg4DeO7.tail | 15 + ...t_intents_pi_3PiTHbFY0qyl6XeW0ViaYHn2.tail | 94 + .../0021_post_v1_payment_methods.tail | 55 + .../0022_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHdFY0qyl6XeW0GkwHXZj.tail | 64 + ...s_pi_3PiTHdFY0qyl6XeW0GkwHXZj_confirm.tail | 98 + ...nonce_QZcWjghujsb90cOevtFxttNZUSu4r4y.tail | 15 + ...t_intents_pi_3PiTHdFY0qyl6XeW0GkwHXZj.tail | 94 + .../0027_post_v1_payment_methods.tail | 55 + .../0028_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHgFY0qyl6XeW156oy7ob.tail | 94 + ...nonce_QZcWHf0Wjmo92W7CSbZWyoUZ7Wjd0vq.tail | 15 + ...t_intents_pi_3PiTHgFY0qyl6XeW156oy7ob.tail | 94 + .../0032_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHiFY0qyl6XeWvHeP60t4.tail | 45 + ...seti_1PiTHiFY0qyl6XeWvHeP60t4_confirm.tail | 79 + ...nonce_QZcWwsqB422p1Fu3hbu6A7grC7cZIiP.tail | 15 + ...intents_seti_1PiTHiFY0qyl6XeWvHeP60t4.tail | 75 + .../0037_post_v1_payment_methods.tail | 55 + .../0038_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHlFY0qyl6XeWJy48DnwP.tail | 45 + ...seti_1PiTHlFY0qyl6XeWJy48DnwP_confirm.tail | 79 + ...nonce_QZcW9RUVWuRkuPLmwGilygNKyjczOeD.tail | 15 + ...intents_seti_1PiTHlFY0qyl6XeWJy48DnwP.tail | 75 + .../0043_post_v1_payment_methods.tail | 55 + .../0044_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHoFY0qyl6XeWbU801LWM.tail | 75 + ...nonce_QZcWNeuTDwJXuRafcPI0TCsslYF5yEW.tail | 15 + ...intents_seti_1PiTHoFY0qyl6XeWbU801LWM.tail | 75 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHpIq2LmpyICo17Aj9ab8.tail | 59 + ...s_pi_3PiTHpIq2LmpyICo17Aj9ab8_confirm.tail | 111 + ...t_intents_pi_3PiTHpIq2LmpyICo17Aj9ab8.tail | 107 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHrIq2LmpyICo0TpElXfc.tail | 59 + ...s_pi_3PiTHrIq2LmpyICo0TpElXfc_confirm.tail | 111 + ...t_intents_pi_3PiTHrIq2LmpyICo0TpElXfc.tail | 107 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHtIq2LmpyICo0AGAD0LQ.tail | 107 + ...t_intents_pi_3PiTHtIq2LmpyICo0AGAD0LQ.tail | 107 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHvKG6vc7r7YC066TAvt0.tail | 59 + ...s_pi_3PiTHvKG6vc7r7YC066TAvt0_confirm.tail | 93 + ...t_intents_pi_3PiTHvKG6vc7r7YC066TAvt0.tail | 89 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHxKG6vc7r7YC0dYkjhBm.tail | 59 + ...s_pi_3PiTHxKG6vc7r7YC0dYkjhBm_confirm.tail | 93 + ...t_intents_pi_3PiTHxKG6vc7r7YC0dYkjhBm.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHzKG6vc7r7YC0HNzNVOz.tail | 89 + ...t_intents_pi_3PiTHzKG6vc7r7YC0HNzNVOz.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTI0FY0qyl6XeW00w0f3UO.tail | 64 + ...s_pi_3PiTI0FY0qyl6XeW00w0f3UO_confirm.tail | 100 + ...t_intents_pi_3PiTI0FY0qyl6XeW00w0f3UO.tail | 96 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTI2FY0qyl6XeW1q37euAp.tail | 64 + ...s_pi_3PiTI2FY0qyl6XeW1q37euAp_confirm.tail | 100 + ...t_intents_pi_3PiTI2FY0qyl6XeW1q37euAp.tail | 96 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTI4FY0qyl6XeW1QCvqch7.tail | 96 + ...t_intents_pi_3PiTI4FY0qyl6XeW1QCvqch7.tail | 96 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTI6HNG4o8pO5l1omLzZGE.tail | 59 + ...s_pi_3PiTI6HNG4o8pO5l1omLzZGE_confirm.tail | 94 + ...t_intents_pi_3PiTI6HNG4o8pO5l1omLzZGE.tail | 90 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTI8HNG4o8pO5l00QjdMZh.tail | 59 + ...s_pi_3PiTI8HNG4o8pO5l00QjdMZh_confirm.tail | 94 + ...t_intents_pi_3PiTI8HNG4o8pO5l00QjdMZh.tail | 90 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIAHNG4o8pO5l0mGlaDkM.tail | 90 + ...t_intents_pi_3PiTIAHNG4o8pO5l0mGlaDkM.tail | 90 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIBAOnZToJom11KGbQC8i.tail | 59 + ...s_pi_3PiTIBAOnZToJom11KGbQC8i_confirm.tail | 95 + .../0003_post_v1_payment_methods.tail | 55 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIDAOnZToJom12JSu0YfY.tail | 59 + ...s_pi_3PiTIDAOnZToJom12JSu0YfY_confirm.tail | 95 + .../0007_post_v1_payment_methods.tail | 55 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIFAOnZToJom118jTKXAJ.tail | 91 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIGKG6vc7r7YC1nVSqYA5.tail | 59 + ...s_pi_3PiTIGKG6vc7r7YC1nVSqYA5_confirm.tail | 95 + ...nonce_QZcXedDGDmgkcE9vJxyzNihKfF5Agm7.tail | 15 + ...t_intents_pi_3PiTIGKG6vc7r7YC1nVSqYA5.tail | 91 + .../0005_post_v1_payment_methods.tail | 57 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIJKG6vc7r7YC1REWOJKA.tail | 59 + ...s_pi_3PiTIJKG6vc7r7YC1REWOJKA_confirm.tail | 95 + ...nonce_QZcXE0hjyH8P8Wxr54IU0WYdnu1rLIf.tail | 15 + ...t_intents_pi_3PiTIJKG6vc7r7YC1REWOJKA.tail | 91 + .../0011_post_v1_payment_methods.tail | 57 + .../0012_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTILKG6vc7r7YC13jTex7C.tail | 91 + ...nonce_QZcXuf4trSMfWJCAo888nspG1xfWsdz.tail | 15 + ...t_intents_pi_3PiTILKG6vc7r7YC13jTex7C.tail | 91 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIMBgCYKNuUnn1mwMKvMb.tail | 59 + ...s_pi_3PiTIMBgCYKNuUnn1mwMKvMb_confirm.tail | 95 + .../0003_post_v1_payment_methods.tail | 55 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIOBgCYKNuUnn1Szo5dUi.tail | 59 + ...s_pi_3PiTIOBgCYKNuUnn1Szo5dUi_confirm.tail | 95 + .../0007_post_v1_payment_methods.tail | 55 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIQBgCYKNuUnn1vrQBvMl.tail | 91 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIRGoesj9fw9Q103BX9Zx.tail | 59 + ...s_pi_3PiTIRGoesj9fw9Q103BX9Zx_confirm.tail | 93 + ...t_intents_pi_3PiTIRGoesj9fw9Q103BX9Zx.tail | 89 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTITGoesj9fw9Q0txOGiIi.tail | 59 + ...s_pi_3PiTITGoesj9fw9Q0txOGiIi_confirm.tail | 93 + ...t_intents_pi_3PiTITGoesj9fw9Q0txOGiIi.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIWGoesj9fw9Q0hE62jyW.tail | 89 + ...t_intents_pi_3PiTIWGoesj9fw9Q0hE62jyW.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIjFY0qyl6XeW1unrjkE5.tail | 64 + ...s_pi_3PiTIjFY0qyl6XeW1unrjkE5_confirm.tail | 100 + .../0003_post_v1_payment_methods.tail | 63 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIlFY0qyl6XeW1viHPW3U.tail | 64 + ...s_pi_3PiTIlFY0qyl6XeW1viHPW3U_confirm.tail | 100 + .../0007_post_v1_payment_methods.tail | 63 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTInFY0qyl6XeW1hM8UL5q.tail | 96 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIoFY0qyl6XeW0vuG3nGU.tail | 64 + ...s_pi_3PiTIoFY0qyl6XeW0vuG3nGU_confirm.tail | 100 + .../0013_post_v1_payment_methods.tail | 63 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIqFY0qyl6XeW1gH0KBo4.tail | 64 + ...s_pi_3PiTIqFY0qyl6XeW1gH0KBo4_confirm.tail | 100 + .../0017_post_v1_payment_methods.tail | 63 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIsFY0qyl6XeW1C1tsKon.tail | 96 + .../0020_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTItFY0qyl6XeWxlhPwXUb.tail | 45 + ...seti_1PiTItFY0qyl6XeWxlhPwXUb_confirm.tail | 81 + .../0023_post_v1_payment_methods.tail | 63 + .../0024_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTIuFY0qyl6XeWxQejK4vr.tail | 45 + ...seti_1PiTIuFY0qyl6XeWxQejK4vr_confirm.tail | 81 + .../0027_post_v1_payment_methods.tail | 63 + .../0028_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTIwFY0qyl6XeWmaMZR3YH.tail | 77 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj9HYIFbdis1OxT0MCooEOZ.tail | 59 + ...s_pi_3Pj9HYIFbdis1OxT0MCooEOZ_confirm.tail | 93 + ...t_intents_pi_3Pj9HYIFbdis1OxT0MCooEOZ.tail | 89 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj9HaIFbdis1OxT1285sniR.tail | 59 + ...s_pi_3Pj9HaIFbdis1OxT1285sniR_confirm.tail | 93 + ...t_intents_pi_3Pj9HaIFbdis1OxT1285sniR.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3Pj9HcIFbdis1OxT1bYtukzw.tail | 89 + ...t_intents_pi_3Pj9HcIFbdis1OxT1bYtukzw.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIYFY0qyl6XeW0RqJBsZe.tail | 64 + ...s_pi_3PiTIYFY0qyl6XeW0RqJBsZe_confirm.tail | 100 + .../0003_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIZFY0qyl6XeW1H6Mig0H.tail | 64 + ...s_pi_3PiTIZFY0qyl6XeW1H6Mig0H_confirm.tail | 100 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIbFY0qyl6XeW1NQuTErM.tail | 96 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIcFY0qyl6XeW0QGtaGZR.tail | 64 + ...s_pi_3PiTIcFY0qyl6XeW0QGtaGZR_confirm.tail | 100 + .../0011_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIdFY0qyl6XeW1q5Waorp.tail | 64 + ...s_pi_3PiTIdFY0qyl6XeW1q5Waorp_confirm.tail | 100 + .../0014_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIfFY0qyl6XeW1XtzML5b.tail | 96 + .../0016_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTIgFY0qyl6XeWLPpUQMbG.tail | 45 + ...seti_1PiTIgFY0qyl6XeWLPpUQMbG_confirm.tail | 81 + .../0019_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTIhFY0qyl6XeWxFheNl8F.tail | 45 + ...seti_1PiTIhFY0qyl6XeWxFheNl8F_confirm.tail | 81 + .../0022_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTIiFY0qyl6XeWiu7YW1U8.tail | 77 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIxFY0qyl6XeW1pYqZmqh.tail | 64 + ...s_pi_3PiTIxFY0qyl6XeW1pYqZmqh_confirm.tail | 98 + ...t_intents_pi_3PiTIxFY0qyl6XeW1pYqZmqh.tail | 94 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTIzFY0qyl6XeW14bCK4tY.tail | 64 + ...s_pi_3PiTIzFY0qyl6XeW14bCK4tY_confirm.tail | 98 + ...t_intents_pi_3PiTIzFY0qyl6XeW14bCK4tY.tail | 94 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJ1FY0qyl6XeW1z2IbX7C.tail | 94 + ...t_intents_pi_3PiTJ1FY0qyl6XeW1z2IbX7C.tail | 94 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJ2FY0qyl6XeW09FXlvBW.tail | 64 + ...s_pi_3PiTJ2FY0qyl6XeW09FXlvBW_confirm.tail | 98 + ...t_intents_pi_3PiTJ2FY0qyl6XeW09FXlvBW.tail | 94 + .../0017_post_v1_payment_methods.tail | 55 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJ4FY0qyl6XeW07RzIcKP.tail | 64 + ...s_pi_3PiTJ4FY0qyl6XeW07RzIcKP_confirm.tail | 98 + ...t_intents_pi_3PiTJ4FY0qyl6XeW07RzIcKP.tail | 94 + .../0022_post_v1_payment_methods.tail | 55 + .../0023_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJ6FY0qyl6XeW07WjYW7e.tail | 94 + ...t_intents_pi_3PiTJ6FY0qyl6XeW07WjYW7e.tail | 94 + .../0026_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTJ7FY0qyl6XeW3knlHSRN.tail | 45 + ...seti_1PiTJ7FY0qyl6XeW3knlHSRN_confirm.tail | 79 + ...intents_seti_1PiTJ7FY0qyl6XeW3knlHSRN.tail | 75 + .../0030_post_v1_payment_methods.tail | 55 + .../0031_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8.tail | 45 + ...seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8_confirm.tail | 79 + ...intents_seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8.tail | 75 + .../0035_post_v1_payment_methods.tail | 55 + .../0036_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTJBFY0qyl6XeWsuSLNbVJ.tail | 75 + ...intents_seti_1PiTJBFY0qyl6XeWsuSLNbVJ.tail | 75 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJCFY0qyl6XeW02odRwT7.tail | 64 + ...s_pi_3PiTJCFY0qyl6XeW02odRwT7_confirm.tail | 98 + ...t_intents_pi_3PiTJCFY0qyl6XeW02odRwT7.tail | 94 + .../0004_post_v1_payment_methods.tail | 55 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJEFY0qyl6XeW0HtZkX1e.tail | 64 + ...s_pi_3PiTJEFY0qyl6XeW0HtZkX1e_confirm.tail | 98 + ...t_intents_pi_3PiTJEFY0qyl6XeW0HtZkX1e.tail | 94 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJGFY0qyl6XeW0uOXbpda.tail | 94 + ...t_intents_pi_3PiTJGFY0qyl6XeW0uOXbpda.tail | 94 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 59 + ...s_pi_3PiTJHKG6vc7r7YC1JL1HSTL_confirm.tail | 98 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + ...t_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail | 94 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 59 + ...s_pi_3PiTJQKG6vc7r7YC1uSovsaM_confirm.tail | 98 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + ...t_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail | 94 + .../0019_post_v1_payment_methods.tail | 55 + .../0020_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + ...t_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail | 94 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 59 + ...s_pi_3PiTJgGoesj9fw9Q12D8WmHA_confirm.tail | 93 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + ...t_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail | 89 + .../0009_post_v1_payment_methods.tail | 55 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 59 + ...s_pi_3PiTJpGoesj9fw9Q1H4oM4xc_confirm.tail | 93 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + ...t_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail | 89 + .../0019_post_v1_payment_methods.tail | 55 + .../0020_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + ...t_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail | 89 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHDFY0qyl6XeW14CLJ6C8.tail | 64 + ...s_pi_3PiTHDFY0qyl6XeW14CLJ6C8_confirm.tail | 99 + ...t_intents_pi_3PiTHDFY0qyl6XeW14CLJ6C8.tail | 95 + .../0004_post_v1_payment_methods.tail | 56 + .../0005_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHFFY0qyl6XeW0ZB08CTk.tail | 64 + ...s_pi_3PiTHFFY0qyl6XeW0ZB08CTk_confirm.tail | 99 + ...t_intents_pi_3PiTHFFY0qyl6XeW0ZB08CTk.tail | 95 + .../0009_post_v1_payment_methods.tail | 56 + .../0010_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHHFY0qyl6XeW1b0SliHE.tail | 95 + ...t_intents_pi_3PiTHHFY0qyl6XeW1b0SliHE.tail | 95 + .../0013_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHJFY0qyl6XeW1R1AS68e.tail | 64 + ...s_pi_3PiTHJFY0qyl6XeW1R1AS68e_confirm.tail | 99 + ...t_intents_pi_3PiTHJFY0qyl6XeW1R1AS68e.tail | 95 + .../0017_post_v1_payment_methods.tail | 56 + .../0018_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHLFY0qyl6XeW1i6upz8S.tail | 64 + ...s_pi_3PiTHLFY0qyl6XeW1i6upz8S_confirm.tail | 99 + ...t_intents_pi_3PiTHLFY0qyl6XeW1i6upz8S.tail | 95 + .../0022_post_v1_payment_methods.tail | 56 + .../0023_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiTHNFY0qyl6XeW0ry3hAKO.tail | 95 + ...t_intents_pi_3PiTHNFY0qyl6XeW0ry3hAKO.tail | 95 + .../0026_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHOFY0qyl6XeWs8SFGyfb.tail | 45 + ...seti_1PiTHOFY0qyl6XeWs8SFGyfb_confirm.tail | 80 + ...intents_seti_1PiTHOFY0qyl6XeWs8SFGyfb.tail | 76 + .../0030_post_v1_payment_methods.tail | 56 + .../0031_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHQFY0qyl6XeWFjQpkQ40.tail | 45 + ...seti_1PiTHQFY0qyl6XeWFjQpkQ40_confirm.tail | 80 + ...intents_seti_1PiTHQFY0qyl6XeWFjQpkQ40.tail | 76 + .../0035_post_v1_payment_methods.tail | 56 + .../0036_post_create_setup_intent.tail | 18 + ...intents_seti_1PiTHSFY0qyl6XeWUxtVchWu.tail | 76 + ...intents_seti_1PiTHSFY0qyl6XeWUxtVchWu.tail | 76 + .../0000_get_v1_elements_sessions.tail | 359 ++ .../0000_get_v1_elements_sessions.tail | 34 + .../0001_get_v1_elements_sessions.tail | 36 + .../0002_get_v1_elements_sessions.tail | 36 + .../0003_get_v1_elements_sessions.tail | 34 + .../0000_get_v1_elements_sessions.tail | 351 ++ .../0001_get_v1_elements_sessions.tail | 303 ++ .../0002_get_v1_elements_sessions.tail | 359 ++ .../0003_get_v1_elements_sessions.tail | 311 ++ .../0004_get_v1_elements_sessions.tail | 359 ++ .../0000_post_create_ephemeral_key.tail | 18 + .../0001_get_v1_payment_methods.tail | 134 + .../0002_get_v1_elements_sessions.tail | 313 ++ ...3_get_v1_customers_cus_OtOGvD0ZVacBoj.tail | 36 + .../0004_get_v1_payment_methods.tail | 34 + .../0005_get_v1_payment_methods.tail | 34 + .../0006_get_v1_payment_methods.tail | 134 + .../0000_post_create_payment_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 581 +++ .../0000_post_create_payment_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 576 +++ .../0000_post_create_payment_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 582 +++ .../0000_post_create_setup_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 555 +++ .../0000_post_create_setup_intent.tail | 18 + .../0001_get_v1_elements_sessions.tail | 377 ++ .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_get_v1_payment_intents_pi_bad.tail | 37 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_get_v1_setup_intents_seti_bad.tail | 37 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRyUFY0qyl6XeW1tWilBBA.tail | 64 + ...s_pi_3PiRyUFY0qyl6XeW1tWilBBA_confirm.tail | 80 + ...t_intents_pi_3PiRyUFY0qyl6XeW1tWilBBA.tail | 77 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_setup_intent.tail | 18 + ...intents_seti_1PiRyXFY0qyl6XeWGpoZl2L6.tail | 45 + ...seti_1PiRyXFY0qyl6XeWGpoZl2L6_confirm.tail | 48 + ...intents_seti_1PiRyXFY0qyl6XeWGpoZl2L6.tail | 45 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRyaFY0qyl6XeW1FHXpHhE.tail | 64 + ...s_pi_3PiRyaFY0qyl6XeW1FHXpHhE_confirm.tail | 80 + ...t_intents_pi_3PiRyaFY0qyl6XeW1FHXpHhE.tail | 77 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRydFY0qyl6XeW11cbTNpX.tail | 64 + ...s_pi_3PiRydFY0qyl6XeW11cbTNpX_confirm.tail | 80 + ...t_intents_pi_3PiRydFY0qyl6XeW11cbTNpX.tail | 77 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRyfFY0qyl6XeW14TKwmwB.tail | 64 + ...t_intents_pi_3PiRyfFY0qyl6XeW14TKwmwB.tail | 64 + .../0000_post_v1_tokens.tail | 70 + .../0001_post_v1_payment_methods.tail | 82 + .../0002_post_create_setup_intent.tail | 18 + ...intents_seti_1PiRyiFY0qyl6XeWO3Bxrlh3.tail | 45 + ...seti_1PiRyiFY0qyl6XeWO3Bxrlh3_confirm.tail | 48 + ...intents_seti_1PiRyiFY0qyl6XeWO3Bxrlh3.tail | 45 + .../0000_post_v1_tokens.tail | 37 + .../0000_post_v1_tokens.tail | 37 + .../0000_post_v1_tokens.tail | 37 + .../0000_post_v1_tokens.tail | 53 + .../testInvalidKey/0000_post_v1_tokens.tail | 25 + .../0000_post_v1_tokens.tail | 41 + .../0000_post_v1_tokens.tail | 41 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_tokens.tail | 66 + .../testInvalidCVC/0000_post_v1_tokens.tail | 41 + .../testInvalidKey/0000_post_v1_tokens.tail | 25 + .../0000_post_v1_files.tail | 38 + .../0000_post_v1_files.tail | 38 + .../testInvalidKey/0000_post_v1_files.tail | 24 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRzAFY0qyl6XeW0U7X5Y2j.tail | 110 + .../0000_post_v1_payment_methods.tail | 77 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiRzBFY0qyl6XeW1yPMQw10.tail | 91 + ...s_pi_3P20wFFY0qyl6XeW0dSOQ6W7_confirm.tail | 78 + ...s_pi_3P20wFFY0qyl6XeW0dSOQ6W7_confirm.tail | 78 + ...seti_1P1xLBFY0qyl6XeWc7c2LrMK_confirm.tail | 59 + ...seti_1P1xLBFY0qyl6XeWc7c2LrMK_confirm.tail | 59 + ...t_intents_pi_3P232pFY0qyl6XeW0FFRtE0A.tail | 37 + ...intents_seti_3P232pFY0qyl6XeW0FFRtE0A.tail | 37 + .../0000_post_v1_payment_methods.tail | 63 + .../0001_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiRzEFY0qyl6XeW1uNVnv0e.tail | 96 + .../0000_post_v1_payment_methods.tail | 63 + .../0001_post_create_setup_intent.tail | 18 + ...intents_seti_1PiRzGFY0qyl6XeWnnWjBj6k.tail | 77 + .../0000_post_create_payment_intent.tail | 18 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzZF7QokQdxBy1sf6aNUz_confirm.tail | 63 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiVNGFY0qyl6XeW1lshp0dO_confirm.tail | 76 + ...s_pi_1GGCGfFY0qyl6XeWbSAsh2hn_confirm.tail | 79 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzbFY0qyl6XeW1Ph4tDwk_confirm.tail | 76 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzbFY0qyl6XeW16Hu1D9Z_confirm.tail | 68 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzdFY0qyl6XeW09qa59NF_confirm.tail | 68 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzfFY0qyl6XeW07ggWuUF_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzgFY0qyl6XeW1alcqdDr_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRziFY0qyl6XeW1WJEsbnY_confirm.tail | 87 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzjKG6vc7r7YC0O3aj1rG_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzkFY0qyl6XeW1HtdYNS8_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRznArGMi59tL419JSHquh_confirm.tail | 65 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzmFY0qyl6XeW1VtQrypE_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PkUZjAlz2yHYCNZ0bXjYgHZ_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzoFY0qyl6XeW0dXgRsGO_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzpAOnZToJom11VRsZSvi_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzqKG6vc7r7YC1FL5KNGy_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzrFY0qyl6XeW0ACFC0n6_confirm.tail | 76 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzsHNG4o8pO5l09utfXDf_confirm.tail | 70 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRztArGMi59tL40UISz6Ce_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzuFY0qyl6XeW0OpNxFXf_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3Pj9JiIFbdis1OxT1hf8gUZn_confirm.tail | 69 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzvFY0qyl6XeW0FgxEVqu_confirm.tail | 81 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzwFY0qyl6XeW1UTuddcE_confirm.tail | 74 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzxFY0qyl6XeW1frhhQo3_confirm.tail | 117 + ...0qyl6XeW1frhhQo3_verify_microdeposits.tail | 73 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiRzzFY0qyl6XeW1mDn9qw9_confirm.tail | 117 + ...0qyl6XeW1mDn9qw9_verify_microdeposits.tail | 73 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiS01FY0qyl6XeW1gWPnvBN_confirm.tail | 118 + .../0000_post_create_payment_intent.tail | 18 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail | 37 + ...t_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail | 64 + ...t_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail | 37 + .../0000_post_v1_payment_methods.tail | 57 + ...t_intents_pi_1GaRLjF7QokQdxByYgFPQEi0.tail | 85 + ...t_intents_pi_1GaRLjF7QokQdxByYgFPQEi0.tail | 85 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3KUFbTFY0qyl6XeW1oDBbiQk.tail | 101 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1HbSAfFY0qyl6XeWRnlezJ7K.tail | 88 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3Oz1AfKG6vc7r7YC0VaP6KiE.tail | 93 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3OmQQ0FY0qyl6XeW0H4X6eI0.tail | 98 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1GdPnbFY0qyl6XeW8Ezvxe87.tail | 88 + ...t_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail | 84 + .../0000_post_v1_payment_methods.tail | 56 + ...t_intents_pi_3JYFj9JQVROkWvqT0d2HYaTk.tail | 84 + ...t_intents_pi_1H5J4RFY0qyl6XeWFTpgue7g.tail | 110 + .../0000_post_v1_payment_methods.tail | 56 + ...t_intents_pi_3MMa4NFY0qyl6XeW1FM3HOts.tail | 89 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1Gj0rqFY0qyl6XeWrug30CPz.tail | 88 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 57 + .../0000_post_v1_payment_methods.tail | 54 + .../0000_post_v1_payment_methods.tail | 77 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + ...ds_pm_1PiS0CFY0qyl6XeWcO0sdTlc_attach.tail | 78 + .../0003_post_v1_payment_methods.tail | 77 + ...ds_pm_1PiS0DFY0qyl6XeWZ9i3YHcf_attach.tail | 78 + .../0005_post_create_customer_session_cs.tail | 18 + .../0006_get_v1_elements_sessions.tail | 491 +++ .../0007_get_v1_payment_methods.tail | 129 + .../0008_get_v1_payment_methods.tail | 129 + ...ds_pm_1PiS0DFY0qyl6XeWZ9i3YHcf_detach.tail | 77 + ...ds_pm_1PiS0CFY0qyl6XeWcO0sdTlc_detach.tail | 77 + .../0011_get_v1_payment_methods.tail | 34 + .../0000_post_create_ephemeral_key.tail | 18 + .../0001_post_v1_payment_methods.tail | 77 + ...ds_pm_1PiS0GKG6vc7r7YC33E9uYEd_attach.tail | 78 + ...t_methods_pm_1PiS0GKG6vc7r7YC33E9uYEd.tail | 78 + ...ds_pm_1PiS0GKG6vc7r7YC33E9uYEd_detach.tail | 77 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1GfsdtFY0qyl6XeWLnepIXCI.tail | 90 + .../0000_post_v1_payment_methods.tail | 55 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3Jn3kUFY0qyl6XeW0mCp95UD.tail | 64 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3PGVQJKG6vc7r7YC1Xs7oiWw.tail | 84 + .../0000_post_v1_payment_methods.tail | 76 + .../0000_post_v1_payment_methods.tail | 39 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3P8R5lFY0qyl6XeW0byterUo.tail | 88 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1HoPqsBte6TMTRd4jX0PwrFa.tail | 85 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1GvAdyHNG4o8pO5l0dr078gf.tail | 83 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0MFY0qyl6XeW02iKCVoz.tail | 69 + .../0002_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0NFY0qyl6XeW1qkmS1Of.tail | 69 + .../0004_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0OFY0qyl6XeW1vPzg3R0.tail | 69 + .../0006_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0OFY0qyl6XeW0aiyhz3B.tail | 69 + .../0008_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0PFY0qyl6XeW12ednumf.tail | 69 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0QFY0qyl6XeWGpO1S6kt.tail | 50 + .../0002_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0QFY0qyl6XeWj297PnQP.tail | 50 + .../0004_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0RFY0qyl6XeWGRj0nMpO.tail | 50 + .../0006_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0RFY0qyl6XeWlU8f2VRe.tail | 50 + .../0008_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0SFY0qyl6XeWWOPVOd70.tail | 50 + .../0000_post_v1_payment_methods.tail | 59 + ...t_intents_pi_1HcI17FY0qyl6XeWcFAAbZCw.tail | 98 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1GciHFFY0qyl6XeWp9RdhmFF.tail | 88 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail | 85 + ...t_intents_pi_3PXrkJIFbdis1OxT0XLmWug3.tail | 92 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1HDdfSFY0qyl6XeWjk7ogYVV.tail | 90 + ...t_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail | 84 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_3Nsu6oKG6vc7r7YC1FJJPNjg.tail | 83 + .../0000_post_v1_payment_methods.tail | 55 + ...t_intents_pi_1HlYxxBte6TMTRd48W66zjTJ.tail | 85 + .../0000_post_v1_payment_methods.tail | 68 + .../0000_post_v1_payment_methods.tail | 68 + .../0000_post_v1_payment_methods.tail | 68 + .../0000_post_v1_payment_methods.tail | 68 + ...t_intents_pi_3KhHLqFY0qyl6XeW1X2ZMsOT.tail | 106 + .../0000_post_v1_radar_session.tail | 32 + .../0001_post_v1_radar_session.tail | 32 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0aF7QokQdxByBD1C7K6i_confirm.tail | 49 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0bFY0qyl6XeWH07n8saf_confirm.tail | 55 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0cFY0qyl6XeWrqnyI4iv_confirm.tail | 98 + ...0qyl6XeWrqnyI4iv_verify_microdeposits.tail | 54 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0dFY0qyl6XeW8aP6E5LA_confirm.tail | 98 + ...0qyl6XeW8aP6E5LA_verify_microdeposits.tail | 54 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1GGCuIFY0qyl6XeWVfbQK6b3.tail | 45 + .../0000_post_v1_sources.tail | 65 + .../0000_post_v1_sources.tail | 68 + .../0000_post_v1_sources.tail | 76 + .../0000_post_v1_sources.tail | 64 + .../0000_post_v1_sources.tail | 64 + .../0000_post_v1_sources.tail | 66 + .../0000_post_v1_sources.tail | 66 + .../0000_post_v1_sources.tail | 66 + .../0000_post_v1_sources.tail | 109 + .../0000_post_v1_sources.tail | 80 + .../0000_post_v1_sources.tail | 63 + .../0000_post_v1_sources.tail | 87 + .../0000_post_v1_sources.tail | 80 + .../0000_post_v1_sources.tail | 69 + .../0000_post_v1_sources.tail | 65 + .../0000_post_v1_sources.tail | 69 + ..._sources_src_1PiS0oBbvEcIpqUbSyzeANyO.tail | 65 + .../testCVCUpdate/0000_post_v1_tokens.tail | 39 + .../testCardToken/0000_post_v1_tokens.tail | 65 + .../0000_post_create_payment_intent.tail | 18 + .../0001_post_create_payment_intent.tail | 18 + ...s_pi_3PiS0qFY0qyl6XeW13o46Tjh_confirm.tail | 68 + ...s_pi_3PiS0qFY0qyl6XeW0SQF0wEH_confirm.tail | 68 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0rFY0qyl6XeWQlOZvEao_confirm.tail | 49 + .../0000_post_v1_tokens.tail | 37 + .../0001_post_v1_tokens.tail | 37 + .../0002_post_v1_tokens.tail | 37 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_payment_methods.tail | 77 + .../0000_post_v1_radar_session.tail | 33 + .../0000_post_v1_sources.tail | 69 + ..._sources_src_1PiS0tFY0qyl6XeWYLZvQ3TJ.tail | 65 + ..._sources_src_1PiS0tFY0qyl6XeWYLZvQ3TJ.tail | 65 + .../0000_post_v1_tokens.tail | 53 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_tokens.tail | 39 + .../0000_post_v1_tokens.tail | 41 + .../0000_post_create_payment_intent.tail | 18 + ...s_pi_3PiS0vFY0qyl6XeW1D8gxVy2_refresh.tail | 68 + .../0000_post_create_setup_intent.tail | 18 + ...seti_1PiS0wFY0qyl6XeWYrQ3d3Gb_refresh.tail | 49 + .../0000_post_create_payment_intent.tail | 18 + ...t_intents_pi_3PiS0xFY0qyl6XeW1QvdM01s.tail | 64 + ...t_intents_pi_3PiS0xFY0qyl6XeW1QvdM01s.tail | 64 + .../0000_post_create_setup_intent.tail | 18 + ...intents_seti_1PiS0xFY0qyl6XeWSVqYxcJi.tail | 45 + .../testUploadFile/0000_post_v1_files.tail | 38 + .../0000_get_edge-internal_card-metadata.tail | 37 + .../0000_get_edge-internal_card-metadata.tail | 24 + .../0001_get_edge-internal_card-metadata.tail | 24 + .../0000_get_edge-internal_card-metadata.tail | 24 + .../STPFixtures+Swift.swift | 111 + .../STPNetworkStubbingTestCase.swift | 225 ++ .../STPTestAPIClient+Swift.swift | 11 + .../STPTestingAPIClient+Swift.swift | 171 + .../StripePaymentsTestUtils.h | 16 + .../Captcha/DispatchQueue__Tests.swift | 188 + .../Captcha/HCaptchaDecoder__Tests.swift | 219 ++ .../Captcha/HCaptchaResult__Tests.swift | 38 + .../HCaptchaWebViewManager__HTML__Tests.swift | 58 + .../HCaptchaWebViewManager__Tests.swift | 649 ++++ .../Captcha/HCaptcha__Bench.swift | 87 + .../Captcha/HCaptcha__Config__Tests.swift | 135 + .../Captcha/HCaptcha__Tests.swift | 142 + .../Helpers/HCaptchaConfig+Helpers.swift | 36 + .../Helpers/HCaptchaDecoder+Helper.swift | 55 + .../Helpers/HCaptchaError+Equatable.swift | 46 + .../HCaptchaWebViewManager+Helpers.swift | 82 + StripePayments/StripePaymentsTests/Info.plist | 22 + .../STPAnalyticsClient+StripePayments.swift | 29 + StripePayments/StripePaymentsTests/mock.html | 65 + StripePaymentsUI/README.md | 44 + .../project.pbxproj | 961 +++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripePaymentsUI.xcscheme | 102 + .../Docs.docc/StripePaymentsUI.md | 3 + StripePaymentsUI/StripePaymentsUI/Info.plist | 22 + .../Resources/JSON/au_becs_bsb.json | 373 ++ .../bg-BG.lproj/Localizable.strings | 67 + .../ca-ES.lproj/Localizable.strings | 67 + .../cs-CZ.lproj/Localizable.strings | 67 + .../da.lproj/Localizable.strings | 67 + .../de.lproj/Localizable.strings | 67 + .../el-GR.lproj/Localizable.strings | 67 + .../en-GB.lproj/Localizable.strings | 67 + .../en.lproj/Localizable.strings | 102 + .../es-419.lproj/Localizable.strings | 67 + .../es.lproj/Localizable.strings | 67 + .../et-EE.lproj/Localizable.strings | 67 + .../fi.lproj/Localizable.strings | 67 + .../fil.lproj/Localizable.strings | 67 + .../fr-CA.lproj/Localizable.strings | 67 + .../fr.lproj/Localizable.strings | 67 + .../hr.lproj/Localizable.strings | 67 + .../hu.lproj/Localizable.strings | 67 + .../id.lproj/Localizable.strings | 67 + .../it.lproj/Localizable.strings | 67 + .../ja.lproj/Localizable.strings | 67 + .../ko.lproj/Localizable.strings | 67 + .../lt-LT.lproj/Localizable.strings | 67 + .../lv-LV.lproj/Localizable.strings | 67 + .../ms-MY.lproj/Localizable.strings | 67 + .../mt.lproj/Localizable.strings | 67 + .../nb.lproj/Localizable.strings | 67 + .../nl.lproj/Localizable.strings | 67 + .../nn-NO.lproj/Localizable.strings | 67 + .../pl-PL.lproj/Localizable.strings | 67 + .../pt-BR.lproj/Localizable.strings | 67 + .../pt-PT.lproj/Localizable.strings | 67 + .../ro-RO.lproj/Localizable.strings | 67 + .../ru.lproj/Localizable.strings | 67 + .../sk-SK.lproj/Localizable.strings | 67 + .../sl-SI.lproj/Localizable.strings | 67 + .../sv.lproj/Localizable.strings | 67 + .../tk.lproj/Localizable.strings | 0 .../tr.lproj/Localizable.strings | 67 + .../vi.lproj/Localizable.strings | 67 + .../zh-HK.lproj/Localizable.strings | 67 + .../zh-Hans.lproj/Localizable.strings | 67 + .../zh-Hant.lproj/Localizable.strings | 67 + .../BECS/Contents.json | 6 + .../BECS/anz.imageset/Contents.json | 12 + .../BECS/anz.imageset/anz.pdf | Bin 0 -> 1535 bytes .../bankofmelbourne.imageset/Contents.json | 12 + .../bankofmelbourne.pdf | Bin 0 -> 1194 bytes .../BECS/banksa.imageset/Contents.json | 12 + .../BECS/banksa.imageset/banksa.pdf | Bin 0 -> 4237 bytes .../BECS/bankwest.imageset/Contents.json | 12 + .../BECS/bankwest.imageset/bankwest.pdf | Bin 0 -> 2938 bytes .../BECS/boq.imageset/Contents.json | 12 + .../BECS/boq.imageset/boq.pdf | Bin 0 -> 2071 bytes .../BECS/cba.imageset/Contents.json | 12 + .../BECS/cba.imageset/cba.pdf | Bin 0 -> 1400 bytes .../BECS/nab.imageset/Contents.json | 12 + .../BECS/nab.imageset/nab.pdf | Bin 0 -> 1981 bytes .../BECS/stgeorges.imageset/Contents.json | 12 + .../BECS/stgeorges.imageset/stgeorges.pdf | Bin 0 -> 4832 bytes .../BECS/stripe.imageset/Contents.json | 12 + .../BECS/stripe.imageset/stripe.pdf | Bin 0 -> 1151 bytes .../BECS/suncorpmetway.imageset/Contents.json | 12 + .../suncorpmetway.imageset/suncorpmetway.pdf | Bin 0 -> 2069 bytes .../BECS/westpac.imageset/Contents.json | 12 + .../BECS/westpac.imageset/westpac.pdf | Bin 0 -> 1168 bytes .../Cards/Contents.json | 6 + .../stp_card_amex.imageset/Contents.json | 15 + .../stp_card_amex.imageset/icon-card-amex.svg | 9 + .../Contents.json | 15 + .../vector (6).svg | 11 + .../stp_card_applepay.imageset/Contents.json | 26 + .../stp_card_applepay.png | Bin 0 -> 653 bytes .../stp_card_applepay@2x.png | Bin 0 -> 1385 bytes .../stp_card_applepay@3x.png | Bin 0 -> 2221 bytes .../Contents.json | 15 + .../icon-card-cartebancaire.svg | 16 + .../Contents.json | 15 + .../vector (12).svg | 10 + .../Cards/stp_card_cbc.imageset/Contents.json | 25 + .../Cards/stp_card_cbc.imageset/cbcDark.svg | 5 + .../Cards/stp_card_cbc.imageset/cbcLight.svg | 5 + .../Cards/stp_card_cvc.imageset/Contents.json | 25 + .../Cards/stp_card_cvc.imageset/cvcDark.svg | 12 + .../Cards/stp_card_cvc.imageset/cvcLight.svg | 12 + .../stp_card_cvc_amex.imageset/Contents.json | 25 + .../cvcAmexDark.svg | 12 + .../cvcAmexLight.svg | 12 + .../stp_card_diners.imageset/Contents.json | 15 + .../icon-card-diners-club.svg | 6 + .../Contents.json | 15 + .../Diners Club-bw.svg | 5 + .../stp_card_discover.imageset/Contents.json | 15 + .../icon-card-discover.svg | 7 + .../Contents.json | 15 + .../vector (8).svg | 11 + .../stp_card_error.imageset/Contents.json | 15 + .../stp_card_error.imageset/cardError.svg | 12 + .../Cards/stp_card_jcb.imageset/Contents.json | 15 + .../stp_card_jcb.imageset/icon-card-jcb.svg | 8 + .../Contents.json | 15 + .../vector (9).svg | 31 + .../stp_card_maestro.imageset/Contents.json | 15 + .../icon-card-maestro.svg | 6 + .../Contents.json | 15 + .../icon-card-mastercard.svg | 6 + .../Contents.json | 26 + .../stp_card_mastercard_template.png | Bin 0 -> 1578 bytes .../stp_card_mastercard_template@2x.png | Bin 0 -> 2965 bytes .../stp_card_mastercard_template@3x.png | Bin 0 -> 1684 bytes .../stp_card_unionpay.imageset/Contents.json | 15 + .../icon-card-unionpay.svg | 6 + .../Contents.json | 15 + .../vector (7).svg | 11 + .../stp_card_unknown.imageset/Contents.json | 25 + .../stp_card_unknown.imageset/cardDark.svg | 4 + .../stp_card_unknown.imageset/cardLight.svg | 4 + .../stp_card_visa.imageset/Contents.json | 15 + .../stp_card_visa.imageset/icon-card-visa.svg | 5 + .../Contents.json | 15 + .../vector (11).svg | 11 + .../CardsNoPadding/Contents.json | 6 + .../Contents.json | 12 + .../stp_card_unpadded_amex.svg | 9 + .../Contents.json | 12 + .../cartesBancairesNoPadding.svg | 16 + .../Contents.json | 12 + .../stp_card_unpadded_diners_club.svg | 6 + .../Contents.json | 12 + .../stp_card_unpadded_discover.svg | 7 + .../Contents.json | 12 + .../stp_card_unpadded_jcb.svg | 8 + .../Contents.json | 12 + .../stp_card_unpadded_maestro.svg | 6 + .../Contents.json | 12 + .../stp_card_unpadded_mastercard.svg | 6 + .../Contents.json | 12 + .../stp_card_unpadded_unionpay.svg | 6 + .../Contents.json | 12 + .../visaNoPadding.svg | 5 + .../StripePaymentsUI.xcassets/Contents.json | 6 + .../stp_icon_bank.imageset/Contents.json | 12 + .../icon-pm-bank-color.svg | 3 + .../Enums+CustomStringConvertible.swift | 49 + .../Helpers/CardElementConfigService.swift | 83 + .../STPBECSDebitAccountNumberValidator.swift | 84 + .../Helpers/STPBSBNumberValidator.swift | 143 + .../Source/Helpers/STPCBCController.swift | 166 + .../Source/Helpers/STPImageLibrary.swift | 181 + .../Source/Helpers/STPLocalizedString.swift | 16 + .../Helpers/STPPhoneNumberValidator.swift | 111 + .../Helpers/STPPostalCodeValidator.swift | 296 ++ .../Source/Helpers/STPPromise.swift | 138 + .../Source/Helpers/STPStringUtils.swift | 69 + .../Source/Helpers/String+Localized.swift | 150 + .../Helpers/StripePayments+Export.swift | 11 + .../Helpers/StripePaymentsBundleLocator.swift | 19 + .../NSAttributedString+Stripe.swift | 19 + .../Categories/STPCardBrand+PaymentsUI.swift | 46 + .../Internal/Categories/UIButton+Stripe.swift | 47 + .../DropDownFieldElement+CardBrand.swift | 47 + .../Internal/UI/Views/CardBrandView.swift | 253 ++ .../UI/Views/DynamicImageView+Unknown.swift | 18 + .../Card/STPCardCVCInputTextField.swift | 93 + .../STPCardCVCInputTextFieldFormatter.swift | 37 + .../STPCardCVCInputTextFieldValidator.swift | 51 + .../Card/STPCardExpiryInputTextField.swift | 51 + ...STPCardExpiryInputTextFieldFormatter.swift | 84 + ...STPCardExpiryInputTextFieldValidator.swift | 81 + .../Card/STPCardNumberInputTextField.swift | 182 + ...STPCardNumberInputTextFieldFormatter.swift | 82 + ...STPCardNumberInputTextFieldValidator.swift | 95 + .../Card/STPPostalCodeInputTextField.swift | 89 + ...STPPostalCodeInputTextFieldFormatter.swift | 48 + ...STPPostalCodeInputTextFieldValidator.swift | 76 + .../Inputs/STPCountryPickerInputField.swift | 135 + .../Inputs/STPGenericInputPickerField.swift | 231 ++ .../Inputs/STPGenericInputTextField.swift | 62 + .../UI/Views/Inputs/STPInputTextField.swift | 296 ++ .../Inputs/STPInputTextFieldFormatter.swift | 62 + .../Inputs/STPInputTextFieldValidator.swift | 65 + .../STPNumericDigitInputTextFormatter.swift | 50 + .../UI/Views/STPAUBECSFormViewModel.swift | 198 + .../UI/Views/STPCardLoadingIndicator.swift | 102 + .../Internal/UI/Views/STPFormTextField.swift | 484 +++ .../Views/STPLabeledFormTextFieldView.swift | 115 + .../STPLabeledMultiFormTextFieldView.swift | 122 + .../STPPaymentCardTextFieldViewModel.swift | 260 ++ .../UI/Views/STPValidatedTextField.swift | 99 + .../UI/Views/STPViewWithSeparator.swift | 89 + ...entMethodMessagingView+Configuration.swift | 86 + .../PaymentMethodMessagingView.swift | 355 ++ .../STPAUBECSDebitFormView.swift | 573 +++ .../STPCardFormView+SwiftUI.swift | 66 + .../UI Components/STPCardFormView.swift | 831 +++++ .../STPFloatingPlaceholderTextField.swift | 422 +++ .../STPFormTextFieldContainer.swift | 33 + .../Source/UI Components/STPFormView.swift | 695 ++++ .../UI Components/STPMultiFormTextField.swift | 337 ++ .../STPPaymentCardTextField+SwiftUI.swift | 66 + .../STPPaymentCardTextField.swift | 2488 +++++++++++++ .../StripePaymentsUI/StripePaymentsUI.h | 18 + .../CardElementConfigServiceTests.swift | 103 + .../StripePaymentsUITests/Info.plist | 22 + .../STPAnalyticsClient+PaymentsUITests.swift | 32 + ...STPPaymentCardTextFieldSnapshotTests.swift | 58 + .../StripeUICore.xcodeproj/project.pbxproj | 1180 ++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/StripeUICore.xcscheme | 104 + StripeUICore/StripeUICore/Info.plist | 22 + .../Resources/JSON/au_becs_bsb.json | 123 + .../JSON/localized_address_data.json | 1197 ++++++ .../bg-BG.lproj/Localizable.strings | 143 + .../ca-ES.lproj/Localizable.strings | 143 + .../cs-CZ.lproj/Localizable.strings | 143 + .../da.lproj/Localizable.strings | 143 + .../de.lproj/Localizable.strings | 143 + .../el-GR.lproj/Localizable.strings | 143 + .../en-GB.lproj/Localizable.strings | 143 + .../en.lproj/Localizable.strings | 223 ++ .../es-419.lproj/Localizable.strings | 143 + .../es.lproj/Localizable.strings | 143 + .../et-EE.lproj/Localizable.strings | 143 + .../fi.lproj/Localizable.strings | 143 + .../fil.lproj/Localizable.strings | 143 + .../fr-CA.lproj/Localizable.strings | 143 + .../fr.lproj/Localizable.strings | 143 + .../hr.lproj/Localizable.strings | 143 + .../hu.lproj/Localizable.strings | 143 + .../id.lproj/Localizable.strings | 143 + .../it.lproj/Localizable.strings | 143 + .../ja.lproj/Localizable.strings | 143 + .../ko.lproj/Localizable.strings | 143 + .../lt-LT.lproj/Localizable.strings | 143 + .../lv-LV.lproj/Localizable.strings | 143 + .../ms-MY.lproj/Localizable.strings | 143 + .../mt.lproj/Localizable.strings | 143 + .../nb.lproj/Localizable.strings | 143 + .../nl.lproj/Localizable.strings | 143 + .../nn-NO.lproj/Localizable.strings | 143 + .../pl-PL.lproj/Localizable.strings | 143 + .../pt-BR.lproj/Localizable.strings | 143 + .../pt-PT.lproj/Localizable.strings | 143 + .../ro-RO.lproj/Localizable.strings | 143 + .../ru.lproj/Localizable.strings | 143 + .../sk-SK.lproj/Localizable.strings | 143 + .../sl-SI.lproj/Localizable.strings | 143 + .../sv.lproj/Localizable.strings | 143 + .../tr.lproj/Localizable.strings | 143 + .../vi.lproj/Localizable.strings | 143 + .../zh-HK.lproj/Localizable.strings | 143 + .../zh-Hans.lproj/Localizable.strings | 143 + .../zh-Hant.lproj/Localizable.strings | 143 + .../StripeUICore.xcassets/Contents.json | 6 + .../brand_stripe.imageset/Contents.json | 12 + .../brand_stripe.imageset/brand_stripe.svg | 11 + .../form_error_icon.imageset/Contents.json | 22 + .../form_error_icon.pdf | Bin 0 -> 2348 bytes .../form_error_icon_dark.pdf | Bin 0 -> 2348 bytes .../icon_chevron_down.imageset/Contents.json | 12 + .../chevronDown.pdf | Bin 0 -> 1154 bytes .../icon_clear.imageset/Contents.json | 12 + .../icon_clear.imageset/icon_clear.svg | 3 + .../Categories/CALayer+StripeUICore.swift | 26 + .../Enums+CustomStringConvertible.swift | 7 + .../Categories/Locale+StripeUICore.swift | 41 + .../NSAttributedString+StripeUICore.swift | 31 + ...NSDirectionalEdgeInsets+StripeUICore.swift | 19 + .../UIBarButtonItem+StripeUICore.swift | 21 + .../Categories/UIButton+StripeUICore.swift | 51 + .../Categories/UIColor+StripeUICore.swift | 182 + .../Categories/UIFont+StripeUICore.swift | 31 + .../UIKeyboardType+StripeUICore.swift | 33 + ...ISpringTimingParameters+StripeUICore.swift | 22 + .../Categories/UIStackView+StripeUICore.swift | 122 + .../UITraitCollection+StripeUICore.swift | 17 + .../Categories/UIView+StripeUICore.swift | 115 + .../UIViewController+StripeUICore.swift | 54 + .../Categories/UIWindow+StripeUICore.swift | 20 + .../Source/Controls/ActivityIndicator.swift | 267 ++ .../StripeUICore/Source/Controls/Button.swift | 559 +++ .../OneTimeCodeTextField-TextStorage.swift | 217 ++ .../Controls/OneTimeCodeTextField.swift | 840 +++++ .../Elements/Checkbox/CheckboxButton.swift | 337 ++ .../Elements/Checkbox/CheckboxElement.swift | 63 + .../Source/Elements/ContainerElement.swift | 84 + .../Source/Elements/DateFieldElement.swift | 196 + .../Elements/DropdownFieldElement.swift | 306 ++ .../Source/Elements/Element.swift | 103 + .../Source/Elements/ElementsUI.swift | 131 + ...dressSectionElement+DummyAddressLine.swift | 72 + .../Address/AddressSectionElement.swift | 432 +++ .../Address/AddressSpec+ElementFactory.swift | 55 + .../Factories/Address/AddressSpec.swift | 147 + .../Address/AddressSpecProvider.swift | 67 + .../Factories/BSB/BSBNumberProvider.swift | 72 + .../DropdownFieldElement+AddressFactory.swift | 54 + .../IDNumberTextFieldConfiguration.swift | 142 + .../TextFieldElement+AccountFactory.swift | 145 + .../TextFieldElement+AddressFactory.swift | 147 + .../Factories/TextFieldElement+Factory.swift | 241 ++ .../Source/Elements/Form/FormElement.swift | 84 + .../Source/Elements/Form/FormView.swift | 61 + .../PhoneNumber/PhoneNumberElement.swift | 190 + .../PickerField/PickerFieldView.swift | 272 ++ .../PickerField/PickerTextField.swift | 39 + .../Section/SectionContainerView.swift | 228 ++ .../SectionElement+MultiElementRow.swift | 30 + .../Elements/Section/SectionElement.swift | 137 + .../Source/Elements/Section/SectionView.swift | 72 + .../Source/Elements/StaticElement.swift | 27 + .../FloatingPlaceholderTextFieldView.swift | 199 + .../TextFieldElement+Validation.swift | 87 + .../Elements/TextField/TextFieldElement.swift | 191 + .../TextFieldElementConfiguration.swift | 144 + .../TextField/TextFieldFormatter.swift | 108 + .../Elements/TextField/TextFieldView.swift | 310 ++ .../Elements/TextOrDropdownElement.swift | 47 + StripeUICore/StripeUICore/Source/Events.swift | 30 + .../Source/Helpers/CompatibleColor.swift | 15 + .../Source/Helpers/ImageMaker.swift | 64 + .../Source/Helpers/InputFormColors.swift | 45 + .../Source/Helpers/RegionCodeProvider.swift | 14 + .../Source/Helpers/STPLocalizedString.swift | 13 + .../Helpers/StackViewWithSeparator.swift | 215 ++ .../Source/Helpers/String+CountryEmoji.swift | 27 + .../Source/Helpers/String+Localized.swift | 355 ++ .../Helpers/String+RegionCodeProvider.swift | 16 + .../Helpers/StripeUICoreBundleLocator.swift | 19 + StripeUICore/StripeUICore/Source/Image.swift | 30 + .../Source/Validators/BankRoutingNumber.swift | 61 + .../Source/Validators/PhoneNumber.swift | 441 +++ .../Validators/STPBlikCodeValidator.swift | 19 + .../Validators/STPEmailAddressValidator.swift | 29 + .../Validators/STPVPANumberValidator.swift | 28 + .../Source/Views/DoneButtonToolbar.swift | 74 + .../Views/DynamicHeightContainerView.swift | 73 + .../Source/Views/DynamicImageView.swift | 52 + .../Source/Views/LinkOpeningTextView.swift | 57 + StripeUICore/StripeUICore/StripeUICore.h | 18 + StripeUICore/StripeUICoreTests/Info.plist | 22 + .../Controls/ButtonSnapshotTest.swift | 100 + .../AddressSectionElementSnapshotTest.swift | 33 + .../CheckboxButtonSnapshotTests.swift | 106 + .../DateFieldElementSnapshotTest.swift | 81 + .../DropdownFieldElementSnapshotTest.swift | 58 + .../PhoneNumberElementSnapshotTests.swift | 64 + .../Categories/Locale+StripeUICoreTests.swift | 73 + ...NSAttributedString+StripeUICoreTests.swift | 25 + .../UIColor+StripeUICoreTests.swift | 222 ++ .../Elements/AddressSectionElementTest.swift | 233 ++ .../Elements/AddressSpecProviderTest.swift | 50 + .../Unit/Elements/BSBNumberProviderTest.swift | 76 + .../Unit/Elements/DateFieldElementTest.swift | 85 + .../Elements/DropdownFieldElementTest.swift | 96 + .../IDNumberTextFieldConfigurationTest.swift | 108 + .../Elements/PhoneNumberElementTests.swift | 177 + .../Unit/Elements/SectionElementTest.swift | 61 + .../TestFieldElement+AccountFactoryTest.swift | 62 + .../TextFieldElement+AddressFactoryTest.swift | 137 + .../Unit/Elements/TextFieldElementTest.swift | 78 + .../Elements/TextFieldFormatterTest.swift | 70 + .../Unit/Validators/BSBNumberTests.swift | 81 + .../Unit/Validators/PhoneNumberTests.swift | 199 + .../Validators/STPBlikCodeValidatorTest.swift | 37 + .../STPEmailAddressValidatorTest.swift | 26 + .../STPVPANumberValidatorTest.swift | 31 + VERSION | 1 + 4419 files changed, 412867 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 MIGRATING.md create mode 100644 Package.swift create mode 100644 Stripe/BuildConfigurations/Stripe Tests-Debug.xcconfig create mode 100644 Stripe/BuildConfigurations/Stripe Tests-Release.xcconfig create mode 100644 Stripe/BuildConfigurations/Stripe-Debug.xcconfig create mode 100644 Stripe/BuildConfigurations/Stripe-Release.xcconfig create mode 100644 Stripe/Stripe.xcodeproj/project.pbxproj create mode 100644 Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOS.xcscheme create mode 100644 Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOSTestHostApp.xcscheme create mode 100644 Stripe/StripeiOS/Docs.docc/Stripe.md create mode 100644 Stripe/StripeiOS/Info.plist create mode 100644 Stripe/StripeiOS/PrivacyInfo.xcprivacy create mode 100644 Stripe/StripeiOS/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/tk.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/stp_card_form_back.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/stp_card_form_back@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/stp_card_form_back@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/stp_bank_fpx_affin_bank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/stp_bank_fpx_affin_bank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/stp_bank_fpx_affin_bank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_muamalat.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_muamalat.imageset/stp_bank_fpx_bank_muamalat.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_muamalat.imageset/stp_bank_fpx_bank_muamalat@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_muamalat.imageset/stp_bank_fpx_bank_muamalat@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/stp_bank_fpx_hong_leong_bank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/stp_bank_fpx_hong_leong_bank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/stp_bank_fpx_hong_leong_bank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/stp_bank_fpx_kfh.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/stp_bank_fpx_kfh@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/stp_bank_fpx_kfh@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/stp_bank_fpx_maybank2e.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/stp_bank_fpx_maybank2e@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/stp_bank_fpx_maybank2e@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/stp_bank_fpx_maybank2u.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/stp_bank_fpx_maybank2u@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/stp_bank_fpx_maybank2u@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/stp_bank_fpx_ocbc.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/stp_bank_fpx_ocbc@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/stp_bank_fpx_ocbc@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/stp_bank_fpx_public_bank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/stp_bank_fpx_public_bank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/stp_bank_fpx_public_bank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_logo.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_logo.imageset/stp_fpx_logo.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_logo.imageset/stp_fpx_logo@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_logo.imageset/stp_fpx_logo@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/stp_icon_bank.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/stp_icon_bank@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/stp_icon_bank@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@3x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/Contents.json create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@2x.png create mode 100644 Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@3x.png create mode 100644 Stripe/StripeiOS/Source/Enums+CustomStringConvertible.swift create mode 100644 Stripe/StripeiOS/Source/PKAddPaymentPassRequest+Stripe_Error.swift create mode 100644 Stripe/StripeiOS/Source/PKPaymentAuthorizationViewController+Stripe_Blocks.swift create mode 100644 Stripe/StripeiOS/Source/STPAPIClient+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPAPIClient+PushProvisioning.swift create mode 100644 Stripe/StripeiOS/Source/STPAddCardViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPAddress+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPAddressFieldTableViewCell.swift create mode 100644 Stripe/StripeiOS/Source/STPAddressViewModel.swift create mode 100644 Stripe/StripeiOS/Source/STPAnalyticsClient+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPAnalyticsClient+Payments.swift create mode 100644 Stripe/StripeiOS/Source/STPApplePayContextDelegate.swift create mode 100644 Stripe/StripeiOS/Source/STPApplePayPaymentOption.swift create mode 100644 Stripe/StripeiOS/Source/STPBackendAPIAdapter.swift create mode 100644 Stripe/StripeiOS/Source/STPBankSelectionTableViewCell.swift create mode 100644 Stripe/StripeiOS/Source/STPBankSelectionViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPBasicUIAnalyticsSerializer.swift create mode 100644 Stripe/StripeiOS/Source/STPBlocks.swift create mode 100644 Stripe/StripeiOS/Source/STPCameraView.swift create mode 100644 Stripe/StripeiOS/Source/STPCard+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPCardScanner.swift create mode 100644 Stripe/StripeiOS/Source/STPCardScannerTableViewCell.swift create mode 100644 Stripe/StripeiOS/Source/STPCardValidationState.swift create mode 100644 Stripe/StripeiOS/Source/STPCoreScrollViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPCoreTableViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPCoreViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPCustomerContext.swift create mode 100644 Stripe/StripeiOS/Source/STPEphemeralKey.swift create mode 100644 Stripe/StripeiOS/Source/STPEphemeralKeyManager.swift create mode 100644 Stripe/StripeiOS/Source/STPEphemeralKeyProvider.swift create mode 100644 Stripe/StripeiOS/Source/STPFPXBankStatusResponse.swift create mode 100644 Stripe/StripeiOS/Source/STPFakeAddPaymentPassViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPImageLibrary.swift create mode 100644 Stripe/StripeiOS/Source/STPIntentActionLinkAuthenticateAccount.swift create mode 100644 Stripe/StripeiOS/Source/STPLocalizedString.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentActivityIndicatorView.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentCardTextFieldCell.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentConfiguration.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentContext.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentContextAmountModel.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentIntentParams+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentMethod+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentMethodParams+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentOption.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentOptionTableViewCell.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentOptionTuple.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentOptionsInternalViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentOptionsViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPPaymentResult.swift create mode 100644 Stripe/StripeiOS/Source/STPPinManagementService.swift create mode 100644 Stripe/StripeiOS/Source/STPPushProvisioningContext.swift create mode 100644 Stripe/StripeiOS/Source/STPPushProvisioningDetails.swift create mode 100644 Stripe/StripeiOS/Source/STPPushProvisioningDetailsParams.swift create mode 100644 Stripe/StripeiOS/Source/STPSectionHeaderView.swift create mode 100644 Stripe/StripeiOS/Source/STPShippingAddressViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPShippingMethodTableViewCell.swift create mode 100644 Stripe/StripeiOS/Source/STPShippingMethodsViewController.swift create mode 100644 Stripe/StripeiOS/Source/STPSource+BasicUI.swift create mode 100644 Stripe/StripeiOS/Source/STPTheme.swift create mode 100644 Stripe/StripeiOS/Source/STPUserInformation.swift create mode 100644 Stripe/StripeiOS/Source/String+Localized.swift create mode 100644 Stripe/StripeiOS/Source/Stripe+Exports.swift create mode 100644 Stripe/StripeiOS/Source/StripeBundleLocator.swift create mode 100644 Stripe/StripeiOS/Source/UIBarButtonItem+Stripe.swift create mode 100644 Stripe/StripeiOS/Source/UINavigationBar+Stripe_Theme.swift create mode 100644 Stripe/StripeiOS/Source/UINavigationController+Stripe_Completion.swift create mode 100644 Stripe/StripeiOS/Source/UITableViewCell+Stripe_Borders.swift create mode 100644 Stripe/StripeiOS/Source/UIToolbar+Stripe_InputAccessory.swift create mode 100644 Stripe/StripeiOS/Source/UIView+Stripe_FirstResponder.swift create mode 100644 Stripe/StripeiOS/Source/UIView+Stripe_SafeAreaBounds.swift create mode 100644 Stripe/StripeiOS/Source/UIViewController+Stripe_KeyboardAvoiding.swift create mode 100644 Stripe/StripeiOS/Source/UIViewController+Stripe_NavigationItemProxy.swift create mode 100644 Stripe/StripeiOS/Source/UIViewController+Stripe_ParentViewController.swift create mode 100644 Stripe/StripeiOS/Source/UserDefaults+Stripe.swift create mode 100644 Stripe/StripeiOS/Stripe-umbrella.h create mode 100644 Stripe/StripeiOS/Stripe.modulemap create mode 100644 Stripe/StripeiOSAppHostedTests/Info.plist create mode 100644 Stripe/StripeiOSTestHostApp/AppDelegate.swift create mode 100644 Stripe/StripeiOSTestHostApp/Info.plist create mode 100644 Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/Contents.json create mode 100644 Stripe/StripeiOSTestHostApp/Resources/Base.lproj/LaunchScreen.storyboard create mode 100644 Stripe/StripeiOSTestHostApp/Resources/Base.lproj/Main.storyboard create mode 100644 Stripe/StripeiOSTestHostApp/ViewController.swift create mode 100644 Stripe/StripeiOSTests.xctestplan create mode 100644 Stripe/StripeiOSTests/APIRequestTest.swift create mode 100644 Stripe/StripeiOSTests/AfterpayPriceBreakdownViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/AnalyticsHelperTests.swift create mode 100644 Stripe/StripeiOSTests/AutoCompleteViewControllerSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/CardExpiryDateTests.swift create mode 100644 Stripe/StripeiOSTests/CircularButtonSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/ConfirmButtonSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/ConfirmButtonTests.swift create mode 100644 Stripe/StripeiOSTests/ConsumerSessionTests.swift create mode 100644 Stripe/StripeiOSTests/CustomerAdapterTests.swift create mode 100644 Stripe/StripeiOSTests/Error+PaymentSheetTests.swift create mode 100644 Stripe/StripeiOSTests/FBSnapshotTestCase+STPViewControllerLoading.swift create mode 100644 Stripe/StripeiOSTests/FormSpecProviderTest.swift create mode 100644 Stripe/StripeiOSTests/FraudDetectionDataTest.swift create mode 100644 Stripe/StripeiOSTests/HostedSurfaceTest.swift create mode 100644 Stripe/StripeiOSTests/ImageTest.swift create mode 100644 Stripe/StripeiOSTests/Info.plist create mode 100644 Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/LinkLegalTermsViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/LinkSignupViewModelTests.swift create mode 100644 Stripe/StripeiOSTests/MKPlacemark+PaymentSheetTests.swift create mode 100644 Stripe/StripeiOSTests/NSArray+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/NSDecimalNumber+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/NSDictionary+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/NSLocale+STPSwizzling.swift create mode 100644 Stripe/StripeiOSTests/NSString+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/NSURLComponents_StripeTest.swift create mode 100644 Stripe/StripeiOSTests/OneTimeCodeTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/OneTimeCodeTextFieldTests.swift create mode 100644 Stripe/StripeiOSTests/OperationDebouncerTests.swift create mode 100644 Stripe/StripeiOSTests/PKPayment+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/PayWithLinkButtonSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/PaymentAnalyticTest.swift create mode 100644 Stripe/StripeiOSTests/PaymentTypeCellSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/Resources/Images.xcassets/Contents.json create mode 100644 Stripe/StripeiOSTests/Resources/MockFiles/paymentIntentResponse.json create mode 100644 Stripe/StripeiOSTests/Resources/stp_test_upload_image.jpeg create mode 100644 Stripe/StripeiOSTests/RotatingCardBrandsViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/RotatingCardBrandsViewTests.swift create mode 100644 Stripe/StripeiOSTests/STPAPIClient+LinkAccountSessionTest.swift create mode 100644 Stripe/StripeiOSTests/STPAPIClientNetworkBridgeTest.swift create mode 100644 Stripe/StripeiOSTests/STPAPIClientStubbedTest.swift create mode 100644 Stripe/StripeiOSTests/STPAPIClientTest.swift create mode 100644 Stripe/StripeiOSTests/STPAPISettingsObjCBridgeTest.m create mode 100644 Stripe/StripeiOSTests/STPAUBECSDebitFormViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPAUBECSFormViewModelTests.swift create mode 100644 Stripe/StripeiOSTests/STPAddCardViewControllerLocalizationSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPAddCardViewControllerTest.swift create mode 100644 Stripe/StripeiOSTests/STPAddressTests.swift create mode 100644 Stripe/StripeiOSTests/STPAddressViewModelTest.swift create mode 100644 Stripe/StripeiOSTests/STPAnalyticsClientPaymentsTest.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayContextFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayContextFunctionalTestExtras.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayContextTest.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayPaymentOptionTest.swift create mode 100644 Stripe/StripeiOSTests/STPApplePayTest.swift create mode 100644 Stripe/StripeiOSTests/STPBECSDebitAccountNumberValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPBSBNumberValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPBankAccountFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPBankAccountParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPBankAccountTest.swift create mode 100644 Stripe/StripeiOSTests/STPBinRangeTest.swift create mode 100644 Stripe/StripeiOSTests/STPBlocks.h create mode 100644 Stripe/StripeiOSTests/STPCardBINMetadataTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardBrandTest.swift create mode 100644 Stripe/StripeiOSTests/STPCardCVCInputTextFieldFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardCVCInputTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardCVCInputTextFieldTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardCVCInputTextFieldValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardExpiryInputTextFieldFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardExpiryInputTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardExpiryInputTextFieldValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardFormViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardFormViewTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPCardNumberInputTextFieldFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardNumberInputTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardNumberInputTextFieldValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPCardParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPCardTest.swift create mode 100644 Stripe/StripeiOSTests/STPCardValidatorTest.swift create mode 100644 Stripe/StripeiOSTests/STPCertTest.swift create mode 100644 Stripe/StripeiOSTests/STPConfirmCardOptionsTest.swift create mode 100644 Stripe/StripeiOSTests/STPConfirmPaymentMethodOptionsTest.swift create mode 100644 Stripe/StripeiOSTests/STPConnectAccountAddressTest.swift create mode 100644 Stripe/StripeiOSTests/STPConnectAccountFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPConnectAccountParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPCountryPickerInputFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPCustomerContextTest.swift create mode 100644 Stripe/StripeiOSTests/STPCustomerTest.swift create mode 100644 Stripe/StripeiOSTests/STPE2ETest.swift create mode 100644 Stripe/StripeiOSTests/STPEphemeralKeyManagerTest.swift create mode 100644 Stripe/StripeiOSTests/STPEphemeralKeyTest.swift create mode 100644 Stripe/StripeiOSTests/STPErrorBridgeTest.m create mode 100644 Stripe/StripeiOSTests/STPFPXBankBrandTest.swift create mode 100644 Stripe/StripeiOSTests/STPFileFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPFileTest.swift create mode 100644 Stripe/StripeiOSTests/STPFloatingPlaceholderTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPFormEncoderTest.swift create mode 100644 Stripe/StripeiOSTests/STPFormTextFieldTest.swift create mode 100644 Stripe/StripeiOSTests/STPFormViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPGenericInputPickerFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPGenericInputPickerFieldValidatorTest.swift create mode 100644 Stripe/StripeiOSTests/STPGenericInputTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPImageLibraryTest.swift create mode 100644 Stripe/StripeiOSTests/STPInputTextFieldFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPInputTextFieldValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionAlipayHandleRedirectTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionMultibancoDisplayDetailsTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionPayNowDisplayQrCodeTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionPromptPayDisplayQrCodeTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionTypeTest.swift create mode 100644 Stripe/StripeiOSTests/STPIntentActionWeChatPayRedirectToAppTest.swift create mode 100644 Stripe/StripeiOSTests/STPLabeledFormTextFieldViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPLabeledMultiFormTextFieldViewSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPMandateCustomerAcceptanceParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPMandateDataParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPMandateOnlineParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPMocks.h create mode 100644 Stripe/StripeiOSTests/STPMocks.m create mode 100644 Stripe/StripeiOSTests/STPNumericDigitInputTextFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPNumericStringValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPPIIFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentCardTextFieldKVOTest.m create mode 100644 Stripe/StripeiOSTests/STPPaymentCardTextFieldTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentCardTextFieldTestsSwift.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentCardTextFieldViewModelTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentConfigurationTest.m create mode 100644 Stripe/StripeiOSTests/STPPaymentContextApplePayTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentContextSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.m create mode 100644 Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentHandlerRefreshTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentHandlerStubbedMockedFilesTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentHandlerTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentIntentEnumsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentIntentFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentIntentLastPaymentErrorTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentIntentParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentIntentTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAddressTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAffirmParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAffirmTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAlmaParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAlmaTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAmazonPayParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodAmazonPayTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBacsDebitTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBancontactParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBancontactTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBillieParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBillieTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTests+Link.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBoletoParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodBoletoTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardChecksTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardWalletMasterpassTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardWalletTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCardWalletVisaCheckoutTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCashAppParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodCashAppTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodEPSParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodEPSTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodFPXTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodGiropayParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodGiropayTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodGrabPayParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodKlarnaParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodKlarnaTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodMobilePayParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodMobilePayTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodMultibancoParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodMultibancoTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodNetBankingParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodNetBankingTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodOXXOParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodOXXOTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodOptionsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodPayPalParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodPayPalTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24ParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24Tests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodRevolutPayParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodRevolutPayTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSEPADebitTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSatispayTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSofortParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSofortTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSunbitTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSwishParamsTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodSwishTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodThreeDSecureUsageTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodUPIParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodUPITests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsStubbedTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentMethodiDEALTest.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPPaymentOptionsViewControllerTest.swift create mode 100644 Stripe/StripeiOSTests/STPPhoneNumberValidatorTest.swift create mode 100644 Stripe/StripeiOSTests/STPPinManagementServiceFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPPostalCodeInputTextFieldFormatterTests.swift create mode 100644 Stripe/StripeiOSTests/STPPostalCodeInputTextFieldSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPPostalCodeInputTextFieldTests.swift create mode 100644 Stripe/StripeiOSTests/STPPostalCodeInputTextFieldValidatorTests.swift create mode 100644 Stripe/StripeiOSTests/STPPostalCodeValidatorTest.swift create mode 100644 Stripe/StripeiOSTests/STPPushProvisioningDetailsFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPRadarSessionFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPRedirectContextTest.swift create mode 100644 Stripe/StripeiOSTests/STPSetupIntentConfirmParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPSetupIntentFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPSetupIntentLastSetupErrorTest.swift create mode 100644 Stripe/StripeiOSTests/STPSetupIntentTest.swift create mode 100644 Stripe/StripeiOSTests/STPShippingAddressViewControllerLocalizationSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPShippingAddressViewControllerTest.swift create mode 100644 Stripe/StripeiOSTests/STPShippingMethodsViewControllerLocalizationSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPSourceCardDetailsTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceFunctionalTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceOwnerTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceParamsTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceReceiverTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceRedirectTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceSEPADebitDetailsTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceTest.swift create mode 100644 Stripe/StripeiOSTests/STPSourceVerificationTest.swift create mode 100644 Stripe/StripeiOSTests/STPStackViewWithSeparatorSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/STPStringUtilsTest.swift create mode 100644 Stripe/StripeiOSTests/STPSwiftFixtures.swift create mode 100644 Stripe/StripeiOSTests/STPTextFieldDelegateProxyTests.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSButtonCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSFooterCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSLabelCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSNavigationBarCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSSelectionCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSTextFieldCustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPThreeDSUICustomizationTest.swift create mode 100644 Stripe/StripeiOSTests/STPTokenTest.swift create mode 100644 Stripe/StripeiOSTests/STPUIVCStripeParentViewControllerTests.swift create mode 100644 Stripe/StripeiOSTests/STPViewWithSeparatorSnapshotTests.swift create mode 100644 Stripe/StripeiOSTests/ServerErrorMapperTest.swift create mode 100644 Stripe/StripeiOSTests/StripeErrorTest.swift create mode 100644 Stripe/StripeiOSTests/StripeTests-Prefix.pch create mode 100644 Stripe/StripeiOSTests/StripeiOS Tests-Bridging-Header.h create mode 100644 Stripe/StripeiOSTests/TextFieldElement+IBANTest.swift create mode 100644 Stripe/StripeiOSTests/UINavigationBar+StripeTest.m create mode 100644 Stripe/StripeiOSTests/UserDefaults+StripeTest.swift create mode 100644 Stripe/StripeiOSTests/WalletHeaderViewSnapshotTests.swift create mode 100644 Stripe3DS2/BuildConfigurations/Project-Debug.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Project-Release.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Project-Shared.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2-Debug.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2-Release.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2-Shared.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Debug.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Release.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Shared.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Debug.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Release.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Shared.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Debug.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Release.xcconfig create mode 100644 Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Shared.xcconfig create mode 100644 Stripe3DS2/Stripe3DS2.xcodeproj/project.pbxproj create mode 100644 Stripe3DS2/Stripe3DS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2.xcscheme create mode 100644 Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2DemoUI.xcscheme create mode 100644 Stripe3DS2/Stripe3DS2/Info.plist create mode 100644 Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.h create mode 100644 Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.m create mode 100644 Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.h create mode 100644 Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.m create mode 100644 Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.h create mode 100644 Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.m create mode 100644 Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.h create mode 100644 Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.m create mode 100644 Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.h create mode 100644 Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.m create mode 100644 Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.h create mode 100644 Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.m create mode 100644 Stripe3DS2/Stripe3DS2/PrivacyInfo.xcprivacy create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/amex.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/cartes-bancaires.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/discover.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/ec_test.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/mastercard.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/ul-test.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/visa.der create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@1x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@2x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@3x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/amex-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/amex-logo.imageset/american-express@1x.pdf create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/cartes-bancaires-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/cartes-bancaires-logo.imageset/cartes-bancaires-logo.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/discover@1x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/error.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/error.imageset/error@1x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/error.imageset/error@2x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/error.imageset/error@3x.png create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/mastercard@1x.pdf create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-logo.imageset/visa@1x.pdf create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/Contents.json create mode 100644 Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/visa-white@1x.pdf create mode 100644 Stripe3DS2/Stripe3DS2/Resources/bg-BG.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ca-ES.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/cs-CZ.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/da.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/de.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/el-GR.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/en-GB.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/en.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/es-419.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/es.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/et-EE.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/fi.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/fil.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/fr-CA.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/fr.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/hr.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/hu.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/id.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/it.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ja.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ko.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/lt-LT.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/lv-LV.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ms-MY.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/mt.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/nb.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/nl.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/nn-NO.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/pl-PL.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/pt-BR.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/pt-PT.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ro-RO.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/ru.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/sk-SK.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/sl-SI.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/sv.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/tr.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/vi.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/zh-HK.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/zh-Hans.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/Resources/zh-Hant.lproj/Localizable.strings create mode 100644 Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSBrandingView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSBrandingView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSBundleLocator.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSBundleLocator.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponse.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseImage.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtension.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfo.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformation.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformation.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter+Private.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSDirectoryServer.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate+Internal.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair+Testing.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSException+Internal.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSIPAddress.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSIPAddress.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSImageLoader.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSImageLoader.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSLocalizedString.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSProcessingView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSProcessingView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSProgressViewController.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSProgressViewController.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSSelectionButton.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSSelectionButton.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSSpacerView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSSpacerView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSStackView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSStackView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSTextChallengeView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSTextChallengeView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion+Private.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSTransaction+Private.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSVisionSupport.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSWebView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSWebView.m create mode 100644 Stripe3DS2/Stripe3DS2/STDSWhitelistView.h create mode 100644 Stripe3DS2/Stripe3DS2/STDSWhitelistView.m create mode 100644 Stripe3DS2/Stripe3DS2/Stripe3DS2-Bridging-Header.h create mode 100644 Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.h create mode 100644 Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.m create mode 100644 Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.h create mode 100644 Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.m create mode 100644 Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.h create mode 100644 Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.m create mode 100644 Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.h create mode 100644 Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.m create mode 100644 Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.h create mode 100644 Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.m create mode 100644 Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.h create mode 100644 Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSAuthenticationResponse.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSChallengeStatusReceiver.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSException.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSException.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSJSONDecodable.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSJSONEncodable.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSThreeDSProtocolVersion.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSTransaction.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSTransaction.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSUICustomization.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSUICustomization.m create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSWarning.h create mode 100644 Stripe3DS2/Stripe3DS2/include/STDSWarning.m create mode 100644 Stripe3DS2/Stripe3DS2/include/Stripe3DS2-Prefix.pch create mode 100644 Stripe3DS2/Stripe3DS2/include/Stripe3DS2.h create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Info.plist create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/150-issuer.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/150-payment.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/300-issuer.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/300-payment.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/450-issuer.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/450-payment.png create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Resources/acs_challenge.html create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.h create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.m create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.h create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.m create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.h create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.m create mode 100644 Stripe3DS2/Stripe3DS2DemoUI/Sources/main.m create mode 100644 Stripe3DS2/Stripe3DS2DemoUITests/Info.plist create mode 100644 Stripe3DS2/Stripe3DS2DemoUITests/STDSChallengeResponseViewControllerSnapshotTests.m create mode 100644 Stripe3DS2/Stripe3DS2Resources/Info.plist create mode 100644 Stripe3DS2/Stripe3DS2Tests/Info.plist create mode 100644 Stripe3DS2/Stripe3DS2Tests/JSON/ARes.json create mode 100644 Stripe3DS2/Stripe3DS2Tests/JSON/CRes.json create mode 100644 Stripe3DS2/Stripe3DS2Tests/JSON/ErrorMessage.json create mode 100644 Stripe3DS2/Stripe3DS2Tests/NSDictionary+DecodingHelpersTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/NSString+EmptyCheckingTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSACSNetworkingManagerTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationRequestParametersTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationResponseTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSBase64URLEncodingTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSChallengeParametersTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSChallengeRequestParametersTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSChallengeResponseObjectTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSConfigParametersTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationManagerTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationParameterTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSDirectoryServerCertificateTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSEllipticCurvePointTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSEphemeralKeyPairTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSErrorMessageTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSJSONEncoderTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSJSONWebEncryptionTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSJSONWebSignatureTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSSecTypeUtilitiesTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSSynchronousLocationManagerTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.h create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSThreeDS2ServiceTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSTransactionTest.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSUICustomizationTests.m create mode 100644 Stripe3DS2/Stripe3DS2Tests/STDSWarningTests.m create mode 100644 Stripe3DS2/exported_symbols.txt create mode 100644 StripeApplePay/README.md create mode 100644 StripeApplePay/StripeApplePay.xcodeproj/project.pbxproj create mode 100644 StripeApplePay/StripeApplePay.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeApplePay/StripeApplePay.xcodeproj/xcshareddata/xcschemes/StripeApplePay.xcscheme create mode 100644 StripeApplePay/StripeApplePay/Docs.docc/StripeApplePay.md create mode 100644 StripeApplePay/StripeApplePay/Info.plist create mode 100644 StripeApplePay/StripeApplePay/Source/ApplePayContext/STPAPIClient+ApplePay.swift create mode 100644 StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext+LegacySupport.swift create mode 100644 StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext.swift create mode 100644 StripeApplePay/StripeApplePay/Source/Blocks.swift create mode 100644 StripeApplePay/StripeApplePay/Source/Extensions/BillingDetails+ApplePay.swift create mode 100644 StripeApplePay/StripeApplePay/Source/Extensions/PKContact+Stripe.swift create mode 100644 StripeApplePay/StripeApplePay/Source/Extensions/PKPayment+Stripe.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Address.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/BillingDetails.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/CardBrand.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntent.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntentParams.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethod.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethodParams.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntent.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntentParams.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/ShippingDetails.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Token.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentIntent+API.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentMethod+API.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/SetupIntent+API.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Token+API.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+Payments.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+PaymentsAPI.swift create mode 100644 StripeApplePay/StripeApplePay/Source/PaymentsCore/Categories/STPAPIClient+PaymentsCore.swift create mode 100644 StripeApplePay/StripeApplePay/Source/StripeCore+Import.swift create mode 100644 StripeApplePay/StripeApplePay/StripeApplePay.h create mode 100644 StripeApplePay/StripeApplePayTests/Info.plist create mode 100644 StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientFunctionalTest.swift create mode 100644 StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientTest.swift create mode 100644 StripeApplePay/StripeApplePayTests/STPAnalyticsClient+ApplePayTest.swift create mode 100644 StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift create mode 100644 StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift create mode 100644 StripeCameraCore/StripeCameraCore.xcodeproj/project.pbxproj create mode 100644 StripeCameraCore/StripeCameraCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeCameraCore/StripeCameraCore.xcodeproj/xcshareddata/xcschemes/StripeCameraCore.xcscheme create mode 100644 StripeCameraCore/StripeCameraCore/Info.plist create mode 100644 StripeCameraCore/StripeCameraCore/Source/CameraExifMetadata.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Categories/CGRect+StripeCameraCore.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Categories/CVPixelBuffer+StripeCameraCore.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Categories/UIDeviceOrientation+StripeCameraCore.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Categories/UIImage+Buffer.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Coordinators/AppSettingsHelper.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraPermissionsManager.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraSession.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Coordinators/MockSimulatorCameraSession.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Coordinators/Torch.swift create mode 100644 StripeCameraCore/StripeCameraCore/Source/Views/CameraPreviewView.swift create mode 100644 StripeCameraCore/StripeCameraCore/StripeCameraCore.h create mode 100644 StripeCameraCore/StripeCameraCoreTestUtils/Info.plist create mode 100644 StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockAppSettingsHelper.swift create mode 100644 StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockCameraPermissionsManager.swift create mode 100644 StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockTestCameraSession.swift create mode 100644 StripeCameraCore/StripeCameraCoreTestUtils/StripeCameraCoreTestUtils.h create mode 100644 StripeCameraCore/StripeCameraCoreTests/Info.plist create mode 100644 StripeCameraCore/StripeCameraCoreTests/Unit/Categories/CGRect_StripeCameraCoreTest.swift create mode 100644 StripeCardScan/BuildConfigurations/StripeCardScan-Debug.xcconfig create mode 100644 StripeCardScan/BuildConfigurations/StripeCardScan-Release.xcconfig create mode 100644 StripeCardScan/README.md create mode 100644 StripeCardScan/StripeCardScan.xcodeproj/project.pbxproj create mode 100644 StripeCardScan/StripeCardScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeCardScan/StripeCardScan.xcodeproj/xcshareddata/xcschemes/StripeCardScan.xcscheme create mode 100644 StripeCardScan/StripeCardScan/Docs.docc/StripeCardScan.md create mode 100644 StripeCardScan/StripeCardScan/Info.plist create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/analytics/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/metadata.json create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/model.espresso.net create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/model.espresso.shape create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/model.espresso.weights create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/model/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/neural_network_optionals/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/analytics/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/metadata.json create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/model.espresso.net create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/model.espresso.shape create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/model.espresso.weights create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/model/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/CompiledModels/UxModel.mlmodelc/neural_network_optionals/coremldata.bin create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/AppleOcr/AppleOcr.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CardUtils/CardNetwork.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CardUtils/CardType.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CardUtils/CreditCardUtils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CardUtils/Expiry.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/AppleCreditCardOcr.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/CreditCardOcrImplementation.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/CreditCardOcrPrediction.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/CreditCardOcrResult.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/ErrorCorrection.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/MachineLearningResult.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/MainLoopStateMachine.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/NonNameWords.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/OcrMainLoop.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/OcrObject.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/CreditCardOcr/SSDCreditCardOcr.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/Array+utils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/CGRectExtension.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/CGrect+utils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/CreditCardOcrPrediction+expiry.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/Image+utils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Extensions/UIImage+pixelBuffer.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLModels/AsyncModelLoading.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLModels/SSDOcr+Utils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLModels/SSDOcr.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLModels/UxModel+Utils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLModels/UxModel.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/ActiveStateComputation.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/AppState.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/DetectedAllBoxes.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/DetectedAllOcrBoxes.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/DetectedBox.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/DetectedSSDBox.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/DetectedSSDOcrBox.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/NMS.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/OcrDD.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/OcrDDUtils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/OcrPriorsGen.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/PostDetectionAlgorithm.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/PredictionAPI.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/PredictionResult.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/PredictionUtilOcr.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/SSDOcrDetect.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/SSDOcrOutputExtensions.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/MLRuntime/SoftNMS.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/BlurView.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/CardScanSheet.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/CornerView.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/InterfaceOrientation.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/PreviewView.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/ScanBaseViewController.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/ScanConfiguration.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/ScanEventsProtocol.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/ScanStats.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/SimpleScanViewController.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/Torch.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/UI/VideoFeed.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Utils/AppInfoUtils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Utils/AtomicPropertyWrapper.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardScan/Utils/DeviceUtils.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/CardImageVerificationDetailsResponse.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/Models/Scan Analytics/ScanStatsPayload+Common.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/Models/Scan Analytics/ScanStatsPayload+Tasks.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/Models/Scan Analytics/ScanStatsPayload.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/Models/VerificationFramesData.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/Models/VerifyFrames.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Api/STPAPIClient+CardImageVerification.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Bouncer.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CancellationReason.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CardImageVerificationController.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CardImageVerificationIntent.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CardImageVerificationSheet.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CardImageVerificationSheetConfiguration.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/CardScanSheetError.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScanAnalyticsManager+Helpers.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScanAnalyticsManager+Managers.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScanAnalyticsManager+Tasks.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScanAnalyticsManager.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScannedCard.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScannedCardImageData+Verification.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/ScannedCardImageData.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Card Image Verification/StripeCore+Import.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/CardBase.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/CardScanFraudData.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/CardScanMisc.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/CardVerifyFraudData.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/CardVerifyStateMachine.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/FadeInAnimation.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/FrameData.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Helpers/EndToEndTestDataSource.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Helpers/STPLocalizedString.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/Helpers/String+Localized.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/PaymentCard.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/SimpleScanViewController+Verify.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/StripeCardScanBundleLocator.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/UxAnalyzer.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/UxAndOcrMainLoop.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/VerifyCardAddViewController.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/VerifyCardViewController.swift create mode 100644 StripeCardScan/StripeCardScan/Source/CardVerify/ZoomedInCGImage.swift create mode 100644 StripeCardScan/StripeCardScan/StripeCardScan.h create mode 100644 StripeCardScan/StripeCardScanTests/Helpers/CardScanMockData.swift create mode 100644 StripeCardScan/StripeCardScanTests/Helpers/Data+Sha256.swift create mode 100644 StripeCardScan/StripeCardScanTests/Helpers/ImageHelpers.swift create mode 100644 StripeCardScan/StripeCardScanTests/Helpers/ScannedCardDetails.swift create mode 100644 StripeCardScan/StripeCardScanTests/Helpers/String+Sha256.swift create mode 100644 StripeCardScan/StripeCardScanTests/Info.plist create mode 100644 StripeCardScan/StripeCardScanTests/Mock Data/JSON/CardImageVerification_CardAdd_200.json create mode 100644 StripeCardScan/StripeCardScanTests/Mock Data/JSON/CardImageVerification_CardSet_200.json create mode 100644 StripeCardScan/StripeCardScanTests/Resources/synthetic_test_image.jpg create mode 100644 StripeCardScan/StripeCardScanTests/Unit/API Bindings/STPAPIClient+CardImageVerificationTest.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/API Bindings/ScanStatsPayloadAPIBindingsTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/API Bindings/VerifyFramesAPIBindingsTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/CardImageVerificationControllerTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/CardImageVerificationDetailsResponseTest.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/ImageCompressionTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/ML Models/UxModelTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/ScanAnalyticsManagerTests.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/StrictModeFramesTest.swift create mode 100644 StripeCardScan/StripeCardScanTests/Unit/StringResourceTests.swift create mode 100644 StripeCore/StripeCore.xcodeproj/project.pbxproj create mode 100644 StripeCore/StripeCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeCore/StripeCore.xcodeproj/xcshareddata/xcschemes/StripeCore.xcscheme create mode 100644 StripeCore/StripeCore/Info.plist create mode 100644 StripeCore/StripeCore/PrivacyInfo.xcprivacy create mode 100644 StripeCore/StripeCore/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripeCore/StripeCore/Source/API Bindings/Models/EmptyResponse.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/Models/StripeFile.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/STPAPIClient+FileUpload.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/STPAPIClient.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/STPAppInfo.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/STPMultipartFormDataEncoder.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/STPMultipartFormDataPart.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/StripeAPI.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/StripeAPIConfiguration+Version.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/StripeAPIConfiguration.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/StripeError.swift create mode 100644 StripeCore/StripeCore/Source/API Bindings/StripeServiceError.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/Analytic.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/AnalyticLoggableError.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/AnalyticLoggableErrorV2.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/AnalyticsClientV2.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/AnalyticsHelper.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/NetworkDetector.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/PluginDetector.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/STPAnalyticsClient+Error.swift create mode 100644 StripeCore/StripeCore/Source/Analytics/STPAnalyticsClient.swift create mode 100644 StripeCore/StripeCore/Source/Categories/Decimal+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Categories/Dictionary+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/Categories/Enums+CustomStringConvertible.swift create mode 100644 StripeCore/StripeCore/Source/Categories/Locale+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSArray+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSBundle+Stripe_AppName.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSCharacterSet+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSError+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSError+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSMutableURLRequest+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/Categories/NSURLComponents+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/Categories/String+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Categories/UIImage+StripeCore.swift create mode 100644 StripeCore/StripeCore/Source/Coder/StripeCodable.swift create mode 100644 StripeCore/StripeCore/Source/Coder/StripeJSONDecoder.swift create mode 100644 StripeCore/StripeCore/Source/Coder/StripeJSONEncoder.swift create mode 100644 StripeCore/StripeCore/Source/Coder/StripeJSONShared.swift create mode 100644 StripeCore/StripeCore/Source/Coder/UnknownFields.swift create mode 100644 StripeCore/StripeCore/Source/Connections Bindings/FinancialConnectionsEvent.swift create mode 100644 StripeCore/StripeCore/Source/Connections Bindings/FinancialConnectionsLinkedBank.swift create mode 100644 StripeCore/StripeCore/Source/Connections Bindings/FinancialConnectionsSDKInterface.swift create mode 100644 StripeCore/StripeCore/Source/Connections Bindings/FinancialConnectionsSDKResult.swift create mode 100644 StripeCore/StripeCore/Source/Connections Bindings/InstantDebitsLinkedBank.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/Async.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/BundleLocatorProtocol.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/FileDownloader.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/InstallMethod.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/PaymentsSDKVariant.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/Result+Extensions.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPAssert.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPDeviceUtils.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPDispatchFunctions.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPError.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPNumericStringValidator.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/STPURLCallbackHandler.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/ServerErrorMapper.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/StripeCoreBundleLocator.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/URLEncoder.swift create mode 100644 StripeCore/StripeCore/Source/Helpers/URLSession+Retry.swift create mode 100644 StripeCore/StripeCore/Source/Localization/STPLocalizationUtils.swift create mode 100644 StripeCore/StripeCore/Source/Localization/STPLocalizedString.swift create mode 100644 StripeCore/StripeCore/Source/Localization/String+Localized.swift create mode 100644 StripeCore/StripeCore/Source/Telemetry/FraudDetectionData.swift create mode 100644 StripeCore/StripeCore/Source/Telemetry/STPTelemetryClient.swift create mode 100644 StripeCore/StripeCore/Source/Telemetry/UserDefaults+PaymentsCore.swift create mode 100644 StripeCore/StripeCore/Source/UI/UIActivityIndicatorView+Stripe.swift create mode 100644 StripeCore/StripeCore/Source/UI/UIFont+Stripe.swift create mode 100644 StripeCore/StripeCore/StripeCore.h create mode 100644 StripeCore/StripeCoreTestUtils/APIStubbedTestCase.swift create mode 100644 StripeCore/StripeCoreTestUtils/Categories/STPAnalyticsClient+StripeCoreTestingUtils.swift create mode 100644 StripeCore/StripeCoreTestUtils/Categories/UIImage+StripeCoreTestingUtils.swift create mode 100644 StripeCore/StripeCoreTestUtils/Categories/UIView+StripeCoreTestingUtils.swift create mode 100644 StripeCore/StripeCoreTestUtils/Info.plist create mode 100644 StripeCore/StripeCoreTestUtils/KeyPathExpectation.swift create mode 100644 StripeCore/StripeCoreTestUtils/Mock Files/File_IdentityDocument.json create mode 100644 StripeCore/StripeCoreTestUtils/Mocks/MockAnalyticsClient.swift create mode 100644 StripeCore/StripeCoreTestUtils/Mocks/MockAnalyticsClientV2.swift create mode 100644 StripeCore/StripeCoreTestUtils/Mocks/MockData.swift create mode 100644 StripeCore/StripeCoreTestUtils/STPSnapshotTestCase.swift create mode 100644 StripeCore/StripeCoreTestUtils/StripeCoreTestUtils.h create mode 100644 StripeCore/StripeCoreTestUtils/TestConstants.swift create mode 100644 StripeCore/StripeCoreTestUtils/URLRequest+StripeTest.swift create mode 100644 StripeCore/StripeCoreTestUtils/XCTestCase+Stripe.swift create mode 100644 StripeCore/StripeCoreTests/API Bindings/STPAPIClient+EmptyResponseTest.swift create mode 100644 StripeCore/StripeCoreTests/API Bindings/STPAPIClient+ErrorResponseTest.swift create mode 100644 StripeCore/StripeCoreTests/API Bindings/StripeCodableTest.swift create mode 100644 StripeCore/StripeCoreTests/Analytics/AnalyticLoggableErrorTest.swift create mode 100644 StripeCore/StripeCoreTests/Analytics/AnalyticsClientV2Test.swift create mode 100644 StripeCore/StripeCoreTests/Analytics/Error_SerializeForLoggingTest.swift create mode 100644 StripeCore/StripeCoreTests/Analytics/STPAnalyticsClientTest.swift create mode 100644 StripeCore/StripeCoreTests/Categories/Dictionary+StripeTests.swift create mode 100644 StripeCore/StripeCoreTests/Categories/NSArray+StripeCoreTest.swift create mode 100644 StripeCore/StripeCoreTests/Categories/NSMutableURLRequest+StripeTest.swift create mode 100644 StripeCore/StripeCoreTests/Categories/UIImage+StripeCoreTests.swift create mode 100644 StripeCore/StripeCoreTests/External/TestJSONEncoder.swift create mode 100644 StripeCore/StripeCoreTests/Helpers/URLEncoderTest.swift create mode 100644 StripeCore/StripeCoreTests/Info.plist create mode 100644 StripeCore/StripeCoreTests/Mock Files/test_image.png create mode 100644 StripeFinancialConnections/README.md create mode 100644 StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj create mode 100644 StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeFinancialConnections/StripeFinancialConnections.xcodeproj/xcshareddata/xcschemes/StripeFinancialConnections.xcscheme create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Docs.docc/StripeFinancialConnections.md create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Info.plist create mode 100644 StripeFinancialConnections/StripeFinancialConnections/PrivacyInfo.xcprivacy create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/add@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/back_arrow@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/brandicon_default@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/bullet@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/cancel_circle@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/check@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/chevron_down@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/close@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/generic_error@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/info@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/link_logo@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/panel_arrow_right@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/person@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/search@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/spinner@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/stripe_logo@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/testmode@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Images/warning_triangle@3x.png create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/APIPollingHelper.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/APIVersion.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/BankAccountToken.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/ConsumerSession/ConsumerSessionModels.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsAccount.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsAuthSession.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsBulletPoint.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsConsent.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsDataAccessNotice.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsImage.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsInstitution.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsInstitutionSearchResultResource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsLegalDetailsNotice.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsLinkLoginPane.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsMixedOAuthParams.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsNetworkedAccountsResponse.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsNetworkingLinkSignup.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsOAuthPrepane.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsPartnerAccount.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsPaymentAccountResource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsPaymentMethodType.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSession.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSessionManifest.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsSynchronize.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/Models/FinancialConnectionsTheme.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsAnalyticsClient.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsSheetAnalytics.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/ExperimentHelper.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/FinancialConnectionsCustomManualEntryRequiredError.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/FinancialConnectionsNavigationController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/FlowRouter.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/LoadingView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/ModalPresentationWrapperViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Common/TestModeAutofillBannerView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSDK/FinancialConnectionsLinkedBankImplementation.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSDK/FinancialConnectionsSDKImplementation.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSDK/InstantDebitsLinkedBankImplementation.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSheet.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/FinancialConnectionsSheetError.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/FinancialConnectionsEvent+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/FinancialConnectionsFont.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/Helpers.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/Image.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/Locale+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/NSAttributedString+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/PaymentAccount+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/STPLocalizedString.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/ScreenNativeScale.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/String+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/String+Localized.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/StripeFinancialConnectionsBundleLocator.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/UIColor+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Helpers/UIViewController+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerAccountLoadErrorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerHelpers.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerNoAccountEligibleErrorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerSelectionListView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerSelectionView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/AccountPickerViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/CheckboxView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AccountPicker/RetrieveAccountsLoadingView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AttachLinkedPaymentAccount/AccountNumberRetrievalErrorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AttachLinkedPaymentAccount/AttachLinkedPaymentAccountDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/AttachLinkedPaymentAccount/AttachLinkedPaymentAccountViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentBodyView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentLogoView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Consent/ConsentViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Error/ErrorDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Error/ErrorViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionCellView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionNoResultsView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionPickerViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionSearchBar.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionTableFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionTableLoadingView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionTableView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/InstitutionPicker/InstitutionTableViewCell.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/AccountUpdateRequiredViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerBodyView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerLoadingView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerNewAccountRowView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkAccountPicker/LinkAccountPickerViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryErrorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryFormView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryValidator.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ManualEntry/ManualEntryViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NativeFlowDataManager.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkLoginWarmup/NetworkingLinkLoginWarmupBodyView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkLoginWarmup/NetworkingLinkLoginWarmupDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkLoginWarmup/NetworkingLinkLoginWarmupViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/EmailTextField.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/LinkSignupFormView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupBodyView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/PhoneCountryCodePickerView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/PhoneCountryCodeSelectorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/PhoneTextField.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkStepUpVerification/NetworkingLinkStepUpVerificationBodyView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkStepUpVerification/NetworkingLinkStepUpVerificationDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkStepUpVerification/NetworkingLinkStepUpVerificationViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingSaveToLinkVerification/NetworkingSaveToLinkVerificationDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingSaveToLinkVerification/NetworkingSaveToLinkVerificationViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PrepaneImageView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PrepaneViews.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ResetFlow/ResetFlowDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/ResetFlow/ResetFlowViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/AccountPickerRowLabelView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/AccountPickerRowView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/AttributedLabel.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/AttributedTextView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/AuthFlowHelpers.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/BulletPointLabelView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/Button+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/CloseConfirmationViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/Constants.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/DataAccessNoticeViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/FeedbackGeneratorAdapter.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/HitTestStackView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/HitTestView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/InstitutionIconView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/LegalDetailsNoticeViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/MerchantDataAccessView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/PaneLayoutView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/PaneLayoutView/PaneLayoutView+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/RoundedIconView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/RoundedTextField.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/SFSafariViewController+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/SheetViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/ShimmeringView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/SpinnerView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/TimeInterval+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/UIImage+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/UIImageView+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/UITableView+Extensions.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/UIViewController+KeyboardAvoiding.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Success/SuccessDataSource.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Success/SuccessFooterView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/Success/SuccessViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/TerminalError/TerminalErrorView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Native/TerminalError/TerminalErrorViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/StripeCore+Import.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Web/AuthenticationSessionManager.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Web/ContinueStateView.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsAccountFetcher.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsSessionFetcher.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/Source/Web/FinancialConnectionsWebFlowViewController.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnections/StripeFinancialConnections.h create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/APIPollingHelperTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/AccountFetcherTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/AccountPickerHelpersTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/AuthFlowHelpersTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/EmptyFinancialConnectionsAPIClient.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAPIClientTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAnalyticsTest.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsSessionTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsSheetTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/FlowRouterTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/Info.plist create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/ManualEntryValidatorTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/MarkdownBoldAttributedStringTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/MockData/FinancialConnectionsSession_both_accounts_la.json create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/MockData/FinancialConnectionsSession_only_accounts.json create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/MockData/FinancialConnectionsSession_only_both_missing.json create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/MockData/FinancialConnectionsSession_only_la.json create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/SessionFetcherTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/SoftLinkTests.swift create mode 100644 StripeFinancialConnections/StripeFinancialConnectionsTests/StringExtensionsTests.swift create mode 100644 StripeIdentity/README.md create mode 100644 StripeIdentity/StripeIdentity.xcodeproj/project.pbxproj create mode 100644 StripeIdentity/StripeIdentity.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeIdentity/StripeIdentity.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 StripeIdentity/StripeIdentity.xcodeproj/xcshareddata/xcschemes/StripeIdentity.xcscheme create mode 100644 StripeIdentity/StripeIdentity/Docs.docc/StripeIdentity.md create mode 100644 StripeIdentity/StripeIdentity/Info.plist create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_add@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_camera@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_camera_classic@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_checkmark@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_checkmark_92@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_clock@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_cloud@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_create_identity_verification@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_dispute_protection@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_document@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_ellipsis@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_id_front@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_info@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_lock@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_moved@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_phone@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_selfie_warmup@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_wallet@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_warning2@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_warning@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Images/icon_warning_92@3x.png create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/DocumentScanner+API.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/DocumentType+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/DocumentUploader+API.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/FaceScanner+API.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/IdentityAPIClient.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/DocumentType.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/TruncatedDecimal.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageFieldType.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageIconType.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageRequirements.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticConsentLineContent.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentBottomSheetContent.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentBottomSheetLineContent.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentConsentPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentCountryNotListedPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentDocumentCaptureModels.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentDocumentCapturePage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentDocumentSelectPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentExperiment.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentIndividualPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentIndividualWelcomePage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentPhoneOtpPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentSelfieModels.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentSelfiePage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPage/VerificationPageStaticContentTextPage.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageData/VerificationPageData.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageData/VerificationPageDataRequirementError.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageData/VerificationPageDataRequirements.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/RequiredInternationalAddress.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageClearData.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageCollectedData.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataDob.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataDocumentFileData.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataFace.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataIdNumber.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataName.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataPhone.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/Models/VerificationPageDataUpdate/VerificationPageDataUpdate.swift create mode 100644 StripeIdentity/StripeIdentity/Source/API Bindings/SelfieUploader+API.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Analytics/IdentityAnalyticsClient.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/Array+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/CGImage+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/MLMultiArray+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/NSAttributedString+HTML.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/TimeInterval+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/UINavigationController+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Categories/VNBarcodeSymbology+StripeIdentity.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Elements/IdNumberElement.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Elements/IdentityElementsFactory.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Elements/IdentityTextButtonElement.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Elements/IndividualFormElement.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Enums+CustomStringConvertible.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Helpers/Image.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Helpers/STPLocalizedString.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Helpers/String+Localized.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Helpers/StripeIdentityBundleLocator.swift create mode 100644 StripeIdentity/StripeIdentity/Source/Helpers/UIImage+utils.swift create mode 100644 StripeIdentity/StripeIdentity/Source/IdentityVerificationSheet.swift create mode 100644 StripeIdentity/StripeIdentity/Source/IdentityVerificationSheetError.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/IdentityTopLevelDestination.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/DocumentScanner/DocumentScanner.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/DocumentScanner/DocumentScannerConfiguration.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/DocumentScanner/DocumentScannerOutput.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/FaceScanner/FaceCaptureData.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/FaceScanner/FaceScanner.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/FaceScanner/FaceScannerConfiguration.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/FaceScanner/FaceScannerOutput.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/ImageScanner.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/ImageScanningConcurrencyManager.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanningSession/ImageScanningSession.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanningSession/ImageScanningSessionDelegate.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageUploaders/DocumentUploader.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageUploaders/IdentityImageUploader.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageUploaders/SelfieUploader.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/VerificationSheetController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/VerificationSheetFlowController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/VerificationSheetFlowControllerError.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/BarcodeDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/FaceDetector/FaceDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/FaceDetector/FaceDetectorOutput.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/IDDetector/IDDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/IDDetector/IDDetectorConstants.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/IDDetector/IDDetectorOutput.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/LaplacianBlurDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/MLDetectorConfiguration.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/MLDetectorMetricsTracker.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/MotionBlurDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Detectors/VisionBasedDetector.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/DocumentSide.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/IdentityDataCollecting.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/IdentityFlowNavigationController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/IdentityUI.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ML/Helpers/MLModelLoader.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ML/Helpers/MLModelUnexpectedOutputError.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ML/Helpers/NonMaxSuppression.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ML/IdentityMLModelLoader.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/BiometricConsentViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/BottomSheetViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/CountryNotListedViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DebugViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DocumentCaptureViewController+Strings.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DocumentCaptureViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DocumentFileUploadViewController+Strings.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DocumentFileUploadViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/DocumentWarmupViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/ErrorViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/IdentityFlowViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/IndividualViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/IndividualWelcomeViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/LoadingViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/PhoneOtpViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/SelfieCaptureViewController+Strings.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/SelfieCaptureViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/SelfieWarmupViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/SuccessViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/ViewControllers/UIViewController+SafariExtension.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/BottomAlignedLabel.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/BottomSheetView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/CameraPreviewContainerView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/CompleteOptionView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ContentCenteringScrollView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DebugView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DocumentCapture/AnimatedBorderView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DocumentCapture/DocumentCaptureView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DocumentCapture/DocumentScanningView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DocumentCapture/InstructionalDocumentScanningView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/DocumentWarmupView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ErrorView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/HeaderIconView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/HeaderView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/IdentityFlowView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/IdentityHTMLView/HTMLTextView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/IdentityHTMLView/HTMLViewWithIconLabels.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/IdentityHTMLView/IconLabelHTMLView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/IdentityHTMLView/MultilineIconLabelHTMLView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/InstructionListView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ListView/ListItemView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ListView/ListView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/PhoneOtpView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/Selfie/SelfieCaptureView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/Selfie/SelfieScanningView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/Selfie/SelfieWarmupView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ShadowConfiguration.swift create mode 100644 StripeIdentity/StripeIdentity/Source/NativeComponents/Views/ShadowedCorneredImageView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/StripeCore+Import.swift create mode 100644 StripeIdentity/StripeIdentity/Source/VerificationClientSecret.swift create mode 100644 StripeIdentity/StripeIdentity/Source/VerificationSheetAnalytics.swift create mode 100644 StripeIdentity/StripeIdentity/Source/WebWrapper/VerificationFlowWebView.swift create mode 100644 StripeIdentity/StripeIdentity/Source/WebWrapper/VerificationFlowWebViewController.swift create mode 100644 StripeIdentity/StripeIdentity/Source/WebWrapper/VerifyWebURLHelper.swift create mode 100644 StripeIdentity/StripeIdentity/StripeIdentity.h create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/DocumentUploaderMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/IdentityAPIClientTestMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/IdentityAnalyticsClientTestHelpers.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/IdentityMLModelLoaderMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/IdentityMockData.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/ImageScannerMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/ImageScanningConcurrencyManagerMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/MLDetectorMetricsTrackerMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/SnapshotTestMockData.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/VerificationFlowResult+Equatable.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/VerificationSheetControllerMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Helpers/VerificationSheetFlowControllerMock.swift create mode 100644 StripeIdentity/StripeIdentityTests/Info.plist create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/Mock Photos/back_drivers_license.jpg create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/Mock Photos/cgimage_stripeidentity_test.png create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/Mock Photos/front_drivers_license.jpg create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/Mock Photos/header_icon.png create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_200.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_200_no_consent_header.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_200_no_exp.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_200_submitted.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_200_testMode.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_no_selfie.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_require_live_capture.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_address.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_doc_require_address.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_doc_require_idNumber.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_doc_require_idNumber_and_address.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_idNumber.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPage/VerificationPage_type_phone.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPageData/VerificationPageData_200.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPageData/VerificationPageData_no_errors.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPageData/VerificationPageData_no_errors_needback.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPageData/VerificationPageData_submitted.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/VerificationPageData/VerificationPageData_submitted_not_closed.json create mode 100644 StripeIdentity/StripeIdentityTests/Mock Files/mock.html create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/AnimatedBorderViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/BiometricConsentViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/BottomSheetViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/CGImage_StripeIdentitySnapshotTest.png create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/CGImage_StripeIdentitySnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/DebugViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/DocumentScanningViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/DocumentWarmupViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/ErrorViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/ErrorViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/HeaderIconViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/HeaderViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/IdentityFlowViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/IdentityHTMLViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/IndividualViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/IndividualWelcomeViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/InstructionListViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/InstructionalDocumentScanningViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/ListItemViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/ListViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/NSAttributedString_HTMLSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/PhoneOtpViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/SelfieCaptureViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/SelfieScanningViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/SelfieWarmupViewSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/SuccessViewControllerSnapshotTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Snapshot/VerificationFlowWebViewSnapshotTests.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/API Bindings/IdentityAPIClientTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/API Bindings/TruncatedDecimalTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/Analytics/IdentityAnalyticsClientTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/Categories/CGImage+StripeIdentityUnitTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/Elements/IdentityElementsFactoryTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/Elements/IndividualElementTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/Coordinators/DocumentUploaderTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/Coordinators/IdentityImageUploaderTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/Coordinators/IdentityVerificationSheetTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/Coordinators/VerificationSheetControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/Coordinators/VerificationSheetFlowControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/BiometricConsentViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/DebugViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/DocumentCaptureViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/DocumentFileUploadViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/DocumentWarmupViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/ErrorViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/IdentityFlowNavigationControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/IndividualViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/IndividualWelcomeViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/PhoneOtpViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/NativeComponents/ViewControllers/SelfieWarmupViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/VerificationClientSecretTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/VerificationSheetAnalyticsTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/WebWrapper/VerificationFlowWebViewControllerTest.swift create mode 100644 StripeIdentity/StripeIdentityTests/Unit/WebWrapper/VerificationFlowWebViewTest.swift create mode 100644 StripePaymentSheet/README.md create mode 100644 StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj create mode 100644 StripePaymentSheet/StripePaymentSheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripePaymentSheet/StripePaymentSheet.xcodeproj/xcshareddata/xcschemes/StripePaymentSheet.xcscheme create mode 100644 StripePaymentSheet/StripePaymentSheet/Docs.docc/StripePaymentSheet.md create mode 100644 StripePaymentSheet/StripePaymentSheet/Info.plist create mode 100644 StripePaymentSheet/StripePaymentSheet/PrivacyInfo.xcprivacy create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/JSON/form_specs.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/tk.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_applepay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_applepay.imageset/applePayDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_applepay.imageset/carousel_applepay.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_amex.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_amex.imageset/americanExpressDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_amex.imageset/americanExpressLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_cartes_bancaires.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_cartes_bancaires.imageset/cartesBancairesDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_cartes_bancaires.imageset/cartesBancairesLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_diners.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_diners.imageset/dinersClubDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_diners.imageset/dinersClubLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_discover.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_discover.imageset/discoverDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_discover.imageset/discoverLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_jcb.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_jcb.imageset/jcbDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_jcb.imageset/jcbLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_mastercard.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_mastercard.imageset/mastercardDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_mastercard.imageset/mastercardLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unionpay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unionpay.imageset/unionPayDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unionpay.imageset/unionPayLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unknown.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unknown.imageset/unknownDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_unknown.imageset/unknownLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_visa.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_visa.imageset/visaDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_card_visa.imageset/visaLight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_sepa.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_sepa.imageset/carousel_sepa.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Carousel/carousel_sepa.imageset/sepaDark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_boa.imageset/BrandIcon--boa.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_boa.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_capitalone.imageset/BrandIcon--capitalone.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_capitalone.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_citibank.imageset/BrandIcon--citibank.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_citibank.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_compass.imageset/BrandIcon--compass.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_compass.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_default.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_default.imageset/bank_icon_default.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_default.imageset/bank_icon_default@2x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_default.imageset/bank_icon_default@3x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_morganchase.imageset/BrandIcon--morganchase.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_morganchase.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_nfcu.imageset/BrandIcon--navyfederal.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_nfcu.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_pnc.imageset/BrandIcon--pnc.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_pnc.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_stripe.imageset/BrandIcon--stripe.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_stripe.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_suntrust.imageset/BrandIcon--suntrust.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_suntrust.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_svb.imageset/BrandIcon--svb.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_svb.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_td.imageset/BrandIcon--td.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_td.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_usaa.imageset/BrandIcon--usaa.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_usaa.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_usbank.imageset/BrandIcon--usbank.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_usbank.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_wellsfargo.imageset/BrandIcon--wellsfargo.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/InstantDebitIcons/bank_icon_wellsfargo.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_icon.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_icon.imageset/Link.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo.imageset/Dark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo.imageset/Light.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo_bw.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo_bw.imageset/link_logo_bw.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo_knockout.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo_knockout.imageset/link_logo_knockout.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Link/link_logo_knockout.imageset/link_logo_knockout_dark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Mandates/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Mandates/bacsdd_logo.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/Mandates/bacsdd_logo.imageset/bacsdd_logo.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-affirm.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-affirm.imageset/icon-pm-affirm.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-afterpay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-afterpay.imageset/icon-pm-afterpay-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-alipay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-alipay.imageset/icon-pm-alipay-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-aubecsdebit.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-aubecsdebit.imageset/icon-pm-bank.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-bancontact.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-bancontact.imageset/icon-pm-bancontact-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-bank.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-bank.imageset/icon-pm-bank.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-blik.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-blik.imageset/icon-pm-blik-color.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-boleto.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-boleto.imageset/icon-pm-boleto.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-card.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-card.imageset/icon-pm-card.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-cashapp.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-cashapp.imageset/icon-pm-cash-app-color.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-eps.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-eps.imageset/icon-pm-eps-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-giropay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-giropay.imageset/icon-pm-giropay-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-ideal.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-ideal.imageset/icon-pm-ideal-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-klarna.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-klarna.imageset/icon-pm-klarna-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-konbini.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-konbini.imageset/icon-pm-konbini-color.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-oxxo.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-oxxo.imageset/icon-pm-oxxo-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-p24.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-p24.imageset/icon-pm-p24-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-paypal.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-paypal.imageset/icon-pm-paypal-color-dark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-paypal.imageset/icon-pm-paypal-color.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-revolutpay.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-revolutpay.imageset/icon-pm-revolutpay@3x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-revolutpay.imageset/icon-pm-revolutpay_dark@3x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-sepa.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-sepa.imageset/icon-pm-sepa-mod.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-swish.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-swish.imageset/icon-pm-swish@3x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-upi.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentMethods/icon-pm-upi.imageset/icon-pm-upi-color.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_checkmark.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_checkmark.imageset/icon_checkmark.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_left.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_left.imageset/icon_chevron_left.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_left_standalone.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_left_standalone.imageset/chevronLeft.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_right.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_chevron_right.imageset/chevronRight.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_edit.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_edit.imageset/icon_edit.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_lock.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_lock.imageset/icon_lock.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_plus.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_plus.imageset/icon_plus.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_x.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_x.imageset/icon_x.pdf create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_x_standalone.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/PaymentSheet/icon_x_standalone.imageset/cancel.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/affirm_mark.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/affirm_mark.imageset/affirm_mark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/affirm_mark.imageset/affirm_mark_dark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/afterpay_icon_info.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/afterpay_icon_info.imageset/info.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/afterpay_mark.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/afterpay_mark.imageset/afterpay_mark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/afterpay_mark.imageset/afterpay_mark_dark.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/apple_pay_mark.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/apple_pay_mark.imageset/applePay.svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/clearpay_mark.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/clearpay_mark.imageset/vector (15).svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/clearpay_mark.imageset/vector (16).svg create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/polling_error_icon.imageset/Contents.json create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/polling_error_icon.imageset/polling_error_icon.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/polling_error_icon.imageset/polling_error_icon@2x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Resources/StripePaymentSheet.xcassets/polling_error_icon.imageset/polling_error_icon@3x.png create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Analytics/PaymentSheetAnalyticsHelper.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Analytics/STPAnalyticsClient+Address.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Analytics/STPAnalyticsClient+CustomerSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Analytics/STPAnalyticsClient+LUXE.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/Data+SHA256.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/NSAttributedString+Stripe.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/STPPaymentMethod+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/STPPaymentMethodParams+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/String+StripePaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Categories/UIApplication+StripePaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/BoolReference.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/DownloadManager.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/Images.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/IntentStatusPoller.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/PaymentSheetLinkAccount.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/STPCameraView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/STPCardScanner.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/STPImageLibrary.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/STPLocalizedString.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/STPStringUtils.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/SavedPaymentMethodManager.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/StripePaymentSheet+Exports.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Helpers/StripePaymentSheetBundleLocator.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession+LookupResponse.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession+PublishableKey.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/ConsumerSession.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetails.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetailsShareResponse.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/VerificationSession.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/STPAPIClient+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/VO/CardExpiryDate.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/CustomerSession.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ElementsCustomer.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ExternalPaymentMethod.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPCardBrandChoice.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/STPElementsSession.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Basic UI/SeparatorLabel.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ACH/InstantDebitsOnlyFinancialConnectionsAuthManager.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Controllers/PayWithLinkWebController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/LinkEmailElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/FormElement+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/Intent+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/STPAnalyticsClient+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Extensions/UIColor+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/LinkUI.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Services/LinkAccountService.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Utils/LinkPopupURLParser.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Utils/LinkURLGenerator.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Utils/Locale+Link.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Utils/OperationDebouncer.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Verification/LinkAccountContext.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Verification/LinkCookieKey.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkLegalTermsView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Views/LinkMoreInfoView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/AddressViewController/AddressViewController+Configuration.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/AddressViewController/AddressViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/BottomSheet/BottomSheetPresentable.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/BottomSheet/BottomSheetPresentationAnimator.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/BottomSheet/BottomSheetPresentationController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/BottomSheet/BottomSheetTransitioningDelegate.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/BottomSheet/UIViewController+BottomSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerAdapter/CustomerAdapter.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerAdapter/CustomerPaymentOption.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerAdapter/UserDefaults+StripePaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSessionAdapter/CustomerSessionAdapter.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerAddPaymentMethodViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheet+API.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheet+PaymentMethodAvailability.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheet+SwiftUI.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetDataSource.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetError.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSection/CVCRecollectionElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSection/CardSectionElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSection/CardSectionWithScannerView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSection/HostedSurface.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/ConnectionsElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/LinkEnabledPaymentMethodElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/PaymentMethodElement/PaymentMethodElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/PaymentMethodElement/PaymentMethodElementWrapper.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/SimpleMandateElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+IBAN.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Error+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Intent.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkPaymentController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PayWithLinkController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PaymentSheet-LinkConfirmOption.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/New Payment Method Screen/AddPaymentMethodViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/New Payment Method Screen/PaymentMethodTypeCollectionView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/New Payment Method Screen/WalletHeaderView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentMethodType.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentOption+Images.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+PaymentMethodAvailability.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+SwiftUI.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetAppearance.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetError.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/FormSpec/FormSpec.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/FormSpec/FormSpecProvider.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+BLIK.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Boleto.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+FormSpec.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Mandates.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+OXXO.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+UPI.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactoryConfig.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetLoader.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/STPAnalyticsClient+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/STPApplePayContext+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/STPPaymentIntentShippingDetailsParams+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/SavedPaymentMethodRowButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/BankAccountInfoView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/InstantDebitsPaymentMethodElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/USBankAccount/USBankAccountPaymentMethodElement.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/FormHeaderView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RightAccessoryButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/VerticalPaymentMethodListViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/AutoComplete/AddressSearchResult.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/AutoComplete/AutoCompleteViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/AutoComplete/String+AutoComplete.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/BottomSheet3DS2ViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/BottomSheetViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/CVCReconfirmationVerticalViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/CVCReconfirmationViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/LoadingViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentMethodFormViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PollingViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PollingViewModel.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SepaMandateViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/AUBECSMandate.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/AffirmCopyLabel.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/AfterpayPriceBreakdownView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/Appearance+FontScaling.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/BacsDDMandateView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CVCPaymentMethodInformationView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CVCRecollectionView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CardScanButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CardScanningView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CircularButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ConfirmButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ManualEntryButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/PayWithLinkButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/PaymentMethodTypeImageView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/PaymentSheetUIKitAdditions.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/RotatingCardBrandsView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/ShadowedRoundedRectangleView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SheetNavigationBar.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SheetNavigationButton.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SimpleMandateTextView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/TestModeView.swift create mode 100644 StripePaymentSheet/StripePaymentSheet/StripePaymentSheet.h create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Info.plist create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/AddPaymentMethodViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/AddressViewController/AddressViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/BacsDDMandateViewSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewControllerTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheet/CustomerSheetConfigurationTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheet/CustomerSheetPaymentMethodAvailabilityTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheet/CustomerSheetTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheet/CustomerSheet_ConfirmFlowTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CustomerSheetSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/DownloadManagerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/Elements+TestHelpers.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/FlowControllerStateTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/IntentConfirmParamsTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/IntentStatusPollerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/Link/LinkPopupURLParserTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/Link/LinkURLGeneratorTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/LinkStubs.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentMethodFormViewControllerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentMethodMessagingViewFunctionalTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentMethodMessagingViewSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentMethodRowButtonSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheet+APITest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheet+DashboardConfirmParamsTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheet+DeferredAPITest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAddressTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetAnalyticsHelperTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetConfigurationTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetDeferredValidatorTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetErrorTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetExternalPaymentMethodTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFlowControllerViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFormFactorySnapshotTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFormFactoryTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetGDPRConfirmFlowTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetImageTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLinkAccountTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLoaderStubbedTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLoaderTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetPaymentMethodTypeTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetVerticalViewControllerSnapshotTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetVerticalViewControllerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PollingViewTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/RightAccessoryButtonTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPAPIClient+PaymentSheetTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPApplePayContext+PaymentSheetTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPCardBrandChoiceTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPElementsSessionTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentMethodManagerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SepaMandateViewControllerSnapshotTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/Stubbed/StubbedBackend.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/TextFieldElement+CardTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UserDefaults+StripePaymentSheetTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UserDefaults+StripePaymentSheetTest.swift.plist create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalPaymentMethodListViewControllerSnapshotTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalPaymentMethodListViewControllerTest.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerTests.swift create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/consumers_lookup_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/customers_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_customerSessionCustomerSheetWithSavedPM_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_customerSessionCustomerSheet_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_link_signup_disabled_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_paymentMethod_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_paymentMethod_link_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/elements_sessions_paymentMethod_savedPM_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/payment_intents_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/saved_payment_methods_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/saved_payment_methods_withCard_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/saved_payment_methods_withSepa_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/saved_payment_methods_withUSBank_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/Resources/MockFiles/setup_intents_200.json create mode 100644 StripePaymentSheet/StripePaymentSheetTests/STPAnalyticsClient+PaymentSheetTests.swift create mode 100644 StripePayments/README.md create mode 100644 StripePayments/StripePayments.xcodeproj/project.pbxproj create mode 100644 StripePayments/StripePayments.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripePayments/StripePayments.xcodeproj/xcshareddata/xcschemes/StripePayments.xcscheme create mode 100644 StripePayments/StripePayments/Docs.docc/StripePayments.md create mode 100644 StripePayments/StripePayments/Info.plist create mode 100644 StripePayments/StripePayments/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/tk.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripePayments/StripePayments/Source/API Bindings/Legacy Compatability/StripeAPI+Deprecated.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Legacy Compatability/StripeApplePay+Import.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Legacy Compatability/StripeCore+Import.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/ACH/LinkAccountSession.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/ACH/STPCollectBankAccountParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmAlipayOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmBLIKOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmCardOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmKonbiniOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmPaymentMethodOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmUSBankAccountOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPConfirmWeChatPayOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntent.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentAction.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentActionRedirectToURL.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentEnums.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentLastPaymentError.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentShippingDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentShippingDetailsAddress.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentShippingDetailsAddressParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentShippingDetailsParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentSourceAction.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentIntents/STPPaymentIntentSourceActionAuthorizeWithURL.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethod.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodAddress.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodAllowRedisplay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodBillingDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodEnums.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/STPPaymentMethodUpdateParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAUBECSDebit.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAUBECSDebitParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAffirm.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAffirmParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAfterpayClearpay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAfterpayClearpayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAlipay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAlipayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAlma.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAlmaParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAmazonPay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodAmazonPayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBLIK.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBLIKParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBacsDebit.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBacsDebitParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBancontact.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBancontactParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBillie.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBillieParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBoleto.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodBoletoParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCard.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardChecks.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardNetworks.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardNetworksParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardPresent.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardWallet.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardWalletMasterpass.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardWalletVisaCheckout.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCashApp.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCashAppParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodEPS.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodEPSParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodFPX.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodFPXParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodGiropay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodGiropayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodGrabPay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodGrabPayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodKlarna.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodKlarnaParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodLink.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodLinkParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodMobilePay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodMobilePayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodMultibanco.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodMultibancoParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodNetBanking.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodNetBankingParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodOXXO.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodOXXOParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodPayPal.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodPayPalParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodPrzelewy24.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodPrzelewy24Params.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodRevolutPay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodRevolutPayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSEPADebit.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSEPADebitParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSatispay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSatispayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSofort.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSofortParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSunbit.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSunbitParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSwish.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodSwishParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodThreeDSecureUsage.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodUPI.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodUPIParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodUSBankAccount.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodUSBankAccountParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodWeChatPay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodWeChatPayParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodiDEAL.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodiDEALParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPAPIResponseDecodable.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPAddress.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPCardBrand.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPConnectAccountAddress.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPConnectAccountCompanyParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPConnectAccountIndividualParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPConnectAccountParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPContactField.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPCustomer.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPFPXBankBrand.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPFile.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPFormEncodable.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPIssuingCardPin.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPRadarSession.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPToken.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/STPiDEALBank.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/SetupIntents/STPSetupIntent.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/SetupIntents/STPSetupIntentConfirmParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/SetupIntents/STPSetupIntentEnums.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/SetupIntents/STPSetupIntentLastSetupError.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/LinkSettings.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentAction.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionAlipayHandleRedirect.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionBoletoDisplayDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionCashAppRedirectToApp.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionKonbiniDisplayDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionMultibancoDisplayDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionOXXODisplayDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionPayNowDisplayQrCode.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionPromptPayDisplayQrCode.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionRedirectToURL.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionSwishHandleRedirect.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionVerifyWithMicrodeposits.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPIntentActionWeChatPayRedirectToApp.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPMandateCustomerAcceptanceParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPMandateDataParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPMandateOnlineParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Shared/STPPaymentMethodOptions.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSource.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceEnums.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceOwner.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceProtocol.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceReceiver.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceRedirect.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/STPSourceVerification.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPBankAccount.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPBankAccountParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPCard.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPCardParams.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPKlarnaLineItem.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPSourceCardDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPSourceKlarnaDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPSourceSEPADebitDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/Models/Sources/Types/STPSourceWeChatPayDetails.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/STPAPIClient+ApplePay.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/STPAPIClient+LinkAccountSession.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Payments.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Radar.swift create mode 100644 StripePayments/StripePayments/Source/API Bindings/STPRedirectContext.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/DispatchQueue+Throttle.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptcha.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaConfig.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaDebugInfo.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaDecoder.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaError.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaEvent.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaHtml.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaLog.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaResult.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaURLOpener.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaWebViewManager+WKNavigationDelegate.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/HCaptchaWebViewManager.swift create mode 100644 StripePayments/StripePayments/Source/Captcha/String+Dict.swift create mode 100644 StripePayments/StripePayments/Source/Categories/Enums+CustomStringConvertible.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPBINController.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPBankAccountCollector.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPBlocks.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPCardValidator.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPLocalizedString.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/STPPaymentConfirmation+SwiftUI.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/StripePayments+Export.swift create mode 100644 StripePayments/StripePayments/Source/Helpers/StripePaymentsBundleLocator.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/APIRequest.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STP3DS2AuthenticateResponse.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPEmptyStripeResponse.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPFormEncoder.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPIntentActionUseStripeSDK.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPInternalAPIResponseDecodable.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPPaymentMethodListDeserializer.swift create mode 100644 StripePayments/StripePayments/Source/Internal/API Bindings/STPSourcePoller.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Analytics/Analytic+Payments.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Analytics/STPAnalyticsClient+Payments.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Categories/NSArray+Stripe.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Categories/NSDecimalNumber+Stripe_Currency.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Categories/NSDictionary+Stripe.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Categories/NSString+Stripe.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Categories/STPAPIClient+PaymentsCore.swift create mode 100644 StripePayments/StripePayments/Source/Internal/Helpers/ConnectionsSDKAvailability.swift create mode 100644 StripePayments/StripePayments/Source/Internal/STPPaymentHandlerActionParams.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPAnalyticsClient+STPPaymentHandler.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPAuthenticationContext.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPPaymentHandler.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSButtonCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSCustomizationSettings.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSFooterCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSLabelCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSNavigationBarCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSSelectionCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSTextFieldCustomization.swift create mode 100644 StripePayments/StripePayments/Source/PaymentHandler/STPThreeDSUICustomization.swift create mode 100644 StripePayments/StripePayments/StripePayments.h create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Info.plist create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/3DSSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/AlipaySource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/ApplePayPaymentMethod.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/BacsDebitPaymentMethod.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/BancontactSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/BankAccount.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/Card.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/CardPaymentMethod.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/CardSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/Customer.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/EPSSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/ElementsSession.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/EphemeralKey.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/FileUpload.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/GiropaySource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/MultibancoSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/P24Source.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/PaymentIntent.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SEPADebitPaymentMethod.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SEPADebitSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SetupIntent.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SofortSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/USBankAccountPaymentMethod.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/WeChatPaySource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/iDEALSource.json create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPFixtures.h create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPFixtures.m create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPTestUtils.h create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPTestUtils.m create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPTestingAPIClient.h create mode 100644 StripePayments/StripePaymentsObjcTestUtils/STPTestingAPIClient.m create mode 100755 StripePayments/StripePaymentsObjcTestUtils/SWHttpTrafficRecorder.h create mode 100755 StripePayments/StripePaymentsObjcTestUtils/SWHttpTrafficRecorder.m create mode 100644 StripePayments/StripePaymentsObjcTestUtils/StripePaymentsObjcTestUtils.h create mode 100644 StripePayments/StripePaymentsTestHostApp/AppDelegate.swift create mode 100644 StripePayments/StripePaymentsTestHostApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 StripePayments/StripePaymentsTestHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 StripePayments/StripePaymentsTestHostApp/Assets.xcassets/Contents.json create mode 100644 StripePayments/StripePaymentsTestHostApp/Base.lproj/LaunchScreen.storyboard create mode 100644 StripePayments/StripePaymentsTestHostApp/Base.lproj/Main.storyboard create mode 100644 StripePayments/StripePaymentsTestHostApp/Info.plist create mode 100644 StripePayments/StripePaymentsTestHostApp/SceneDelegate.swift create mode 100644 StripePayments/StripePaymentsTestHostApp/ViewController.swift create mode 100644 StripePayments/StripePaymentsTestUtils/Info.plist create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/test429Backoff/0000_get_status_429.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/test429Backoff/0001_get_status_429.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/test429Backoff/0002_get_status_429.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/test429NoBackoff/0000_get_status_429.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/testDelete/0000_delete_delete.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/testGet/0000_get_get.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/testPost/0000_post_post.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/APIRequestTest/testPublishableKeyAuthorization/0000_get_bearer.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testLookupSessionexistingConsumer/0000_post_v1_consumers_sessions_lookup.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testLookupSessionnewConsumer/0000_post_v1_consumers_sessions_lookup.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testSignUpAndCreateDetailsAndLogout/0000_post_v1_consumers_accounts_sign_up.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testSignUpAndCreateDetailsAndLogout/0001_post_v1_consumers_payment_details.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testSignUpAndCreateDetailsAndLogout/0002_post_v1_consumers_sessions_log_out.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/ConsumerSessionTests/testSignUpAndCreateDetailsAndLogout/0003_post_v1_consumers_payment_details.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0002_get_v1_setup_intents_seti_1PiS2BFY0qyl6XeWRzP5gQiw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0003_post_v1_setup_intents_seti_1PiS2BFY0qyl6XeWRzP5gQiw_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0004_get_v1_setup_intents_seti_1PiS2BFY0qyl6XeWRzP5gQiw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaycustomerSession/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0002_get_v1_setup_intents_seti_1PiS2EFY0qyl6XeW9Fvr0HAM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0003_post_v1_setup_intents_seti_1PiS2EFY0qyl6XeW9Fvr0HAM_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0004_get_v1_setup_intents_seti_1PiS2EFY0qyl6XeW9Fvr0HAM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testAllowRedisplaylegacy/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmation/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmation/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmation/0002_get_v1_setup_intents_seti_1PiS2KFY0qyl6XeWaSCEpeqZ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmation/0003_post_v1_setup_intents_seti_1PiS2KFY0qyl6XeWaSCEpeqZ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmationFR/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmationFR/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmationFR/0002_get_v1_setup_intents_seti_1PiS2IKG6vc7r7YCvXuia3LK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testCardConfirmationFR/0003_post_v1_setup_intents_seti_1PiS2IKG6vc7r7YCvXuia3LK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testSepaConfirmation/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testSepaConfirmation/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testSepaConfirmation/0002_get_v1_setup_intents_seti_1PiS2NFY0qyl6XeWBZGcxTE5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testSepaConfirmation/0003_post_v1_setup_intents_seti_1PiS2NFY0qyl6XeWBZGcxTE5_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccount/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccount/0001_get_v1_setup_intents_seti_1PiS2PFY0qyl6XeWUpOO5bwC.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccount/0002_post_v1_setup_intents_seti_1PiS2PFY0qyl6XeWUpOO5bwC_link_account_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccountAttachDefaults/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccountAttachDefaults/0001_get_v1_setup_intents_seti_1PiS2OFY0qyl6XeWfFtnj0kz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/CustomerSheetConfirmFlowTests/testUSBankAccountAttachDefaults/0002_post_v1_setup_intents_seti_1PiS2OFY0qyl6XeWfFtnj0kz_link_account_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentMethodMessagingViewFunctionalTest/testCreatesViewFromServerResponse/0000_get_content.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentMethodMessagingViewFunctionalTest/testInitializingWithBadConfigurationReturnsError/0000_get_content.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0002_get_v1_payment_intents_pi_3PiS2RFY0qyl6XeW0Li38Ol8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0003_post_v1_payment_intents_pi_3PiS2RFY0qyl6XeW0Li38Ol8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0006_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0007_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0008_get_v1_setup_intents_seti_1PiS2VFY0qyl6XeWcg242nPq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0009_post_v1_setup_intents_seti_1PiS2VFY0qyl6XeWcg242nPq_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0010_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmnewinsufficientfundscard/0011_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentclientsideconfirmvalidates/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentclientsideconfirmvalidates/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentclientsideconfirmvalidates/0002_get_v1_payment_intents_pi_3PiS2YFY0qyl6XeW025NyBhO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentserversideconfirmdoesntvalidate/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentserversideconfirmdoesntvalidate/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmpaymentintentserversideconfirmdoesntvalidate/0002_get_v1_payment_intents_pi_3PiS2ZFY0qyl6XeW0HOLru0s.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0001_get_v1_payment_intents_pi_3PiS2bFY0qyl6XeW0QGoXuQ6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0002_post_v1_payment_intents_pi_3PiS2bFY0qyl6XeW0QGoXuQ6_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0003_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0004_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0005_get_v1_setup_intents_seti_1PiS2eFY0qyl6XeWjBlYwG8E.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0006_post_v1_setup_intents_seti_1PiS2eFY0qyl6XeWjBlYwG8E_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsavedinsufficientfundscard/0007_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentserversideconfirmdoesntvalidate/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentserversideconfirmdoesntvalidate/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentserversideconfirmdoesntvalidate/0002_get_v1_setup_intents_seti_1PiS2hFY0qyl6XeW59ADP6Ul.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentusagedoesntmatchintentconfig/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentusagedoesntmatchintentconfig/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmsetupintentusagedoesntmatchintentconfig/0002_get_v1_setup_intents_seti_1PiS2iFY0qyl6XeWqVyArXRB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0002_get_v1_payment_intents_pi_3PiS2jFY0qyl6XeW0whXhj0e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0003_post_v1_payment_intents_pi_3PiS2jFY0qyl6XeW0whXhj0e_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0004_get_v1_payment_intents_pi_3PiS2jFY0qyl6XeW0whXhj0e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0005_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0007_get_v1_payment_intents_pi_3PiS2mFY0qyl6XeW1b5TXiWF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxdeselected/0008_get_v1_payment_intents_pi_3PiS2mFY0qyl6XeW1b5TXiWF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0002_get_v1_payment_intents_pi_3PiS2oFY0qyl6XeW1oMcHfVh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0003_post_v1_payment_intents_pi_3PiS2oFY0qyl6XeW1oMcHfVh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0004_get_v1_payment_intents_pi_3PiS2oFY0qyl6XeW1oMcHfVh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0005_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0007_get_v1_payment_intents_pi_3PiS2rFY0qyl6XeW05Fedut8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidnewcardandsavecheckboxselected/0008_get_v1_payment_intents_pi_3PiS2rFY0qyl6XeW05Fedut8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0002_get_v1_payment_intents_pi_3PiS2tFY0qyl6XeW05MpbWg1.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0003_post_v1_payment_intents_pi_3PiS2tFY0qyl6XeW05MpbWg1_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0004_get_v1_payment_intents_pi_3PiS2tFY0qyl6XeW05MpbWg1.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0005_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0007_get_v1_payment_intents_pi_3PiS2vFY0qyl6XeW0HK2Qn1f.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0008_get_v1_payment_intents_pi_3PiS2vFY0qyl6XeW0HK2Qn1f.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0010_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0011_get_v1_setup_intents_seti_1PiS2xFY0qyl6XeW0RJBh9ro.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0012_post_v1_setup_intents_seti_1PiS2xFY0qyl6XeW0RJBh9ro_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0013_get_v1_setup_intents_seti_1PiS2xFY0qyl6XeW0RJBh9ro.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0014_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0015_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0016_get_v1_setup_intents_seti_1PiS30FY0qyl6XeW4HMbAKPF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testDeferredConfirmvalidsavedcard/0017_get_v1_setup_intents_seti_1PiS30FY0qyl6XeW4HMbAKPF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntent/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntent/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntent/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntent/0003_get_v1_payment_intents_pi_3PiS32FY0qyl6XeW1HcyyvIm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntent/0004_post_v1_payment_intents_pi_3PiS32FY0qyl6XeW1HcyyvIm_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntentserverSideConfirmation/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntentserverSideConfirmation/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntentserverSideConfirmation/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithDeferredIntentserverSideConfirmation/0003_get_v1_payment_intents_pi_3PiS35FY0qyl6XeW1YNPLCbq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntent/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntent/0002_post_v1_payment_intents_pi_3PiS36FY0qyl6XeW0l85ktJx_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntent/0003_get_v1_payment_intents_pi_3PiS36FY0qyl6XeW0l85ktJx.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntentAttachedPaymentMethod/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntentAttachedPaymentMethod/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntentAttachedPaymentMethod/0002_post_v1_payment_intents_pi_3PiS38FY0qyl6XeW0OAa7B47_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testPaymentSheetLoadAndConfirmWithPaymentIntentAttachedPaymentMethod/0003_get_v1_payment_intents_pi_3PiS38FY0qyl6XeW0OAa7B47.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0001_get_v1_payment_intents_pi_3PiS3FFY0qyl6XeW0zpYxa1Z.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0002_post_v1_payment_intents_pi_3PiS3FFY0qyl6XeW0zpYxa1Z_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0005_get_v1_payment_intents_pi_3PiS3IFY0qyl6XeW1R8mJEdf.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0006_post_v1_payment_intents_pi_3PiS3IFY0qyl6XeW1R8mJEdf_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0007_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0008_get_v1_payment_intents_pi_3PiS3KFY0qyl6XeW0m1XmnUK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0009_post_v1_payment_intents_pi_3PiS3KFY0qyl6XeW0m1XmnUK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0010_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0011_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0012_get_v1_payment_intents_pi_3PiS3MFY0qyl6XeW011ApMz7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPI/0013_post_v1_payment_intents_pi_3PiS3MFY0qyl6XeW011ApMz7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0001_get_v1_payment_intents_pi_3PiS3BFY0qyl6XeW06c19ynF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0002_post_v1_payment_intents_pi_3PiS3BFY0qyl6XeW06c19ynF_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0005_get_v1_payment_intents_pi_3PiS3DFY0qyl6XeW0CVkYoHP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultPaymentIntentSFU/0006_post_v1_payment_intents_pi_3PiS3DFY0qyl6XeW0CVkYoHP_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0001_get_v1_setup_intents_seti_1PiS3OFY0qyl6XeWoJKyoMjz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0002_post_v1_setup_intents_seti_1PiS3OFY0qyl6XeWoJKyoMjz_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0004_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0005_get_v1_setup_intents_seti_1PiS3QFY0qyl6XeWA06BiwA7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testSetsNewlySavedPMAsDefaultSetupIntent/0006_post_v1_setup_intents_seti_1PiS3QFY0qyl6XeWA06BiwA7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdate/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdate/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdate/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateFails/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateFails/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateFails/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateIgnoresInFlightUpdate/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateIgnoresInFlightUpdate/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetAPITest/testUpdateIgnoresInFlightUpdate/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0003_get_v1_payment_intents_pi_3PjWLiFY0qyl6XeW1WQQJxTb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0004_post_v1_payment_intents_pi_3PjWLiFY0qyl6XeW1WQQJxTb_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0005_get_v1_payment_intents_pi_3PjWLiFY0qyl6XeW1WQQJxTb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0006_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0007_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0008_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0009_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0010_get_v1_payment_intents_pi_3PjWLlFY0qyl6XeW0LAeRYcp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0011_post_v1_payment_intents_pi_3PjWLlFY0qyl6XeW0LAeRYcp_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0012_get_v1_payment_intents_pi_3PjWLlFY0qyl6XeW0LAeRYcp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0013_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0014_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0015_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0016_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0017_get_v1_payment_intents_pi_3PjWLoFY0qyl6XeW0iKpD6M5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0018_post_v1_payment_intents_pi_3PjWLoFY0qyl6XeW0iKpD6M5_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0019_get_v1_payment_intents_pi_3PjWLoFY0qyl6XeW0iKpD6M5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIDeferredCSC/0020_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0002_get_v1_payment_intents_pi_3PjWLHFY0qyl6XeW0b92TYtH.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0003_post_v1_payment_intents_pi_3PjWLHFY0qyl6XeW0b92TYtH_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0004_get_v1_payment_intents_pi_3PjWLHFY0qyl6XeW0b92TYtH.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0007_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0008_get_v1_payment_intents_pi_3PjWLKFY0qyl6XeW1gjmiaLT.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0009_post_v1_payment_intents_pi_3PjWLKFY0qyl6XeW1gjmiaLT_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0010_get_v1_payment_intents_pi_3PjWLKFY0qyl6XeW1gjmiaLT.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0014_get_v1_payment_intents_pi_3PjWLNFY0qyl6XeW0WKDQbyd.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0015_post_v1_payment_intents_pi_3PjWLNFY0qyl6XeW0WKDQbyd_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0016_get_v1_payment_intents_pi_3PjWLNFY0qyl6XeW0WKDQbyd.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIIntentFirst/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0003_get_v1_payment_intents_pi_3PiS5WFY0qyl6XeW0TEoZrzq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0004_post_v1_payment_intents_pi_3PiS5WFY0qyl6XeW0TEoZrzq_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0005_get_v1_payment_intents_pi_3PiS5WFY0qyl6XeW0TEoZrzq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0006_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0007_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0008_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0009_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0010_get_v1_payment_intents_pi_3PiS5ZFY0qyl6XeW0IRPongV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0011_post_v1_payment_intents_pi_3PiS5ZFY0qyl6XeW0IRPongV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0012_get_v1_payment_intents_pi_3PiS5ZFY0qyl6XeW0IRPongV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0013_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0014_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0015_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0016_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0017_get_v1_payment_intents_pi_3PiS5dFY0qyl6XeW13nb2Yi8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0018_post_v1_payment_intents_pi_3PiS5dFY0qyl6XeW13nb2Yi8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0019_get_v1_payment_intents_pi_3PiS5dFY0qyl6XeW13nb2Yi8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentcsc/0020_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0003_get_v1_payment_intents_pi_3PiS5gFY0qyl6XeW1RMB0Ifn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0004_get_v1_payment_intents_pi_3PiS5gFY0qyl6XeW1RMB0Ifn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0009_get_v1_payment_intents_pi_3PiS5jFY0qyl6XeW1anpvcKh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0010_get_v1_payment_intents_pi_3PiS5jFY0qyl6XeW1anpvcKh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0015_get_v1_payment_intents_pi_3PiS5mFY0qyl6XeW05mcfsYr.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0016_get_v1_payment_intents_pi_3PiS5mFY0qyl6XeW05mcfsYr.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUdeferredIntentssc/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0002_get_v1_payment_intents_pi_3PiS5pFY0qyl6XeW0JjoWPFh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0003_post_v1_payment_intents_pi_3PiS5pFY0qyl6XeW0JjoWPFh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0004_get_v1_payment_intents_pi_3PiS5pFY0qyl6XeW0JjoWPFh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0007_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0008_get_v1_payment_intents_pi_3PiS5tFY0qyl6XeW1GiWT7BV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0009_post_v1_payment_intents_pi_3PiS5tFY0qyl6XeW1GiWT7BV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0010_get_v1_payment_intents_pi_3PiS5tFY0qyl6XeW1GiWT7BV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0014_get_v1_payment_intents_pi_3PiS5wFY0qyl6XeW1iavV4Xv.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0015_post_v1_payment_intents_pi_3PiS5wFY0qyl6XeW1iavV4Xv_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0016_get_v1_payment_intents_pi_3PiS5wFY0qyl6XeW1iavV4Xv.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPISFUintentFirstcsc/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0003_get_v1_payment_intents_pi_3PjWM4FY0qyl6XeW1p4g32Lz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0004_get_v1_payment_intents_pi_3PjWM4FY0qyl6XeW1p4g32Lz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0009_get_v1_payment_intents_pi_3PjWM7FY0qyl6XeW0bfoI51q.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0010_get_v1_payment_intents_pi_3PjWM7FY0qyl6XeW0bfoI51q.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0015_get_v1_payment_intents_pi_3PjWMAFY0qyl6XeW1hqdkyKh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0016_get_v1_payment_intents_pi_3PjWMAFY0qyl6XeW1hqdkyKh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayPIdeferredIntentssc/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0002_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0003_get_v1_setup_intents_seti_1PiS5zFY0qyl6XeW7VSdzoRF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0004_post_v1_setup_intents_seti_1PiS5zFY0qyl6XeW7VSdzoRF_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0005_get_v1_setup_intents_seti_1PiS5zFY0qyl6XeW7VSdzoRF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0006_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0007_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0008_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0009_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0010_get_v1_setup_intents_seti_1PiS63FY0qyl6XeW1r1MusJ5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0011_post_v1_setup_intents_seti_1PiS63FY0qyl6XeW1r1MusJ5_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0012_get_v1_setup_intents_seti_1PiS63FY0qyl6XeW1r1MusJ5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0013_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0014_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0015_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0016_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0017_get_v1_setup_intents_seti_1PiS66FY0qyl6XeW9NpSVdmL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0018_post_v1_setup_intents_seti_1PiS66FY0qyl6XeW9NpSVdmL_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0019_get_v1_setup_intents_seti_1PiS66FY0qyl6XeW9NpSVdmL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentcsc/0020_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0002_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0003_get_v1_setup_intents_seti_1PiS69FY0qyl6XeWf9hJqA5J.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0004_get_v1_setup_intents_seti_1PiS69FY0qyl6XeWf9hJqA5J.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0008_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0009_get_v1_setup_intents_seti_1PiS6CFY0qyl6XeWwl1fntB3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0010_get_v1_setup_intents_seti_1PiS6CFY0qyl6XeWwl1fntB3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0014_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0015_get_v1_setup_intents_seti_1PiS6FFY0qyl6XeWeiWitQvy.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0016_get_v1_setup_intents_seti_1PiS6FFY0qyl6XeWeiWitQvy.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIdeferredIntentssc/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0002_get_v1_setup_intents_seti_1PiS6IFY0qyl6XeWWT82QhxK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0003_post_v1_setup_intents_seti_1PiS6IFY0qyl6XeWWT82QhxK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0004_get_v1_setup_intents_seti_1PiS6IFY0qyl6XeWWT82QhxK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0007_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0008_get_v1_setup_intents_seti_1PiS6LFY0qyl6XeWfwqQkcpe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0009_post_v1_setup_intents_seti_1PiS6LFY0qyl6XeWfwqQkcpe_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0010_get_v1_setup_intents_seti_1PiS6LFY0qyl6XeWfwqQkcpe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0012_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0013_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0014_get_v1_setup_intents_seti_1PiS6OFY0qyl6XeWeafPdyl1.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0015_post_v1_setup_intents_seti_1PiS6OFY0qyl6XeWeafPdyl1_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0016_get_v1_setup_intents_seti_1PiS6OFY0qyl6XeWeafPdyl1.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaySIintentFirstcsc/0017_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0002_get_v1_payment_intents_pi_3Pj42RFY0qyl6XeW1G4psghK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0003_post_v1_payment_intents_pi_3Pj42RFY0qyl6XeW1G4psghK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0004_get_v1_payment_intents_pi_3Pj42RFY0qyl6XeW1G4psghK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0009_get_v1_payment_intents_pi_3Pj42VFY0qyl6XeW1S6Bvk4O.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0010_post_v1_payment_intents_pi_3Pj42VFY0qyl6XeW1S6Bvk4O_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0011_get_v1_payment_intents_pi_3Pj42VFY0qyl6XeW1S6Bvk4O.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0012_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0013_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0014_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0015_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0016_get_v1_payment_intents_pi_3Pj42YFY0qyl6XeW0TuZwW2K.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0017_post_v1_payment_intents_pi_3Pj42YFY0qyl6XeW0TuZwW2K_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0018_get_v1_payment_intents_pi_3Pj42YFY0qyl6XeW0TuZwW2K.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0019_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0020_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0021_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0022_get_v1_setup_intents_seti_1Pj42bFY0qyl6XeWW7izP2aM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0023_post_v1_setup_intents_seti_1Pj42bFY0qyl6XeWW7izP2aM_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0024_get_v1_setup_intents_seti_1Pj42bFY0qyl6XeWW7izP2aM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0025_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0026_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0028_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0029_get_v1_setup_intents_seti_1Pj42fFY0qyl6XeWpJZpd8qv.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0030_post_v1_setup_intents_seti_1Pj42fFY0qyl6XeWpJZpd8qv_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0031_get_v1_setup_intents_seti_1Pj42fFY0qyl6XeWpJZpd8qv.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0032_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0033_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0034_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0035_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0036_get_v1_setup_intents_seti_1Pj42iFY0qyl6XeWO5zkclKJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0037_get_v1_setup_intents_seti_1Pj42iFY0qyl6XeWO5zkclKJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplayallowOverride/0038_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0002_get_v1_payment_intents_pi_3PiS4YFY0qyl6XeW00bjI1bV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0003_post_v1_payment_intents_pi_3PiS4YFY0qyl6XeW00bjI1bV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0004_get_v1_payment_intents_pi_3PiS4YFY0qyl6XeW00bjI1bV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0009_get_v1_payment_intents_pi_3PiS4cFY0qyl6XeW1SkdNnUV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0010_post_v1_payment_intents_pi_3PiS4cFY0qyl6XeW1SkdNnUV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0011_get_v1_payment_intents_pi_3PiS4cFY0qyl6XeW1SkdNnUV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0012_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0013_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0014_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0015_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0016_get_v1_payment_intents_pi_3PiS4fFY0qyl6XeW1bFCizTK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0017_get_v1_payment_intents_pi_3PiS4fFY0qyl6XeW1bFCizTK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSFUlegacyEphemeralKey/0018_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0002_get_v1_payment_intents_pi_3PiS4iFY0qyl6XeW1SY1RjLl.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0003_post_v1_payment_intents_pi_3PiS4iFY0qyl6XeW1SY1RjLl_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0004_get_v1_payment_intents_pi_3PiS4iFY0qyl6XeW1SY1RjLl.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0009_get_v1_payment_intents_pi_3PiS4mFY0qyl6XeW18rpc2LS.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0010_post_v1_payment_intents_pi_3PiS4mFY0qyl6XeW18rpc2LS_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0011_get_v1_payment_intents_pi_3PiS4mFY0qyl6XeW18rpc2LS.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0012_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0013_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0014_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0015_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0016_get_v1_payment_intents_pi_3PiS4pFY0qyl6XeW0QFV0awd.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0017_get_v1_payment_intents_pi_3PiS4pFY0qyl6XeW0QFV0awd.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentSIlegacyEphemeralKey/0018_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0002_get_v1_payment_intents_pi_3PiS4EFY0qyl6XeW0yOJYEqG.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0003_post_v1_payment_intents_pi_3PiS4EFY0qyl6XeW0yOJYEqG_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0004_get_v1_payment_intents_pi_3PiS4EFY0qyl6XeW0yOJYEqG.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0006_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0009_get_v1_payment_intents_pi_3PiS4HFY0qyl6XeW0xhKArcl.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0010_post_v1_payment_intents_pi_3PiS4HFY0qyl6XeW0xhKArcl_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0011_get_v1_payment_intents_pi_3PiS4HFY0qyl6XeW0xhKArcl.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0012_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0013_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0014_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0015_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0016_get_v1_payment_intents_pi_3PiS4LFY0qyl6XeW0IUAqohP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0017_get_v1_payment_intents_pi_3PiS4LFY0qyl6XeW0IUAqohP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0018_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0019_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0020_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0021_get_v1_payment_intents_pi_3PiS4OFY0qyl6XeW1QmNfV3P.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0022_post_v1_payment_intents_pi_3PiS4OFY0qyl6XeW1QmNfV3P_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0023_get_v1_payment_intents_pi_3PiS4OFY0qyl6XeW1QmNfV3P.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0024_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0025_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0026_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0027_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0028_get_v1_payment_intents_pi_3PiS4SFY0qyl6XeW1pnsNTSc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0029_post_v1_payment_intents_pi_3PiS4SFY0qyl6XeW1pnsNTSc_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0030_get_v1_payment_intents_pi_3PiS4SFY0qyl6XeW1pnsNTSc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0031_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0032_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0033_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0034_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0035_get_v1_payment_intents_pi_3PiS4VFY0qyl6XeW1khrwbQ6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0036_get_v1_payment_intents_pi_3PiS4VFY0qyl6XeW1khrwbQ6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetGDPRConfirmFlowTests/testAllowRedisplaypaymentIntentlegacyEphemeralKey/0037_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0001_get_v1_payment_intents_pi_3PiTFJF7QokQdxBy1oLWJSGM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0002_post_v1_payment_intents_pi_3PiTFJF7QokQdxBy1oLWJSGM_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0005_get_v1_payment_intents_pi_3PiTFLF7QokQdxBy2skk4jd2.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0006_post_v1_payment_intents_pi_3PiTFLF7QokQdxBy2skk4jd2_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0009_get_v1_payment_intents_pi_3PiTFNF7QokQdxBy0o0gue0K.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0011_get_v1_payment_intents_pi_3PiTFOF7QokQdxBy2DeLSlCw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0012_post_v1_payment_intents_pi_3PiTFOF7QokQdxBy2DeLSlCw_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0015_get_v1_payment_intents_pi_3PiTFQF7QokQdxBy1mcIi24C.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0016_post_v1_payment_intents_pi_3PiTFQF7QokQdxBy1mcIi24C_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0019_get_v1_payment_intents_pi_3PiTFSF7QokQdxBy2aCZCbRj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0020_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0021_get_v1_setup_intents_seti_1PiTFTF7QokQdxBy85OjWngM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0022_post_v1_setup_intents_seti_1PiTFTF7QokQdxBy85OjWngM_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0023_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0024_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0025_get_v1_setup_intents_seti_1PiTFVF7QokQdxByjRz9RlYV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0026_post_v1_setup_intents_seti_1PiTFVF7QokQdxByjRz9RlYV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0028_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAUBecsDebitConfirmFlows/0029_get_v1_setup_intents_seti_1PiTFWF7QokQdxByLZXVizQJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTEEFY0qyl6XeW0s1Y1qGj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTEEFY0qyl6XeW0s1Y1qGj_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0003_get_v1_payment_intents_pi_3PiTEEFY0qyl6XeW0s1Y1qGj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0006_get_v1_payment_intents_pi_3PiTEGFY0qyl6XeW042Gfi2g.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0007_post_v1_payment_intents_pi_3PiTEGFY0qyl6XeW042Gfi2g_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0008_get_v1_payment_intents_pi_3PiTEGFY0qyl6XeW042Gfi2g.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0011_get_v1_payment_intents_pi_3PiTEJFY0qyl6XeW1GTJWEDq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlipayConfirmFlows/0012_get_v1_payment_intents_pi_3PiTEJFY0qyl6XeW1GTJWEDq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0001_get_v1_payment_intents_pi_3PiTEKKG6vc7r7YC1tBz8NmF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0002_post_v1_payment_intents_pi_3PiTEKKG6vc7r7YC1tBz8NmF_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0003_get_v1_payment_intents_pi_3PiTEKKG6vc7r7YC1tBz8NmF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0006_get_v1_payment_intents_pi_3PiTEMKG6vc7r7YC0w1UTQQ7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0007_post_v1_payment_intents_pi_3PiTEMKG6vc7r7YC0w1UTQQ7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0008_get_v1_payment_intents_pi_3PiTEMKG6vc7r7YC0w1UTQQ7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0011_get_v1_payment_intents_pi_3PiTEOKG6vc7r7YC1MHc68qb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAlmaConfirmFlows/0012_get_v1_payment_intents_pi_3PiTEOKG6vc7r7YC1MHc68qb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0003_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0004_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0005_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0006_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0007_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0008_get_v1_payment_intents_pi_3PiTEPFY0qyl6XeW1MDTgfBY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0011_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0012_post_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0013_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0014_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0015_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0016_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0017_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0018_get_v1_payment_intents_pi_3PiTEiFY0qyl6XeW086jB16T.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0019_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0020_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0021_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0022_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0023_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0024_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0025_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0026_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testAmazonPayConfirmFlows/0027_get_v1_payment_intents_pi_3PiTF1FY0qyl6XeW0inpEO26.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0001_get_v1_payment_intents_pi_3PiTFwArGMi59tL41WJNey9A.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0002_post_v1_payment_intents_pi_3PiTFwArGMi59tL41WJNey9A_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0005_get_v1_payment_intents_pi_3PiTFyArGMi59tL40XEPxieD.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBLIKConfirmFlows/0006_post_v1_payment_intents_pi_3PiTFyArGMi59tL40XEPxieD_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0001_get_v1_payment_intents_pi_3PiTFXGoesj9fw9Q1Xou3zeP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0002_post_v1_payment_intents_pi_3PiTFXGoesj9fw9Q1Xou3zeP_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0005_get_v1_payment_intents_pi_3PiTFZGoesj9fw9Q1ZrT7KZV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0006_post_v1_payment_intents_pi_3PiTFZGoesj9fw9Q1ZrT7KZV_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0009_get_v1_payment_intents_pi_3PiTFbGoesj9fw9Q1Ex7WyLP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0011_get_v1_payment_intents_pi_3PiTFcGoesj9fw9Q0toFtRG6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0012_post_v1_payment_intents_pi_3PiTFcGoesj9fw9Q0toFtRG6_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0015_get_v1_payment_intents_pi_3PiTFeGoesj9fw9Q1UcMUUqn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0016_post_v1_payment_intents_pi_3PiTFeGoesj9fw9Q1UcMUUqn_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBacsDDConfirmFlows/0019_get_v1_payment_intents_pi_3PiTFgGoesj9fw9Q1Adpxm6L.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0001_get_v1_payment_intents_pi_3PiTFhFY0qyl6XeW0jmVo7PO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0002_post_v1_payment_intents_pi_3PiTFhFY0qyl6XeW0jmVo7PO_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0003_get_v1_payment_intents_pi_3PiTFhFY0qyl6XeW0jmVo7PO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0006_get_v1_payment_intents_pi_3PiTFjFY0qyl6XeW1fcdU0OJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0007_post_v1_payment_intents_pi_3PiTFjFY0qyl6XeW1fcdU0OJ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0008_get_v1_payment_intents_pi_3PiTFjFY0qyl6XeW1fcdU0OJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0011_get_v1_payment_intents_pi_3PiTFlFY0qyl6XeW02rwAJgQ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0012_get_v1_payment_intents_pi_3PiTFlFY0qyl6XeW02rwAJgQ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0014_get_v1_payment_intents_pi_3PiTFmFY0qyl6XeW0ktOZq7I.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0015_post_v1_payment_intents_pi_3PiTFmFY0qyl6XeW0ktOZq7I_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0016_get_v1_payment_intents_pi_3PiTFmFY0qyl6XeW0ktOZq7I.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0019_get_v1_payment_intents_pi_3PiTFoFY0qyl6XeW0IzVfqHs.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0020_post_v1_payment_intents_pi_3PiTFoFY0qyl6XeW0IzVfqHs_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0021_get_v1_payment_intents_pi_3PiTFoFY0qyl6XeW0IzVfqHs.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0022_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0023_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0024_get_v1_payment_intents_pi_3PiTFqFY0qyl6XeW0n428V7D.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0025_get_v1_payment_intents_pi_3PiTFqFY0qyl6XeW0n428V7D.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0026_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0027_get_v1_setup_intents_seti_1PiTFsFY0qyl6XeW7jaV90Md.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0028_post_v1_setup_intents_seti_1PiTFsFY0qyl6XeW7jaV90Md_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0029_get_v1_setup_intents_seti_1PiTFsFY0qyl6XeW7jaV90Md.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0030_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0031_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0032_get_v1_setup_intents_seti_1PiTFtFY0qyl6XeWBaOXa6Hu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0033_post_v1_setup_intents_seti_1PiTFtFY0qyl6XeWBaOXa6Hu_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0034_get_v1_setup_intents_seti_1PiTFtFY0qyl6XeWBaOXa6Hu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0035_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0036_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0037_get_v1_setup_intents_seti_1PiTFvFY0qyl6XeWtjYpzRrr.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBancontactConfirmFlows/0038_get_v1_setup_intents_seti_1PiTFvFY0qyl6XeWtjYpzRrr.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0001_get_v1_payment_intents_pi_3Pgvu8Goesj9fw9Q0IOK5vPe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0002_post_v1_payment_intents_pi_3Pgvu8Goesj9fw9Q0IOK5vPe_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0003_get_v1_payment_intents_pi_3Pgvu8Goesj9fw9Q0IOK5vPe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0006_get_v1_payment_intents_pi_3PgvuAGoesj9fw9Q0HWarpDa.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0007_post_v1_payment_intents_pi_3PgvuAGoesj9fw9Q0HWarpDa_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0008_get_v1_payment_intents_pi_3PgvuAGoesj9fw9Q0HWarpDa.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0011_get_v1_payment_intents_pi_3PgvuDGoesj9fw9Q1rBUIspY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBillieConfirmFlows/0012_get_v1_payment_intents_pi_3PgvuDGoesj9fw9Q1rBUIspY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0001_get_v1_payment_intents_pi_3PiTFzJQVROkWvqT2AHpcsRS.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0002_post_v1_payment_intents_pi_3PiTFzJQVROkWvqT2AHpcsRS_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0003_get_v1_payment_intents_pi_3PiTFzJQVROkWvqT2AHpcsRS.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0006_get_v1_payment_intents_pi_3PiTG2JQVROkWvqT2PNPBtsB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0007_post_v1_payment_intents_pi_3PiTG2JQVROkWvqT2PNPBtsB_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0008_get_v1_payment_intents_pi_3PiTG2JQVROkWvqT2PNPBtsB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0011_get_v1_payment_intents_pi_3PiTG4JQVROkWvqT2DcJBbmi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0012_get_v1_payment_intents_pi_3PiTG4JQVROkWvqT2DcJBbmi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0014_get_v1_payment_intents_pi_3PiTG5JQVROkWvqT1aduWbdJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0015_post_v1_payment_intents_pi_3PiTG5JQVROkWvqT1aduWbdJ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0016_get_v1_payment_intents_pi_3PiTG5JQVROkWvqT1aduWbdJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0019_get_v1_payment_intents_pi_3PiTG8JQVROkWvqT1ZjX5xPZ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0020_post_v1_payment_intents_pi_3PiTG8JQVROkWvqT1ZjX5xPZ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0021_get_v1_payment_intents_pi_3PiTG8JQVROkWvqT1ZjX5xPZ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0022_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0023_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0024_get_v1_payment_intents_pi_3PiTGAJQVROkWvqT2tTBSBLu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0025_get_v1_payment_intents_pi_3PiTGAJQVROkWvqT2tTBSBLu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0026_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0027_get_v1_setup_intents_seti_1PiTGBJQVROkWvqTTzTGuL9A.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0028_post_v1_setup_intents_seti_1PiTGBJQVROkWvqTTzTGuL9A_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0029_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0030_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0031_get_v1_setup_intents_seti_1PiTGDJQVROkWvqTentcyB7A.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0032_post_v1_setup_intents_seti_1PiTGDJQVROkWvqTentcyB7A_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0033_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0034_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testBoletoConfirmFlows/0035_get_v1_setup_intents_seti_1PiTGEJQVROkWvqT2JMW6jDA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0001_get_v1_payment_intents_pi_3PiTGFFY0qyl6XeW0ftVWFhB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0002_post_v1_payment_intents_pi_3PiTGFFY0qyl6XeW0ftVWFhB_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0005_get_v1_payment_intents_pi_3PiTGIFY0qyl6XeW1M7cc3hi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0006_post_v1_payment_intents_pi_3PiTGIFY0qyl6XeW1M7cc3hi_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0009_get_v1_payment_intents_pi_3PiTGKFY0qyl6XeW1IYJLkNz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0011_get_v1_payment_intents_pi_3PiTGMFY0qyl6XeW0BsWTvpQ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0012_post_v1_payment_intents_pi_3PiTGMFY0qyl6XeW0BsWTvpQ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0015_get_v1_payment_intents_pi_3PiTGOFY0qyl6XeW1dFZRTpk.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0016_post_v1_payment_intents_pi_3PiTGOFY0qyl6XeW1dFZRTpk_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0019_get_v1_payment_intents_pi_3PiTGRFY0qyl6XeW0qDrruu9.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0020_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0021_get_v1_setup_intents_seti_1PiTGSFY0qyl6XeWMTPiPbvt.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0022_post_v1_setup_intents_seti_1PiTGSFY0qyl6XeWMTPiPbvt_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0023_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0024_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0025_get_v1_setup_intents_seti_1PiTGUFY0qyl6XeWTyqCLhFp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0026_post_v1_setup_intents_seti_1PiTGUFY0qyl6XeWTyqCLhFp_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0028_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardAllFieldsWithDefaults/0029_get_v1_setup_intents_seti_1PiTGWFY0qyl6XeWx3jWxDK0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0001_get_v1_payment_intents_pi_3PiTGYFY0qyl6XeW1w5uq2LD.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0002_post_v1_payment_intents_pi_3PiTGYFY0qyl6XeW1w5uq2LD_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0005_get_v1_payment_intents_pi_3PiTGaFY0qyl6XeW1UjPiJbG.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0006_post_v1_payment_intents_pi_3PiTGaFY0qyl6XeW1UjPiJbG_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0009_get_v1_payment_intents_pi_3PiTGdFY0qyl6XeW1SqUIwh0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0011_get_v1_payment_intents_pi_3PiTGeFY0qyl6XeW14vbbzHp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0012_post_v1_payment_intents_pi_3PiTGeFY0qyl6XeW14vbbzHp_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0015_get_v1_payment_intents_pi_3PiTGhFY0qyl6XeW1ANdoojK.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0016_post_v1_payment_intents_pi_3PiTGhFY0qyl6XeW1ANdoojK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0019_get_v1_payment_intents_pi_3PiTGkFY0qyl6XeW0nLUKYR4.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0020_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0021_get_v1_setup_intents_seti_1PiTGlFY0qyl6XeWDBSwU0Ji.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0022_post_v1_setup_intents_seti_1PiTGlFY0qyl6XeWDBSwU0Ji_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0023_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0024_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0025_get_v1_setup_intents_seti_1PiTGoFY0qyl6XeWtNO3Gq7J.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0026_post_v1_setup_intents_seti_1PiTGoFY0qyl6XeWtNO3Gq7J_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0028_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCardOnlyCardInfoWithDefaults/0029_get_v1_setup_intents_seti_1PiTGqFY0qyl6XeWqSYQnSMj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0001_get_v1_payment_intents_pi_3PiTGsFY0qyl6XeW1cydassq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0002_post_v1_payment_intents_pi_3PiTGsFY0qyl6XeW1cydassq_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0003_post_v1_payment_intents_pi_3PiTGsFY0qyl6XeW1cydassq_refresh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0006_get_v1_payment_intents_pi_3PiTGvFY0qyl6XeW07Oz79VA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0007_post_v1_payment_intents_pi_3PiTGvFY0qyl6XeW07Oz79VA_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0008_post_v1_payment_intents_pi_3PiTGvFY0qyl6XeW07Oz79VA_refresh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0011_get_v1_payment_intents_pi_3PiTGyFY0qyl6XeW0PUH0y6X.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testCashAppConfirmFlows/0012_post_v1_payment_intents_pi_3PiTGyFY0qyl6XeW0PUH0y6X_refresh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0001_get_v1_payment_intents_pi_3PiTH1INV13NEzqK12Jr5dBU.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0002_post_v1_payment_intents_pi_3PiTH1INV13NEzqK12Jr5dBU_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0003_get_v1_payment_intents_pi_3PiTH1INV13NEzqK12Jr5dBU.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0006_get_v1_payment_intents_pi_3PiTH3INV13NEzqK1pkJxeaL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0007_post_v1_payment_intents_pi_3PiTH3INV13NEzqK1pkJxeaL_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0008_get_v1_payment_intents_pi_3PiTH3INV13NEzqK1pkJxeaL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0011_get_v1_payment_intents_pi_3PiTH5INV13NEzqK1SfhebHX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testFPXConfirmFlows/0012_get_v1_payment_intents_pi_3PiTH5INV13NEzqK1SfhebHX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTH7AOnZToJom10keWm7Vo.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTH7AOnZToJom10keWm7Vo_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0003_get_v1_payment_intents_pi_3PiTH7AOnZToJom10keWm7Vo.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0006_get_v1_payment_intents_pi_3PiTHAAOnZToJom10ZeKkJvu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0007_post_v1_payment_intents_pi_3PiTHAAOnZToJom10ZeKkJvu_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0008_get_v1_payment_intents_pi_3PiTHAAOnZToJom10ZeKkJvu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0011_get_v1_payment_intents_pi_3PiTHCAOnZToJom10MAh7KxU.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testGrabPayConfirmFlows/0012_get_v1_payment_intents_pi_3PiTHCAOnZToJom10MAh7KxU.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0001_get_v1_payment_intents_pi_3PiTHTFY0qyl6XeW04DQWUEN.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0002_post_v1_payment_intents_pi_3PiTHTFY0qyl6XeW04DQWUEN_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0003_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcWOgi94iiGXAWwMbz9dSzJ2xAh5S1.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0004_get_v1_payment_intents_pi_3PiTHTFY0qyl6XeW04DQWUEN.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0005_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0007_get_v1_payment_intents_pi_3PiTHWFY0qyl6XeW0MSNKjMC.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0008_post_v1_payment_intents_pi_3PiTHWFY0qyl6XeW0MSNKjMC_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0009_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcW5TtpMtB8PKkvoqj5BVNruPM7C5S.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0010_get_v1_payment_intents_pi_3PiTHWFY0qyl6XeW0MSNKjMC.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0011_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0012_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0013_get_v1_payment_intents_pi_3PiTHZFY0qyl6XeW1NaLbUlA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0014_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcWUmwEYQRrYKqJmZd4KaL5NKsND5I.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0015_get_v1_payment_intents_pi_3PiTHZFY0qyl6XeW1NaLbUlA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0016_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0017_get_v1_payment_intents_pi_3PiTHbFY0qyl6XeW0ViaYHn2.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0018_post_v1_payment_intents_pi_3PiTHbFY0qyl6XeW0ViaYHn2_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0019_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcWXqGqpErtQo8FfcwxefSINg4DeO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0020_get_v1_payment_intents_pi_3PiTHbFY0qyl6XeW0ViaYHn2.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0021_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0022_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0023_get_v1_payment_intents_pi_3PiTHdFY0qyl6XeW0GkwHXZj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0024_post_v1_payment_intents_pi_3PiTHdFY0qyl6XeW0GkwHXZj_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0025_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcWjghujsb90cOevtFxttNZUSu4r4y.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0026_get_v1_payment_intents_pi_3PiTHdFY0qyl6XeW0GkwHXZj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0028_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0029_get_v1_payment_intents_pi_3PiTHgFY0qyl6XeW156oy7ob.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0030_get_authorize_acct_1G6m1pFY0qyl6XeW_pa_nonce_QZcWHf0Wjmo92W7CSbZWyoUZ7Wjd0vq.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0031_get_v1_payment_intents_pi_3PiTHgFY0qyl6XeW156oy7ob.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0032_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0033_get_v1_setup_intents_seti_1PiTHiFY0qyl6XeWvHeP60t4.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0034_post_v1_setup_intents_seti_1PiTHiFY0qyl6XeWvHeP60t4_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0035_get_authorize_acct_1G6m1pFY0qyl6XeW_sa_nonce_QZcWwsqB422p1Fu3hbu6A7grC7cZIiP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0036_get_v1_setup_intents_seti_1PiTHiFY0qyl6XeWvHeP60t4.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0037_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0038_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0039_get_v1_setup_intents_seti_1PiTHlFY0qyl6XeWJy48DnwP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0040_post_v1_setup_intents_seti_1PiTHlFY0qyl6XeWJy48DnwP_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0041_get_authorize_acct_1G6m1pFY0qyl6XeW_sa_nonce_QZcW9RUVWuRkuPLmwGilygNKyjczOeD.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0042_get_v1_setup_intents_seti_1PiTHlFY0qyl6XeWJy48DnwP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0043_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0044_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0045_get_v1_setup_intents_seti_1PiTHoFY0qyl6XeWbU801LWM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0046_get_authorize_acct_1G6m1pFY0qyl6XeW_sa_nonce_QZcWNeuTDwJXuRafcPI0TCsslYF5yEW.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKlarnaConfirmFlows/0047_get_v1_setup_intents_seti_1PiTHoFY0qyl6XeWbU801LWM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0001_get_v1_payment_intents_pi_3PiTHpIq2LmpyICo17Aj9ab8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0002_post_v1_payment_intents_pi_3PiTHpIq2LmpyICo17Aj9ab8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0003_get_v1_payment_intents_pi_3PiTHpIq2LmpyICo17Aj9ab8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0006_get_v1_payment_intents_pi_3PiTHrIq2LmpyICo0TpElXfc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0007_post_v1_payment_intents_pi_3PiTHrIq2LmpyICo0TpElXfc_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0008_get_v1_payment_intents_pi_3PiTHrIq2LmpyICo0TpElXfc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0011_get_v1_payment_intents_pi_3PiTHtIq2LmpyICo0AGAD0LQ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testKonbiniConfirmFlows/0012_get_v1_payment_intents_pi_3PiTHtIq2LmpyICo0AGAD0LQ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTHvKG6vc7r7YC066TAvt0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTHvKG6vc7r7YC066TAvt0_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0003_get_v1_payment_intents_pi_3PiTHvKG6vc7r7YC066TAvt0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0006_get_v1_payment_intents_pi_3PiTHxKG6vc7r7YC0dYkjhBm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0007_post_v1_payment_intents_pi_3PiTHxKG6vc7r7YC0dYkjhBm_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0008_get_v1_payment_intents_pi_3PiTHxKG6vc7r7YC0dYkjhBm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0011_get_v1_payment_intents_pi_3PiTHzKG6vc7r7YC0HNzNVOz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMobilePayConfirmFlows/0012_get_v1_payment_intents_pi_3PiTHzKG6vc7r7YC0HNzNVOz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0001_get_v1_payment_intents_pi_3PiTI0FY0qyl6XeW00w0f3UO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0002_post_v1_payment_intents_pi_3PiTI0FY0qyl6XeW00w0f3UO_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0003_get_v1_payment_intents_pi_3PiTI0FY0qyl6XeW00w0f3UO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0006_get_v1_payment_intents_pi_3PiTI2FY0qyl6XeW1q37euAp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0007_post_v1_payment_intents_pi_3PiTI2FY0qyl6XeW1q37euAp_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0008_get_v1_payment_intents_pi_3PiTI2FY0qyl6XeW1q37euAp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0011_get_v1_payment_intents_pi_3PiTI4FY0qyl6XeW1QCvqch7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testMultibancoConfirmFlows/0012_get_v1_payment_intents_pi_3PiTI4FY0qyl6XeW1QCvqch7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0001_get_v1_payment_intents_pi_3PiTI6HNG4o8pO5l1omLzZGE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0002_post_v1_payment_intents_pi_3PiTI6HNG4o8pO5l1omLzZGE_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0003_get_v1_payment_intents_pi_3PiTI6HNG4o8pO5l1omLzZGE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0006_get_v1_payment_intents_pi_3PiTI8HNG4o8pO5l00QjdMZh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0007_post_v1_payment_intents_pi_3PiTI8HNG4o8pO5l00QjdMZh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0008_get_v1_payment_intents_pi_3PiTI8HNG4o8pO5l00QjdMZh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0011_get_v1_payment_intents_pi_3PiTIAHNG4o8pO5l0mGlaDkM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testOXXOConfirmFlows/0012_get_v1_payment_intents_pi_3PiTIAHNG4o8pO5l0mGlaDkM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIBAOnZToJom11KGbQC8i.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIBAOnZToJom11KGbQC8i_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0005_get_v1_payment_intents_pi_3PiTIDAOnZToJom12JSu0YfY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0006_post_v1_payment_intents_pi_3PiTIDAOnZToJom12JSu0YfY_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayNowConfirmFlows/0009_get_v1_payment_intents_pi_3PiTIFAOnZToJom118jTKXAJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIGKG6vc7r7YC1nVSqYA5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIGKG6vc7r7YC1nVSqYA5_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0003_get_authorize_acct_1JtgfQKG6vc7r7YC_pa_nonce_QZcXedDGDmgkcE9vJxyzNihKfF5Agm7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0004_get_v1_payment_intents_pi_3PiTIGKG6vc7r7YC1nVSqYA5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0005_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0007_get_v1_payment_intents_pi_3PiTIJKG6vc7r7YC1REWOJKA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0008_post_v1_payment_intents_pi_3PiTIJKG6vc7r7YC1REWOJKA_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0009_get_authorize_acct_1JtgfQKG6vc7r7YC_pa_nonce_QZcXE0hjyH8P8Wxr54IU0WYdnu1rLIf.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0010_get_v1_payment_intents_pi_3PiTIJKG6vc7r7YC1REWOJKA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0011_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0012_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0013_get_v1_payment_intents_pi_3PiTILKG6vc7r7YC13jTex7C.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0014_get_authorize_acct_1JtgfQKG6vc7r7YC_pa_nonce_QZcXuf4trSMfWJCAo888nspG1xfWsdz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPayPalConfirmFlows/0015_get_v1_payment_intents_pi_3PiTILKG6vc7r7YC13jTex7C.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIMBgCYKNuUnn1mwMKvMb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIMBgCYKNuUnn1mwMKvMb_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0005_get_v1_payment_intents_pi_3PiTIOBgCYKNuUnn1Szo5dUi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0006_post_v1_payment_intents_pi_3PiTIOBgCYKNuUnn1Szo5dUi_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testPromptPayConfirmFlows/0009_get_v1_payment_intents_pi_3PiTIQBgCYKNuUnn1vrQBvMl.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIRGoesj9fw9Q103BX9Zx.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIRGoesj9fw9Q103BX9Zx_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0003_get_v1_payment_intents_pi_3PiTIRGoesj9fw9Q103BX9Zx.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0006_get_v1_payment_intents_pi_3PiTITGoesj9fw9Q0txOGiIi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0007_post_v1_payment_intents_pi_3PiTITGoesj9fw9Q0txOGiIi_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0008_get_v1_payment_intents_pi_3PiTITGoesj9fw9Q0txOGiIi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0011_get_v1_payment_intents_pi_3PiTIWGoesj9fw9Q0hE62jyW.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testRevolutPayConfirmFlows/0012_get_v1_payment_intents_pi_3PiTIWGoesj9fw9Q0hE62jyW.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIjFY0qyl6XeW1unrjkE5.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIjFY0qyl6XeW1unrjkE5_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0005_get_v1_payment_intents_pi_3PiTIlFY0qyl6XeW1viHPW3U.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0006_post_v1_payment_intents_pi_3PiTIlFY0qyl6XeW1viHPW3U_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0007_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0009_get_v1_payment_intents_pi_3PiTInFY0qyl6XeW1hM8UL5q.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0011_get_v1_payment_intents_pi_3PiTIoFY0qyl6XeW0vuG3nGU.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0012_post_v1_payment_intents_pi_3PiTIoFY0qyl6XeW0vuG3nGU_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0013_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0015_get_v1_payment_intents_pi_3PiTIqFY0qyl6XeW1gH0KBo4.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0016_post_v1_payment_intents_pi_3PiTIqFY0qyl6XeW1gH0KBo4_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0019_get_v1_payment_intents_pi_3PiTIsFY0qyl6XeW1C1tsKon.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0020_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0021_get_v1_setup_intents_seti_1PiTItFY0qyl6XeWxlhPwXUb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0022_post_v1_setup_intents_seti_1PiTItFY0qyl6XeWxlhPwXUb_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0023_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0024_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0025_get_v1_setup_intents_seti_1PiTIuFY0qyl6XeWxQejK4vr.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0026_post_v1_setup_intents_seti_1PiTIuFY0qyl6XeWxQejK4vr_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0027_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0028_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSEPADebitConfirmFlows/0029_get_v1_setup_intents_seti_1PiTIwFY0qyl6XeWmaMZR3YH.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0001_get_v1_payment_intents_pi_3Pj9HYIFbdis1OxT0MCooEOZ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0002_post_v1_payment_intents_pi_3Pj9HYIFbdis1OxT0MCooEOZ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0003_get_v1_payment_intents_pi_3Pj9HYIFbdis1OxT0MCooEOZ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0006_get_v1_payment_intents_pi_3Pj9HaIFbdis1OxT1285sniR.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0007_post_v1_payment_intents_pi_3Pj9HaIFbdis1OxT1285sniR_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0008_get_v1_payment_intents_pi_3Pj9HaIFbdis1OxT1285sniR.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0011_get_v1_payment_intents_pi_3Pj9HcIFbdis1OxT1bYtukzw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSatispayConfirmFlows/0012_get_v1_payment_intents_pi_3Pj9HcIFbdis1OxT1bYtukzw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0001_get_v1_payment_intents_pi_3PiTIYFY0qyl6XeW0RqJBsZe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0002_post_v1_payment_intents_pi_3PiTIYFY0qyl6XeW0RqJBsZe_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0003_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0004_get_v1_payment_intents_pi_3PiTIZFY0qyl6XeW1H6Mig0H.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0005_post_v1_payment_intents_pi_3PiTIZFY0qyl6XeW1H6Mig0H_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0007_get_v1_payment_intents_pi_3PiTIbFY0qyl6XeW1NQuTErM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0009_get_v1_payment_intents_pi_3PiTIcFY0qyl6XeW0QGtaGZR.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0010_post_v1_payment_intents_pi_3PiTIcFY0qyl6XeW0QGtaGZR_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0011_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0012_get_v1_payment_intents_pi_3PiTIdFY0qyl6XeW1q5Waorp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0013_post_v1_payment_intents_pi_3PiTIdFY0qyl6XeW1q5Waorp_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0014_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0015_get_v1_payment_intents_pi_3PiTIfFY0qyl6XeW1XtzML5b.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0016_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0017_get_v1_setup_intents_seti_1PiTIgFY0qyl6XeWLPpUQMbG.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0018_post_v1_setup_intents_seti_1PiTIgFY0qyl6XeWLPpUQMbG_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0019_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0020_get_v1_setup_intents_seti_1PiTIhFY0qyl6XeWxFheNl8F.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0021_post_v1_setup_intents_seti_1PiTIhFY0qyl6XeWxFheNl8F_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0022_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSavedSEPA/0023_get_v1_setup_intents_seti_1PiTIiFY0qyl6XeWiu7YW1U8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0001_get_v1_payment_intents_pi_3PiTIxFY0qyl6XeW1pYqZmqh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0002_post_v1_payment_intents_pi_3PiTIxFY0qyl6XeW1pYqZmqh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0003_get_v1_payment_intents_pi_3PiTIxFY0qyl6XeW1pYqZmqh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0006_get_v1_payment_intents_pi_3PiTIzFY0qyl6XeW14bCK4tY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0007_post_v1_payment_intents_pi_3PiTIzFY0qyl6XeW14bCK4tY_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0008_get_v1_payment_intents_pi_3PiTIzFY0qyl6XeW14bCK4tY.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0011_get_v1_payment_intents_pi_3PiTJ1FY0qyl6XeW1z2IbX7C.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0012_get_v1_payment_intents_pi_3PiTJ1FY0qyl6XeW1z2IbX7C.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0014_get_v1_payment_intents_pi_3PiTJ2FY0qyl6XeW09FXlvBW.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0015_post_v1_payment_intents_pi_3PiTJ2FY0qyl6XeW09FXlvBW_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0016_get_v1_payment_intents_pi_3PiTJ2FY0qyl6XeW09FXlvBW.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0019_get_v1_payment_intents_pi_3PiTJ4FY0qyl6XeW07RzIcKP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0020_post_v1_payment_intents_pi_3PiTJ4FY0qyl6XeW07RzIcKP_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0021_get_v1_payment_intents_pi_3PiTJ4FY0qyl6XeW07RzIcKP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0022_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0023_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0024_get_v1_payment_intents_pi_3PiTJ6FY0qyl6XeW07WjYW7e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0025_get_v1_payment_intents_pi_3PiTJ6FY0qyl6XeW07WjYW7e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0026_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0027_get_v1_setup_intents_seti_1PiTJ7FY0qyl6XeW3knlHSRN.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0028_post_v1_setup_intents_seti_1PiTJ7FY0qyl6XeW3knlHSRN_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0029_get_v1_setup_intents_seti_1PiTJ7FY0qyl6XeW3knlHSRN.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0030_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0031_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0032_get_v1_setup_intents_seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0033_post_v1_setup_intents_seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0034_get_v1_setup_intents_seti_1PiTJ9FY0qyl6XeWy6Hu3Sy8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0035_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0036_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0037_get_v1_setup_intents_seti_1PiTJBFY0qyl6XeWsuSLNbVJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSofortConfirmFlows/0038_get_v1_setup_intents_seti_1PiTJBFY0qyl6XeWsuSLNbVJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0001_get_v1_payment_intents_pi_3PiTJCFY0qyl6XeW02odRwT7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0002_post_v1_payment_intents_pi_3PiTJCFY0qyl6XeW02odRwT7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0003_get_v1_payment_intents_pi_3PiTJCFY0qyl6XeW02odRwT7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0006_get_v1_payment_intents_pi_3PiTJEFY0qyl6XeW0HtZkX1e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0007_post_v1_payment_intents_pi_3PiTJEFY0qyl6XeW0HtZkX1e_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0008_get_v1_payment_intents_pi_3PiTJEFY0qyl6XeW0HtZkX1e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0011_get_v1_payment_intents_pi_3PiTJGFY0qyl6XeW0uOXbpda.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSunbitConfirmFlows/0012_get_v1_payment_intents_pi_3PiTJGFY0qyl6XeW0uOXbpda.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0001_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0002_post_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0003_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0004_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0005_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0006_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0007_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0008_get_v1_payment_intents_pi_3PiTJHKG6vc7r7YC1JL1HSTL.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0011_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0012_post_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0013_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0014_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0015_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0016_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0017_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0018_get_v1_payment_intents_pi_3PiTJQKG6vc7r7YC1uSovsaM.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0019_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0020_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0021_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0022_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0023_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0024_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0025_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0026_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testSwishConfirmFlows/0027_get_v1_payment_intents_pi_3PiTJYKG6vc7r7YC0mvx0JMX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0001_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0002_post_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0003_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0004_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0005_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0006_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0007_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0008_get_v1_payment_intents_pi_3PiTJgGoesj9fw9Q12D8WmHA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0011_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0012_post_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0013_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0014_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0015_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0016_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0017_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0018_get_v1_payment_intents_pi_3PiTJpGoesj9fw9Q1H4oM4xc.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0019_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0020_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0021_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0022_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0023_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0024_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0025_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0026_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testTwintConfirmFlows/0027_get_v1_payment_intents_pi_3PiTJxGoesj9fw9Q0idzhAO7.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0001_get_v1_payment_intents_pi_3PiTHDFY0qyl6XeW14CLJ6C8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0002_post_v1_payment_intents_pi_3PiTHDFY0qyl6XeW14CLJ6C8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0003_get_v1_payment_intents_pi_3PiTHDFY0qyl6XeW14CLJ6C8.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0004_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0005_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0006_get_v1_payment_intents_pi_3PiTHFFY0qyl6XeW0ZB08CTk.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0007_post_v1_payment_intents_pi_3PiTHFFY0qyl6XeW0ZB08CTk_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0008_get_v1_payment_intents_pi_3PiTHFFY0qyl6XeW0ZB08CTk.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0009_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0010_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0011_get_v1_payment_intents_pi_3PiTHHFY0qyl6XeW1b0SliHE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0012_get_v1_payment_intents_pi_3PiTHHFY0qyl6XeW1b0SliHE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0013_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0014_get_v1_payment_intents_pi_3PiTHJFY0qyl6XeW1R1AS68e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0015_post_v1_payment_intents_pi_3PiTHJFY0qyl6XeW1R1AS68e_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0016_get_v1_payment_intents_pi_3PiTHJFY0qyl6XeW1R1AS68e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0017_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0018_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0019_get_v1_payment_intents_pi_3PiTHLFY0qyl6XeW1i6upz8S.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0020_post_v1_payment_intents_pi_3PiTHLFY0qyl6XeW1i6upz8S_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0021_get_v1_payment_intents_pi_3PiTHLFY0qyl6XeW1i6upz8S.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0022_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0023_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0024_get_v1_payment_intents_pi_3PiTHNFY0qyl6XeW0ry3hAKO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0025_get_v1_payment_intents_pi_3PiTHNFY0qyl6XeW0ry3hAKO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0026_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0027_get_v1_setup_intents_seti_1PiTHOFY0qyl6XeWs8SFGyfb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0028_post_v1_setup_intents_seti_1PiTHOFY0qyl6XeWs8SFGyfb_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0029_get_v1_setup_intents_seti_1PiTHOFY0qyl6XeWs8SFGyfb.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0030_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0031_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0032_get_v1_setup_intents_seti_1PiTHQFY0qyl6XeWFjQpkQ40.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0033_post_v1_setup_intents_seti_1PiTHQFY0qyl6XeWFjQpkQ40_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0034_get_v1_setup_intents_seti_1PiTHQFY0qyl6XeWFjQpkQ40.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0035_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0036_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0037_get_v1_setup_intents_seti_1PiTHSFY0qyl6XeWUxtVchWu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLPMConfirmFlowTests/testiDEALConfirmFlows/0038_get_v1_setup_intents_seti_1PiTHSFY0qyl6XeWUxtVchWu.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testLoadPerformance/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentFails/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentFails/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentFails/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentFails/0003_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentSucceeds/0000_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentSucceeds/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentSucceeds/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentSucceeds/0003_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadDeferredIntentSucceeds/0004_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0001_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0002_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0003_get_v1_customers_cus_OtOGvD0ZVacBoj.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0004_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0005_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadFiltersSavedApplePayCards/0006_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithExternalPaymentMethods/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithExternalPaymentMethods/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithInvalidExternalPaymentMethods/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithInvalidExternalPaymentMethods/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithPaymentIntent/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithSetupIntent/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithSetupIntentAttachedPaymentMethod/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/PaymentSheetLoaderTest/testPaymentSheetLoadWithSetupIntentAttachedPaymentMethod/0001_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadPaymentIntentClientSecretErrors/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadPaymentIntentClientSecretErrors/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadPaymentIntentClientSecretErrors/0002_get_v1_payment_intents_pi_bad.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadSetupIntentClientSecretErrors/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadSetupIntentClientSecretErrors/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testBadSetupIntentClientSecretErrors/0002_get_v1_setup_intents_seti_bad.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0003_get_v1_payment_intents_pi_3PiRyUFY0qyl6XeW1tWilBBA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0004_post_v1_payment_intents_pi_3PiRyUFY0qyl6XeW1tWilBBA_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterPaymentIntentConfirmsStillSucceeds/0005_get_v1_payment_intents_pi_3PiRyUFY0qyl6XeW1tWilBBA.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0002_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0003_get_v1_setup_intents_seti_1PiRyXFY0qyl6XeWGpoZl2L6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0004_post_v1_setup_intents_seti_1PiRyXFY0qyl6XeWGpoZl2L6_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelAfterSetupIntentConfirmsStillSucceeds/0005_get_v1_setup_intents_seti_1PiRyXFY0qyl6XeWGpoZl2L6.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelBeforeIntentConfirmsCancels/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCancelBeforeIntentConfirmsCancels/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0003_get_v1_payment_intents_pi_3PiRyaFY0qyl6XeW1FHXpHhE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0004_post_v1_payment_intents_pi_3PiRyaFY0qyl6XeW1FHXpHhE_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntent/0005_get_v1_payment_intents_pi_3PiRyaFY0qyl6XeW1FHXpHhE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0003_get_v1_payment_intents_pi_3PiRydFY0qyl6XeW11cbTNpX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0004_post_v1_payment_intents_pi_3PiRydFY0qyl6XeW11cbTNpX_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesAutomaticConfirmationPaymentIntentManualCapture/0005_get_v1_payment_intents_pi_3PiRydFY0qyl6XeW11cbTNpX.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesManualConfirmationPaymentIntent/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesManualConfirmationPaymentIntent/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesManualConfirmationPaymentIntent/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesManualConfirmationPaymentIntent/0003_get_v1_payment_intents_pi_3PiRyfFY0qyl6XeW14TKwmwB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesManualConfirmationPaymentIntent/0004_get_v1_payment_intents_pi_3PiRyfFY0qyl6XeW14TKwmwB.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0002_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0003_get_v1_setup_intents_seti_1PiRyiFY0qyl6XeWO3Bxrlh3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0004_post_v1_setup_intents_seti_1PiRyiFY0qyl6XeWO3Bxrlh3_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayContextFunctionalTest/testCompletesSetupIntent/0005_get_v1_setup_intents_seti_1PiRyiFY0qyl6XeWO3Bxrlh3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayFunctionalTest/testCreateSourceWithPayment/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayFunctionalTest/testCreateTokenWithPayment/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPApplePayFunctionalTest/testCreateTokenWithPaymentClassic/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPBankAccountFunctionalTest/testCreateAndRetreiveBankAccountToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPBankAccountFunctionalTest/testInvalidKey/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testCardTokenCreationWithExpiredCard/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testCardTokenCreationWithInvalidParams/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testCreateCVCUpdateToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testCreateCardToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testInvalidCVC/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPCardFunctionalTest/testInvalidKey/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPFileFunctionalTest/testCreateFileForDisputeEvidence/0000_post_v1_files.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPFileFunctionalTest/testCreateFileForIdentityDocument/0000_post_v1_files.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPFileFunctionalTest/testInvalidKey/0000_post_v1_files.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPIIFunctionalTest/testCreatePersonallyIdentifiableInformationToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPIIFunctionalTest/testSSNLast4Token/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardpaymentintentserversideconfirmation/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardpaymentintentserversideconfirmation/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardpaymentintentserversideconfirmation/0002_get_v1_payment_intents_pi_3PiRzAFY0qyl6XeW0U7X5Y2j.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardsetupintentserversideconfirmation/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardsetupintentserversideconfirmation/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testcardsetupintentserversideconfirmation/0002_get_v1_setup_intents_seti_1PiRzBFY0qyl6XeW1yPMQw10.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testconfirmpaymentintentsavedpmsendsanalytic/0000_post_v1_payment_intents_pi_3P20wFFY0qyl6XeW0dSOQ6W7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testconfirmpaymentintentsendsanalytic/0000_post_v1_payment_intents_pi_3P20wFFY0qyl6XeW0dSOQ6W7_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testconfirmsetupintentsavedpmsendsanalytic/0000_post_v1_setup_intents_seti_1P1xLBFY0qyl6XeWc7c2LrMK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testconfirmsetupintentsendsanalytic/0000_post_v1_setup_intents_seti_1P1xLBFY0qyl6XeWc7c2LrMK_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testhandlenextactionpaymentintentsendsanalytic/0000_get_v1_payment_intents_pi_3P232pFY0qyl6XeW0FFRtE0A.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testhandlenextactionsetupintentsendsanalytic/0000_get_v1_setup_intents_seti_3P232pFY0qyl6XeW0FFRtE0A.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitpaymentintentserversideconfirmation/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitpaymentintentserversideconfirmation/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitpaymentintentserversideconfirmation/0002_get_v1_payment_intents_pi_3PiRzEFY0qyl6XeW1uNVnv0e.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitsetupintentserversideconfirmation/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitsetupintentserversideconfirmation/0001_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerFunctionalSwiftTest/testsepadebitsetupintentserversideconfirmation/0002_get_v1_setup_intents_seti_1PiRzGFY0qyl6XeWnnWjBj6k.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentHandlerStubbedTests/testCanPresentErrorsAreReported/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmAUBECSDebitPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmAUBECSDebitPaymentIntent/0001_post_v1_payment_intents_pi_3PiRzZF7QokQdxBy1sf6aNUz_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmAlipayPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmAlipayPaymentIntent/0001_post_v1_payment_intents_pi_3PiVNGFY0qyl6XeW1lshp0dO_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCanceledPaymentIntentFails/0000_post_v1_payment_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithInvalidNetworkParam/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithInvalidNetworkParam/0001_post_v1_payment_intents_pi_3PiRzbFY0qyl6XeW1Ph4tDwk_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithNetworkParam/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithNetworkParam/0001_post_v1_payment_intents_pi_3PiRzbFY0qyl6XeW16Hu1D9Z_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithoutNetworkParam/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmCardWithoutNetworkParam/0001_post_v1_payment_intents_pi_3PiRzdFY0qyl6XeW09qa59NF_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWith3DSCardPaymentMethodSucceeds/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWith3DSCardPaymentMethodSucceeds/0001_post_v1_payment_intents_pi_3PiRzfFY0qyl6XeW07ggWuUF_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWith3DSCardSucceeds/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWith3DSCardSucceeds/0001_post_v1_payment_intents_pi_3PiRzgFY0qyl6XeW1alcqdDr_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAffirm/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAffirm/0001_post_v1_payment_intents_pi_3PiRziFY0qyl6XeW1WJEsbnY_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAlma/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAlma/0001_post_v1_payment_intents_pi_3PiRzjKG6vc7r7YC0O3aj1rG_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAmazonPay/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithAmazonPay/0001_post_v1_payment_intents_pi_3PiRzkFY0qyl6XeW1HtdYNS8_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBLIK/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBLIK/0001_post_v1_payment_intents_pi_3PiRznArGMi59tL419JSHquh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBancontact/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBancontact/0001_post_v1_payment_intents_pi_3PiRzmFY0qyl6XeW1VtQrypE_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBillie/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithBillie/0001_post_v1_payment_intents_pi_3PkUZjAlz2yHYCNZ0bXjYgHZ_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithEPS/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithEPS/0001_post_v1_payment_intents_pi_3PiRzoFY0qyl6XeW0dXgRsGO_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithGrabPay/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithGrabPay/0001_post_v1_payment_intents_pi_3PiRzpAOnZToJom11VRsZSvi_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithMobilePay/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithMobilePay/0001_post_v1_payment_intents_pi_3PiRzqKG6vc7r7YC1FL5KNGy_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithMultibanco/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithMultibanco/0001_post_v1_payment_intents_pi_3PiRzrFY0qyl6XeW0ACFC0n6_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithOXXO/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithOXXO/0001_post_v1_payment_intents_pi_3PiRzsHNG4o8pO5l09utfXDf_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithPayPal/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithPayPal/0001_post_v1_payment_intents_pi_3PiRztArGMi59tL40UISz6Ce_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithPrzelewy24/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithPrzelewy24/0001_post_v1_payment_intents_pi_3PiRzuFY0qyl6XeW0OpNxFXf_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithSatispay/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithSatispay/0001_post_v1_payment_intents_pi_3Pj9JiIFbdis1OxT1hf8gUZn_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithShippingDetailsSucceeds/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithShippingDetailsSucceeds/0001_post_v1_payment_intents_pi_3PiRzvFY0qyl6XeW0FgxEVqu_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithSunbit/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithSunbit/0001_post_v1_payment_intents_pi_3PiRzwFY0qyl6XeW1UTuddcE_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithAmounts/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithAmounts/0001_post_v1_payment_intents_pi_3PiRzxFY0qyl6XeW1frhhQo3_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithAmounts/0002_post_v1_payment_intents_pi_3PiRzxFY0qyl6XeW1frhhQo3_verify_microdeposits.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithDescriptorCode/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithDescriptorCode/0001_post_v1_payment_intents_pi_3PiRzzFY0qyl6XeW1mDn9qw9_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmPaymentIntentWithUSBankAccountverifyWithDescriptorCode/0002_post_v1_payment_intents_pi_3PiRzzFY0qyl6XeW1mDn9qw9_verify_microdeposits.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmUSBankAccountWithPaymentMethodOptions/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testConfirmUSBankAccountWithPaymentMethodOptions/0001_post_v1_payment_intents_pi_3PiS01FY0qyl6XeW1gWPnvBN_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testCreatePaymentIntentWithInvalidCurrency/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testCreatePaymentIntentWithTestingServer/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testRetrieveMismatchedPublishableKey/0000_get_v1_payment_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testRetrievePreviousCreatedPaymentIntent/0000_get_v1_payment_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentIntentFunctionalTest/testRetrieveWithWrongSecret/0000_get_v1_payment_intents_pi_1GGCGfFY0qyl6XeWbSAsh2hn.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAUBECSDebitParamsTests/testCreateAUBECSPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAUBECSDebitTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1GaRLjF7QokQdxByYgFPQEi0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAUBECSDebitTests/testFailWithoutRequired/0000_get_v1_payment_intents_pi_1GaRLjF7QokQdxByYgFPQEi0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAffirmParamsTests/testCreateAffirmPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAffirmTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3KUFbTFY0qyl6XeW1oDBbiQk.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAfterpayClearpayParamsTest/testCreateAfterpayClearpayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAfterpayClearpayTest/testCorrectParsing/0000_get_v1_payment_intents_pi_1HbSAfFY0qyl6XeWRnlezJ7K.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAlmaParamsTests/testCreateAlmaPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAlmaTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3Oz1AfKG6vc7r7YC0VaP6KiE.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAmazonPayParamsTests/testCreateAmazonPayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodAmazonPayTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3OmQQ0FY0qyl6XeW0H4X6eI0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodBancontactParamsTests/testCreateBancontactPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodBancontactTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1GdPnbFY0qyl6XeW8Ezvxe87.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodBillieTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodBoletoParamsTests/testCreateBoletoPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodBoletoTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3JYFj9JQVROkWvqT0d2HYaTk.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodCardTest/testCorrectParsing/0000_get_v1_payment_intents_pi_1H5J4RFY0qyl6XeWFTpgue7g.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodCashAppParamsTests/testCreateCashAppPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodCashAppTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3MMa4NFY0qyl6XeW1FM3HOts.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodEPSParamsTests/testCreateEPSPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodEPSTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1Gj0rqFY0qyl6XeWrug30CPz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateAlipayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateAlmaPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateAmazonPayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateBLIKPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateBacsPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateBilliePaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateCardPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateMobilePayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateMultibancoPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateSatispayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testCreateSunbitPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0002_post_v1_payment_methods_pm_1PiS0CFY0qyl6XeWcO0sdTlc_attach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0003_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0004_post_v1_payment_methods_pm_1PiS0DFY0qyl6XeWZ9i3YHcf_attach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0005_post_create_customer_session_cs.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0006_get_v1_elements_sessions.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0007_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0008_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0009_post_v1_payment_methods_pm_1PiS0DFY0qyl6XeWZ9i3YHcf_detach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0010_post_v1_payment_methods_pm_1PiS0CFY0qyl6XeWcO0sdTlc_detach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testMulitpleCardCreationWithCustomerSessionAndMultiDelete/0011_get_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testUpdateCardPaymentMethod/0000_post_create_ephemeral_key.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testUpdateCardPaymentMethod/0001_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testUpdateCardPaymentMethod/0002_post_v1_payment_methods_pm_1PiS0GKG6vc7r7YC33E9uYEd_attach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testUpdateCardPaymentMethod/0003_post_v1_payment_methods_pm_1PiS0GKG6vc7r7YC33E9uYEd.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodFunctionalTest/testUpdateCardPaymentMethod/0004_post_v1_payment_methods_pm_1PiS0GKG6vc7r7YC33E9uYEd_detach.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodGiropayParamsTests/testCreateGiropayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodGiropayTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1GfsdtFY0qyl6XeWLnepIXCI.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodGrabPayParamsTest/testCreateGrabPayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodKlarnaParamsTests/testCreateKlarnaPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodKlarnaTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3Jn3kUFY0qyl6XeW0mCp95UD.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodMobilePayParamsTests/testCreateMobilePayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodMobilePayTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3PGVQJKG6vc7r7YC1Xs7oiWw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodModernTest/testCreateCardPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodModernTest/testCreateCardPaymentMethodWithAdditionalAPIStuff/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodMultibancoParamsTests/testCreateMultibancoPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodMultibancoTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3P8R5lFY0qyl6XeW0byterUo.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodNetBankingParamsTests/testCreateNetBankingPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodNetBankingTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1HoPqsBte6TMTRd4jX0PwrFa.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOXXOParamsTests/testCreateOXXOPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOXXOTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1GvAdyHNG4o8pO5l0dr078gf.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0001_get_v1_payment_intents_pi_3PiS0MFY0qyl6XeW02iKCVoz.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0002_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0003_get_v1_payment_intents_pi_3PiS0NFY0qyl6XeW1qkmS1Of.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0004_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0005_get_v1_payment_intents_pi_3PiS0OFY0qyl6XeW1vPzg3R0.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0006_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0007_get_v1_payment_intents_pi_3PiS0OFY0qyl6XeW0aiyhz3B.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0008_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsPaymentIntent/0009_get_v1_payment_intents_pi_3PiS0PFY0qyl6XeW12ednumf.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0001_get_v1_setup_intents_seti_1PiS0QFY0qyl6XeWGpO1S6kt.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0002_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0003_get_v1_setup_intents_seti_1PiS0QFY0qyl6XeWj297PnQP.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0004_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0005_get_v1_setup_intents_seti_1PiS0RFY0qyl6XeWGRj0nMpO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0006_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0007_get_v1_setup_intents_seti_1PiS0RFY0qyl6XeWlU8f2VRe.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0008_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodOptionsTest/testUSBankAccountOptionsSetupIntent/0009_get_v1_setup_intents_seti_1PiS0SFY0qyl6XeWWOPVOd70.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodPayPalParamsTests/testCreatePayPalPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodPayPalTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1HcI17FY0qyl6XeWcFAAbZCw.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodPrzelewy24ParamsTests/testCreatePrzelewy24PaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodPrzelewy24Tests/testCorrectParsing/0000_get_v1_payment_intents_pi_1GciHFFY0qyl6XeWp9RdhmFF.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodRevolutPayParamsTests/testCreateRevolutPayPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodRevolutPayTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSatispayTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3PXrkJIFbdis1OxT0XLmWug3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSofortParamsTests/testCreateSofortPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSofortTests/testCorrectParsing/0000_get_v1_payment_intents_pi_1HDdfSFY0qyl6XeWjk7ogYVV.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSunbitPayTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3NqgBBGoesj9fw9Q1TkY7iBp.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSwishParamsTests/testCreateSwishPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodSwishTests/testObjectDecoding/0000_get_v1_payment_intents_pi_3Nsu6oKG6vc7r7YC1FJJPNjg.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUPIParamsTests/testCreateUPIPaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUPITests/testCorrectParsing/0000_get_v1_payment_intents_pi_1HlYxxBte6TMTRd48W66zjTJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUSBankAccountParamsTest/testCreateUSBankAccountPaymentMethodcheckingindividual/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUSBankAccountParamsTest/testCreateUSBankAccountPaymentMethodcheckingindividualsetwithstring/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUSBankAccountParamsTest/testCreateUSBankAccountPaymentMethodsavingscompany/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUSBankAccountParamsTest/testCreateUSBankAccountPaymentMethodsavingscompanysetwithstring/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPPaymentMethodUSBankAccountTest/testObjectDecoding/0000_get_v1_payment_intents_pi_3KhHLqFY0qyl6XeW1X2ZMsOT.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPRadarSessionFunctionalTest/testCreateWithoutInitialFraudDetection/0000_post_v1_radar_session.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPRadarSessionFunctionalTest/testCreateWithoutInitialFraudDetection/0001_post_v1_radar_session.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmAUBECSDebitSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmAUBECSDebitSetupIntent/0001_post_v1_setup_intents_seti_1PiS0aF7QokQdxByBD1C7K6i_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentSucceeds/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentSucceeds/0001_post_v1_setup_intents_seti_1PiS0bFY0qyl6XeWH07n8saf_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithAmounts/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithAmounts/0001_post_v1_setup_intents_seti_1PiS0cFY0qyl6XeWrqnyI4iv_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithAmounts/0002_post_v1_setup_intents_seti_1PiS0cFY0qyl6XeWrqnyI4iv_verify_microdeposits.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithDescriptorCode/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithDescriptorCode/0001_post_v1_setup_intents_seti_1PiS0dFY0qyl6XeW8aP6E5LA_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testConfirmSetupIntentWithUSBankAccountverifyWithDescriptorCode/0002_post_v1_setup_intents_seti_1PiS0dFY0qyl6XeW8aP6E5LA_verify_microdeposits.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testCreateSetupIntentWithTestingServer/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSetupIntentFunctionalTestSwift/testRetrieveSetupIntentSucceeds/0000_get_v1_setup_intents_seti_1GGCuIFY0qyl6XeWVfbQK6b3.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcealipay/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcebancontact/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcecard/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceeps/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceepsnostatementdescriptor/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceideal/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceidealemptyOptionalFields/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceidealmissingOptionalFields/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourceklarna/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcemultibanco/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcep24/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcesepaDebit/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcesepaDebitNoAddress/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcesofort/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testCreateSourcewechatPaytestMode/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testRetrieveSourcesofort/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/STPSourceFunctionalTest/testRetrieveSourcesofort/0001_get_v1_sources_src_1PiS0oBbvEcIpqUbSyzeANyO.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCVCUpdate/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCardToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmPaymentIntent/0001_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmPaymentIntent/0002_post_v1_payment_intents_pi_3PiS0qFY0qyl6XeW13o46Tjh_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmPaymentIntent/0003_post_v1_payment_intents_pi_3PiS0qFY0qyl6XeW0SQF0wEH_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testConfirmSetupIntent/0001_post_v1_setup_intents_seti_1PiS0rFY0qyl6XeWQlOZvEao_confirm.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateApplePayToken/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateApplePayToken/0001_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateApplePayToken/0002_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateConnectAccount/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreatePaymentMethod/0000_post_v1_payment_methods.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateRadarSession/0000_post_v1_radar_session.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateRetrieveAndPollSource/0000_post_v1_sources.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateRetrieveAndPollSource/0001_get_v1_sources_src_1PiS0tFY0qyl6XeWYLZvQ3TJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateRetrieveAndPollSource/0002_get_v1_sources_src_1PiS0tFY0qyl6XeWYLZvQ3TJ.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateTokenWithBankAccount/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateTokenWithPII/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testCreateTokenWithSSNLast4/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testPKPaymentError/0000_post_v1_tokens.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRefreshPaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRefreshPaymentIntent/0001_post_v1_payment_intents_pi_3PiS0vFY0qyl6XeW1D8gxVy2_refresh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRefreshSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRefreshSetupIntent/0001_post_v1_setup_intents_seti_1PiS0wFY0qyl6XeWYrQ3d3Gb_refresh.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRetrievePaymentIntent/0000_post_create_payment_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRetrievePaymentIntent/0001_get_v1_payment_intents_pi_3PiS0xFY0qyl6XeW1QvdM01s.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRetrievePaymentIntent/0002_get_v1_payment_intents_pi_3PiS0xFY0qyl6XeW1QvdM01s.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRetrieveSetupIntent/0000_post_create_setup_intent.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testRetrieveSetupIntent/0001_get_v1_setup_intents_seti_1PiS0xFY0qyl6XeWSVqYxcJi.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/StripeAPIBridgeNetworkTest/testUploadFile/0000_post_v1_files.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/TextFieldElementCardTest/testBINRangeThatRequiresNetworkCallToValidate/0000_get_edge-internal_card-metadata.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/TextFieldElementCardTest/testBINRangeThatRequiresNetworkCallToValidateWhenCallFails/0000_get_edge-internal_card-metadata.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/TextFieldElementCardTest/testBINRangeThatRequiresNetworkCallToValidateWhenCallFails/0001_get_edge-internal_card-metadata.tail create mode 100644 StripePayments/StripePaymentsTestUtils/Resources/recorded_network_traffic/TextFieldElementCardTest/testPANValidation/0000_get_edge-internal_card-metadata.tail create mode 100644 StripePayments/StripePaymentsTestUtils/STPFixtures+Swift.swift create mode 100644 StripePayments/StripePaymentsTestUtils/STPNetworkStubbingTestCase.swift create mode 100644 StripePayments/StripePaymentsTestUtils/STPTestAPIClient+Swift.swift create mode 100644 StripePayments/StripePaymentsTestUtils/STPTestingAPIClient+Swift.swift create mode 100644 StripePayments/StripePaymentsTestUtils/StripePaymentsTestUtils.h create mode 100644 StripePayments/StripePaymentsTests/Captcha/DispatchQueue__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptchaDecoder__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptchaResult__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptchaWebViewManager__HTML__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptchaWebViewManager__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptcha__Bench.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptcha__Config__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/HCaptcha__Tests.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/Helpers/HCaptchaConfig+Helpers.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/Helpers/HCaptchaDecoder+Helper.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/Helpers/HCaptchaError+Equatable.swift create mode 100644 StripePayments/StripePaymentsTests/Captcha/Helpers/HCaptchaWebViewManager+Helpers.swift create mode 100644 StripePayments/StripePaymentsTests/Info.plist create mode 100644 StripePayments/StripePaymentsTests/STPAnalyticsClient+StripePayments.swift create mode 100644 StripePayments/StripePaymentsTests/mock.html create mode 100644 StripePaymentsUI/README.md create mode 100644 StripePaymentsUI/StripePaymentsUI.xcodeproj/project.pbxproj create mode 100644 StripePaymentsUI/StripePaymentsUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripePaymentsUI/StripePaymentsUI.xcodeproj/xcshareddata/xcschemes/StripePaymentsUI.xcscheme create mode 100644 StripePaymentsUI/StripePaymentsUI/Docs.docc/StripePaymentsUI.md create mode 100644 StripePaymentsUI/StripePaymentsUI/Info.plist create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/JSON/au_becs_bsb.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/tk.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/anz.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/anz.imageset/anz.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/bankofmelbourne.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/bankofmelbourne.imageset/bankofmelbourne.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/banksa.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/banksa.imageset/banksa.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/bankwest.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/bankwest.imageset/bankwest.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/boq.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/boq.imageset/boq.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/cba.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/cba.imageset/cba.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/nab.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/nab.imageset/nab.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/stgeorges.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/stgeorges.imageset/stgeorges.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/stripe.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/stripe.imageset/stripe.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/suncorpmetway.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/suncorpmetway.imageset/suncorpmetway.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/westpac.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/BECS/westpac.imageset/westpac.pdf create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_amex.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_amex.imageset/icon-card-amex.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_amex_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_amex_template.imageset/vector (6).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_applepay.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_applepay.imageset/stp_card_applepay.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_applepay.imageset/stp_card_applepay@2x.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_applepay.imageset/stp_card_applepay@3x.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cartes_bancaires.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cartes_bancaires.imageset/icon-card-cartebancaire.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cartes_bancaires_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cartes_bancaires_template.imageset/vector (12).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cbc.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cbc.imageset/cbcDark.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cbc.imageset/cbcLight.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc.imageset/cvcDark.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc.imageset/cvcLight.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc_amex.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc_amex.imageset/cvcAmexDark.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_cvc_amex.imageset/cvcAmexLight.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_diners.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_diners.imageset/icon-card-diners-club.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_diners_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_diners_template.imageset/Diners Club-bw.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_discover.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_discover.imageset/icon-card-discover.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_discover_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_discover_template.imageset/vector (8).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_error.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_error.imageset/cardError.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_jcb.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_jcb.imageset/icon-card-jcb.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_jcb_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_jcb_template.imageset/vector (9).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_maestro.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_maestro.imageset/icon-card-maestro.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard.imageset/icon-card-mastercard.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard_template.imageset/stp_card_mastercard_template.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard_template.imageset/stp_card_mastercard_template@2x.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_mastercard_template.imageset/stp_card_mastercard_template@3x.png create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unionpay.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unionpay.imageset/icon-card-unionpay.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unionpay_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unionpay_template.imageset/vector (7).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unknown.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unknown.imageset/cardDark.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_unknown.imageset/cardLight.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_visa.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_visa.imageset/icon-card-visa.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_visa_template.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Cards/stp_card_visa_template.imageset/vector (11).svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_amex.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_amex.imageset/stp_card_unpadded_amex.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_cartes_bancaires.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_cartes_bancaires.imageset/cartesBancairesNoPadding.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_diners_club.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_diners_club.imageset/stp_card_unpadded_diners_club.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_discover.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_discover.imageset/stp_card_unpadded_discover.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_jcb.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_jcb.imageset/stp_card_unpadded_jcb.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_maestro.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_maestro.imageset/stp_card_unpadded_maestro.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_mastercard.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_mastercard.imageset/stp_card_unpadded_mastercard.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_unionpay.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_unionpay.imageset/stp_card_unpadded_unionpay.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_visa.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/CardsNoPadding/stp_card_unpadded_visa.imageset/visaNoPadding.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/stp_icon_bank.imageset/Contents.json create mode 100644 StripePaymentsUI/StripePaymentsUI/Resources/StripePaymentsUI.xcassets/stp_icon_bank.imageset/icon-pm-bank-color.svg create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Categories/Enums+CustomStringConvertible.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/CardElementConfigService.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPBECSDebitAccountNumberValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPBSBNumberValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPCBCController.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPImageLibrary.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPLocalizedString.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPPhoneNumberValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPPostalCodeValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPPromise.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/STPStringUtils.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/String+Localized.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/StripePayments+Export.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Helpers/StripePaymentsBundleLocator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/Categories/NSAttributedString+Stripe.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/Categories/STPCardBrand+PaymentsUI.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/Categories/UIButton+Stripe.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Elements/DropDownFieldElement+CardBrand.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/CardBrandView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/DynamicImageView+Unknown.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardCVCInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardCVCInputTextFieldFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardCVCInputTextFieldValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardExpiryInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardExpiryInputTextFieldFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardExpiryInputTextFieldValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardNumberInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardNumberInputTextFieldFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPCardNumberInputTextFieldValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPPostalCodeInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPPostalCodeInputTextFieldFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/Card/STPPostalCodeInputTextFieldValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPCountryPickerInputField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPGenericInputPickerField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPGenericInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPInputTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPInputTextFieldFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPInputTextFieldValidator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/Inputs/STPNumericDigitInputTextFormatter.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPAUBECSFormViewModel.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPCardLoadingIndicator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPFormTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPLabeledFormTextFieldView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPLabeledMultiFormTextFieldView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPPaymentCardTextFieldViewModel.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPValidatedTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/Internal/UI/Views/STPViewWithSeparator.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/PaymentMethodMessagingView+Configuration.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/PaymentMethodMessagingView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPAUBECSDebitFormView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView+SwiftUI.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPFloatingPlaceholderTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPFormTextFieldContainer.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPFormView.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPMultiFormTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPPaymentCardTextField+SwiftUI.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPPaymentCardTextField.swift create mode 100644 StripePaymentsUI/StripePaymentsUI/StripePaymentsUI.h create mode 100644 StripePaymentsUI/StripePaymentsUITests/CardElementConfigServiceTests.swift create mode 100644 StripePaymentsUI/StripePaymentsUITests/Info.plist create mode 100644 StripePaymentsUI/StripePaymentsUITests/STPAnalyticsClient+PaymentsUITests.swift create mode 100644 StripePaymentsUI/StripePaymentsUITests/STPPaymentCardTextFieldSnapshotTests.swift create mode 100644 StripeUICore/StripeUICore.xcodeproj/project.pbxproj create mode 100644 StripeUICore/StripeUICore.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 StripeUICore/StripeUICore.xcodeproj/xcshareddata/xcschemes/StripeUICore.xcscheme create mode 100644 StripeUICore/StripeUICore/Info.plist create mode 100644 StripeUICore/StripeUICore/Resources/JSON/au_becs_bsb.json create mode 100644 StripeUICore/StripeUICore/Resources/JSON/localized_address_data.json create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/bg-BG.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ca-ES.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/cs-CZ.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/da.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/de.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/el-GR.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/en-GB.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/en.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/es-419.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/es.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/et-EE.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/fi.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/fil.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/fr-CA.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/fr.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/hr.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/hu.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/id.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/it.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ja.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ko.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/lt-LT.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/lv-LV.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ms-MY.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/mt.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/nb.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/nl.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/nn-NO.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/pl-PL.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/pt-BR.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/pt-PT.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ro-RO.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/ru.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/sk-SK.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/sl-SI.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/sv.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/tr.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/vi.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/zh-HK.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/zh-Hans.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/Localizations/zh-Hant.lproj/Localizable.strings create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/Contents.json create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/brand_stripe.imageset/Contents.json create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/brand_stripe.imageset/brand_stripe.svg create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/form_error_icon.imageset/Contents.json create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/form_error_icon.imageset/form_error_icon.pdf create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/form_error_icon.imageset/form_error_icon_dark.pdf create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/icon_chevron_down.imageset/Contents.json create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/icon_chevron_down.imageset/chevronDown.pdf create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/icon_clear.imageset/Contents.json create mode 100644 StripeUICore/StripeUICore/Resources/StripeUICore.xcassets/icon_clear.imageset/icon_clear.svg create mode 100644 StripeUICore/StripeUICore/Source/Categories/CALayer+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/Enums+CustomStringConvertible.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/Locale+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/NSAttributedString+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/NSDirectionalEdgeInsets+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIBarButtonItem+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIButton+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIColor+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIFont+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIKeyboardType+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UISpringTimingParameters+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIStackView+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UITraitCollection+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIView+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIViewController+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Categories/UIWindow+StripeUICore.swift create mode 100644 StripeUICore/StripeUICore/Source/Controls/ActivityIndicator.swift create mode 100644 StripeUICore/StripeUICore/Source/Controls/Button.swift create mode 100644 StripeUICore/StripeUICore/Source/Controls/OneTimeCodeTextField-TextStorage.swift create mode 100644 StripeUICore/StripeUICore/Source/Controls/OneTimeCodeTextField.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxButton.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Checkbox/CheckboxElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/ContainerElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/DateFieldElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/DropdownFieldElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Element.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/Address/AddressSectionElement+DummyAddressLine.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/Address/AddressSectionElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/Address/AddressSpec+ElementFactory.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/Address/AddressSpec.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/Address/AddressSpecProvider.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/BSB/BSBNumberProvider.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/DropdownFieldElement+AddressFactory.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/IDNumberTextFieldConfiguration.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+AccountFactory.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+AddressFactory.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+Factory.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Form/FormElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Form/FormView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/PhoneNumber/PhoneNumberElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/PickerField/PickerFieldView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/PickerField/PickerTextField.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Section/SectionContainerView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Section/SectionElement+MultiElementRow.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Section/SectionElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/Section/SectionView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/StaticElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/FloatingPlaceholderTextFieldView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement+Validation.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElementConfiguration.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldFormatter.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldView.swift create mode 100644 StripeUICore/StripeUICore/Source/Elements/TextOrDropdownElement.swift create mode 100644 StripeUICore/StripeUICore/Source/Events.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/CompatibleColor.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/ImageMaker.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/InputFormColors.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/RegionCodeProvider.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/STPLocalizedString.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/StackViewWithSeparator.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/String+CountryEmoji.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/String+Localized.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/String+RegionCodeProvider.swift create mode 100644 StripeUICore/StripeUICore/Source/Helpers/StripeUICoreBundleLocator.swift create mode 100644 StripeUICore/StripeUICore/Source/Image.swift create mode 100644 StripeUICore/StripeUICore/Source/Validators/BankRoutingNumber.swift create mode 100644 StripeUICore/StripeUICore/Source/Validators/PhoneNumber.swift create mode 100644 StripeUICore/StripeUICore/Source/Validators/STPBlikCodeValidator.swift create mode 100644 StripeUICore/StripeUICore/Source/Validators/STPEmailAddressValidator.swift create mode 100644 StripeUICore/StripeUICore/Source/Validators/STPVPANumberValidator.swift create mode 100644 StripeUICore/StripeUICore/Source/Views/DoneButtonToolbar.swift create mode 100644 StripeUICore/StripeUICore/Source/Views/DynamicHeightContainerView.swift create mode 100644 StripeUICore/StripeUICore/Source/Views/DynamicImageView.swift create mode 100644 StripeUICore/StripeUICore/Source/Views/LinkOpeningTextView.swift create mode 100644 StripeUICore/StripeUICore/StripeUICore.h create mode 100644 StripeUICore/StripeUICoreTests/Info.plist create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Controls/ButtonSnapshotTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Elements/AddressSectionElementSnapshotTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Elements/CheckboxButtonSnapshotTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Elements/DateFieldElementSnapshotTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Elements/DropdownFieldElementSnapshotTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Snapshot/Elements/PhoneNumberElementSnapshotTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Categories/Locale+StripeUICoreTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Categories/NSAttributedString+StripeUICoreTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Categories/UIColor+StripeUICoreTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/AddressSectionElementTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/AddressSpecProviderTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/BSBNumberProviderTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/DateFieldElementTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/DropdownFieldElementTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/IDNumberTextFieldConfigurationTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/PhoneNumberElementTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/SectionElementTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/TestFieldElement+AccountFactoryTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/TextFieldElement+AddressFactoryTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/TextFieldElementTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Elements/TextFieldFormatterTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Validators/BSBNumberTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Validators/PhoneNumberTests.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Validators/STPBlikCodeValidatorTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Validators/STPEmailAddressValidatorTest.swift create mode 100644 StripeUICore/StripeUICoreTests/Unit/Validators/STPVPANumberValidatorTest.swift create mode 100644 VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..092723aa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1412 @@ +## 23.29.1 2024-08-12 +### PaymentSheet +* [Fixed] Fixed an issue where signing up with Link and paying would vend an empty `STPPaymentMethod` object to an `IntentConfiguration` confirmHandler callback. +* [Fixed] Fixed PaymentSheet.FlowController returning unlocalized labels for certain payment methods e.g. "AfterPay ClearPay" instead of "Afterpay" or "Clearpay" depending on locale. + +### PaymentsUI +* [Fixed] Fixed an issue where STPPaymentCardTextField wouldn't call its delegate `paymentCardTextFieldDidChange` method when the preferred card network changed. + + +## 23.29.0 2024-08-05 +### PaymentSheet +* [Fixed] Fixed a scroll issue with native 3DS2 authentication screen when the keyboard appears. +* [Added] When a card is saved (ie you're using a PaymentIntent + setup_future_usage or SetupIntent), legal disclaimer text now appears below the form indicating the card can be charged for future payments. +* [Fixed] iOS 18 Compatibility with removing multiple saved payment methods +* [Fixed] Fixed an issue where the keyboard could focus on a hidden phone number field. +* [Added] Support for Sunbit (Private Beta) with PaymentIntents. +* [Added] Support for Billie (Private Beta) with PaymentIntents. +* [Fixed] Fixed an issue where saved payment method UI wouldn't respect `PaymentSheet.Configuration.style` when selected. +* [Added] Support for Satispay (Private Beta) with PaymentIntents. + +### Payments +* [Added] Support for Sunbit (Private Beta) bindings. +* [Added] Support for Billie (Private Beta) bindings. +* [Added] Support for Satispay (Private Beta) bindings. + +## 23.28.1 2024-07-16 +### Payments +* [Fixed] Improved reliability when paying or setting up with Cash App Pay. +* [Fixed] Pass stripeAccount context when presenting PayWithLinkWebController for connected accounts + +## 23.28.0 2024-07-08 + +### Payments +* [Fixed] An issue where the correct card brand was not being displayed for card brand choice in STPPaymentOptionsViewController and STPPaymentContext. +* [Added] Adds coupon support to STPApplePayContext with a new `didChangeCouponCode` delegate method (h/t @JoeyLeeMEA). +* [Fixed] Fixed an issue where successful TWINT payments were sometimes incorrectly considered 'canceled'. + +## PaymentSheet +* [Fixed] Fixed an issue where certain cobranded cards showed a generic card icon instead of using the other card brand. +* [Fixed] Fixed an issue where amounts with currency=IDR were displayed as-is, instead of dropping the last two digits. +* [Fixed] Fixed an issue where some payment method images in the horizontal scrollview could briefly flash. + +## 23.27.6 2024-06-25 +### All +* [Fixed] Improved reliability when paying with Swish. + +## 23.27.5 2024-06-20 +### PaymentSheet +* [Fixed] An issue that was preventing users from completing checkout with SetupIntents and PaymentIntents using `setup_future_usage` for the following payment method types: Amazon Pay, Cash App Pay, PayPal, and Revolut Pay. + +## 23.27.4 2024-06-18 +### PaymentSheet +* [Fixed] Fixed an issue where when displaying an LPM with no input fields, the sheet would take up the entire height of the screen. + +## 23.27.3 2024-06-14 +### PaymentSheet +* [Fixed] Fixed an issue where changing the country of a phone number would not update the UI when the phone number's validity changed. +* [Changed] The "save this card" checkbox is now unchecked by default. To change this behavior, set your PaymentSheet.Configuration.savePaymentMethodOptInBehavior to `.requiresOptOut`. +* [Fixed] Fixed an issue where PaymentSheet would not present in the iOS 18 beta when using SwiftUI. +* [Fixed] Fixed an issue in PaymentSheet.FlowController that could lead to the CVC recollection form being shown on presentPaymentOptions() + +### CustomerSheet +* [Fixed] Fixed an issue where CustomerSheet would not present in the iOS 18 beta when using SwiftUI. + +### Payments +* [Added] Updated support for MobilePay bindings. +* [Changed] Some Payment Methods (including Klarna and PayPal) may now authenticate using ASWebAuthenticationSession, enabling these payment methods to share session storage across apps. +* [Fixed] Fixed printing spurious STPAssertionFailure warnings. + +## 23.27.2 2024-05-06 +### CardScan +* [Changed] ScannedCard to allow access for expiryMonth, expiryYear and name. + +### PaymentSheet +* [Added] Support for Multibanco with PaymentIntents. +* [Fixed] Fixed an issue where STPPaymentHandler sometimes reported errors using `unexpectedErrorCode` instead of a more specific error when customers fail a next action. +* [Changed] PaymentSheet displays Apple Pay as a button when there are saved payment methods and Link isn't available instead of within the list of saved payment methods. +* [Fixed] Expiration dates more than 50 years in the past (e.g. `95`) are now blocked. + +### Payments +* [Added] Support for Multibanco bindings. +* [Fixed] Expiration dates more than 50 years in the past (e.g. `95`) are now blocked. + +## 23.27.1 2024-04-22 +### Payments +* [Fixed] An issue where the PrivacyInfo.xcprivacy was not bundled with StripePayments when installing with Cocoapods. + +### Apple Pay +* [Changed] Apple Pay additionalEnabledApplePayNetworks are now in front of the supported network list. + +### PaymentsUI +* [Added] Added support for `onBehalfOf` to STPPaymentCardTextField and STPCardFormView. This parameter may be required when setting a connected account as the merchant of record for a payment. For more information, see the [Connect docs](https://docs.stripe.com/connect/charges#on_behalf_of). + +## 23.27.0 2024-04-08 +### Payments +* [Added] Support for Alma bindings. +* [Fixed] STPBankAccountCollector errors now use "STPBankAccountCollectorErrorDomain" instead of "STPPaymentHandlerErrorDomain". + +### All +* [Fixed] Fixed an issue with generating App Privacy reports. + +## 23.26.0 2024-03-25 +### PaymentSheet +* [Fixed] When confirming a SetupIntent with Link, "Set up" will be shown as the confirm button text instead of "Pay". + +### CustomerSheet +* [Fixed] Fixed an issue dismissing the sheet when Link is the default payment method. + +### Financial Connections +* [Fixed] Improved the UX of an edge case in Financial Connections authentication flow. + +### All +* Added a [Privacy Manifest](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files). + +## 23.25.1 2024-03-18 +### All +* Xcode 14 is [no longer supported by Apple](https://developer.apple.com/news/upcoming-requirements/). Please upgrade to Xcode 15 or later. + +### PaymentSheet +* [Fixed] A bug where `PaymentSheet.FlowController` was not respecting `PaymentSheet.Configuration.primaryButtonLabel`. +* [Added] Support for Klarna with SetupIntents and PaymentIntents with `setup_future_usage`. + +### Financial Connections +* [Changed] Updated the design of Financial Connections authentication flow. + +## 23.25.0 2024-03-11 +### CustomerSheet +* [Added] Added `paymentMethodTypes` in `CustomerAdapter` to control what payment methods are displayed. + +### PaymentSheet +* [Fixed] The rotating [card brand view](https://docs.stripe.com/co-badged-cards-compliance) is now shown when card brand choice is enabled if the card number is empty. + +## 23.24.1 2024-03-05 +### PaymentSheet +* [Fixed] Fixed an assertionFailure that happens when using FlowController and switching between saved payment methods + +## 23.24.0 2024-03-04 +### PaymentSheet +* [Added] Added support for [Link](https://docs.stripe.com/payments/link/mobile-payment-element-link) in PaymentSheet. Enabling Link in your [payment method settings](https://dashboard.stripe.com/settings/payment_methods) will enable Link in PaymentSheet. To choose different Link availability settings on web and mobile, use a custom [payment method configuration](https://docs.stripe.com/payments/multiple-payment-method-configs). +* [Fixed] Fixed an issue where some 3DS2 payments may fail to complete successfully. + +### Payments +* [Added] Support for Amazon Pay bindings. + +## 23.23.0 2024-02-26 +### PaymentSheet +* [Added] Added support for [payment method configurations](https://docs.stripe.com/payments/multiple-payment-method-configs) when using the deferred intent integration path. + +### CustomerSheet +* [Fixed] Fixed a bug where if an exception is thrown in detachPaymentMethod(), the payment method was removed in the UI [#3309](https://github.com/stripe/stripe-ios/pull/3309) + +## 23.22.0 2024-02-12 +### PaymentSheet +* [Changed] The separator text under the Apple Pay button from "Or pay with a card" to "Or use a card" when using a SetupIntent. +* [Fixed] Fixed a bug where deleting the last saved payment method in PaymentSheet wouldn't automatically transition to the "Add a payment method" screen. +* [Added] Support for CVC recollection in PaymentSheet and PaymentSheet.FlowController (client-side confirmation) + +* [Changed] Make STPPinManagementService still usable from Swift. + +## 23.21.2 2024-02-05 +### Payments +* [Changed] We now auto append `mandate_data` when using Klarna with a SetupIntent. If you are interested in using Klarna with SetupIntents you sign up for the beta [here](https://stripe.com/docs/payments/klarna/accept-a-payment). + +## 23.21.1 2024-01-22 +### Payments +* [Changed] Increased the maximum number of status update retries when waiting for an intent to update to a terminal state. This impacts Cash App Pay and 3DS2. + +## 23.21.0 2024-01-16 +### PaymentSheet +* [Fixed] Fixed a few design issues on visionOS. +* [Added] Added billing details and type properties to [`PaymentSheet.FlowController.PaymentOptionDisplayData`](https://stripe.dev/stripe-ios/stripepaymentsheet/documentation/stripepaymentsheet/paymentsheet/flowcontroller/paymentoptiondisplaydata). + +## 23.20.0 2023-12-18 +### PaymentSheet +* [Added] Support for [card brand choice](https://stripe.com/docs/card-brand-choice). To set default preferred networks, use the new configuration option `PaymentSheet.Configuration.preferredNetworks`. +* [Fixed] Fixed visionOS support in Swift Package Manager and Cocoapods. + +### CustomerSheet +* [Added] Support for [card brand choice](https://stripe.com/docs/card-brand-choice). To set default preferred networks, use the new configuration option `PaymentSheet.Configuration.preferredNetworks`. + +### PaymentsUI +* [Added] Adds support for [card brand choice](https://stripe.com/docs/card-brand-choice) to STPPaymentCardTextField and STPCardFormView. To set a default preferred network for these UI elements, use the new `preferredNetworks` parameter. + +* [Changed] Mark STPPinManagementService deprecated & suggest alternative. + +## 23.19.0 2023-12-11 +### Apple Pay +* [Fixed] STPApplePayContext initializer returns nil in more cases where the request is invalid. +* [Fixed] STPApplePayContext now allows Apple Pay when the customer doesn’t have saved cards but can set them up in the Apple Pay sheet (iOS 15+). + +### PaymentSheet +* [Fixed] PaymentSheet sets newly saved payment methods as the default so that they're pre-selected the next time the customer pays. +* [Added] PaymentSheet now supports external payment methods. See https://stripe.com/docs/payments/external-payment-methods?platform=ios + +### CustomerSheet +* [Added] Saved SEPA payment methods are now displayed to the customer for reuse, similar to saved cards. + + +## 23.18.3 2023-11-28 +### PaymentSheet +* [Fixed] Visual bug where re-presenting PaymentSheet wouldn't show a spinner while it reloads. +* [Added] If PaymentSheet fails to load a deferred intent configuration, we fall back to displaying cards (or the intent configuration payment method types) instead of failing. +* [Fixed] Fixed an issue where PaymentSheet wouldn't accept valid Mexican phone numbers. +* [Added] The ability to customize the success colors of the primary button with `PaymentSheetAppearance.primaryButton.successBackgroundColor` and `PaymentSheetAppearance.primaryButton.successTextColor`. + +## 23.18.2 2023-11-06 +### CustomerSheet +* [Fixed] CustomerSheet no longer displays saved cards that originated from Apple Pay or Google Pay. + +## 23.18.1 2023-10-30 +### PaymentSheet +* [Fixed] Added a public initializer for `PaymentSheet.BillingDetails`. +* [Fixed] Fixed some payment method icons not updating to use the latest assets. +* [Fixed] PaymentSheet no longer displays saved cards that originated from Apple Pay or Google Pay. + +### PaymentsUI +* [Fixed] Fixed an issue with `STPPaymentCardTextField` where the `paymentCardTextFieldDidEndEditing` delegate method was not called. + +### PaymentSheet +* [Fixed] Fixed some payment method icons not updating to use the latest assets. + +## 23.18.0 2023-10-23 +### PaymentSheet +* [Added] Saved SEPA payment methods are now displayed to the customer for reuse, similar to saved cards. + +### PaymentsUI +* [Fixed] Fixed an issue where the unknown card icon would sometimes pick up the view's tint color. + +## 23.17.2 2023-10-16 +### PaymentsUI +* [Fixed] An issue with `STPPaymentCardTextField`, where the card params were not updated after deleting an empty sub field. +* [Fixed] Switched to Asset Catalogs and updated to the latest card brand logos. + +### Payments +* [Added] Support for MobilePay bindings. + +## 23.17.1 2023-10-09 +### PaymentSheet +* [Fixed] Fixed an issue when advancing from the country dropdown that prevented user's' from typing in their postal code. ([#2936](https://github.com/stripe/stripe-ios/issues/2936)) + +### PaymentsUI +* [Fixed] An issue with `STPPaymentCardTextField`, where the `paymentCardTextFieldDidChange` delegate method wasn't being called after deleting an empty sub field. + +## 23.17.0 2023-10-02 +### PaymentSheet +* [Fixed] Fixed an issue with selecting from lists on macOS Catalyst. Note that only macOS 11 or later is supported: We do not recommend releasing a Catalyst app targeting macOS 10.15. +* [Fixed] Fixed an issue with scanning card expiration dates. +* [Fixed] Fixed an issue where billing address collection configuration was not passed to Apple Pay. +* [Added] Support for Swish with PaymentIntents. +* [Added] Support for Bacs Direct Debit with PaymentIntents. + +### Basic Integration +* [Fixed] Fixed an issue with scanning card expiration dates. + +### Payments +* [Fixed] Fixed an issue where amounts in Serbian Dinar were displayed incorrectly. +* [Fixed] Fixed an issue where the SDK could hang in macOS Catalyst. +* [Added] Support for Swish bindings. + +## 23.16.0 2023-09-18 +### Payments +* [Added] Properties of STPConnectAccountParams are now mutable. +* [Fixed] Fixed STPConnectAccountCompanyParams.address being force unwrapped. It's now optional. +* [Added] Support for RevolutPay bindings + +### PaymentSheet +* [Added] Support for Alipay with PaymentIntents. +* [Added] Support for Cash App Pay with SetupIntents and PaymentIntents with `setup_future_usage`. +* [Added] Support for AU BECS Debit with SetupIntents. +* [Added] Support for OXXO with PaymentIntents. +* [Added] Support for Konbini with PaymentIntents. +* [Added] Support for PayNow with PaymentIntents. +* [Added] Support for PromptPay with PaymentIntents. +* [Added] Support for Boleto with PaymentIntents and SetupIntets. +* [Added] Support for External Payment Method as an invite-only private beta. +* [Added] Support for RevolutPay with SetupIntents and PaymentIntents with setup_future_usage (private beta). Note: PaymentSheet doesn't display this as a saved payment method yet. +* [Added] Support for Alma (Private Beta) with PaymentIntents. + +## 23.15.0 2023-08-28 +### PaymentSheet +* [Added] Support for AmazonPay (private beta), BLIK, and FPX with PaymentIntents. +* [Fixed] A bug where payment amounts were not displayed correctly for LAK currency. + +### StripeApplePay +* Fixed a compile-time issue with using StripeApplePay in an App Extension. ([#2853](https://github.com/stripe/stripe-ios/issues/2853)) + +### CustomerSheet +* [Added] `CustomerSheet`(https://stripe.com/docs/elements/customer-sheet?platform=ios) API, a prebuilt UI component that lets your customers manage their saved payment methods. + +## 23.14.0 2023-08-21 +### All +* Improved redirect UX when using Cash App Pay. + +### PaymentSheet +* [Added] Support for GrabPay with PaymentIntents. + +### Payments +* [Added] You can now create an STPConnectAccountParams without specifying a business type. + +### Basic Integration +* [Added] Adds `applePayLaterAvailability` to `STPPaymentContext`, a property that mirrors `PKPaymentRequest.applePayLaterAvailability`. This is useful if you need to disable Apple Pay Later. Note: iOS 17+. + + +## 23.13.0 2023-08-07 +### All +* [Fixed] Fixed compatibility with Xcode 15 beta 3. visionOS is now supported in iPadOS compatibility mode. +### PaymentSheet +* [Added] Enable bancontact and sofort for SetupIntents and PaymentIntents with setup_future_usage. Note: PaymentSheet doesn't display saved SEPA Debit payment methods yet. +### CustomerSheet +* [Added] `us_bank_account` PaymentMethod is now available in CustomerSheet + +## 23.12.0 2023-07-31 +### PaymentSheet +* [Added] Enable SEPA Debit and iDEAL for SetupIntents and PaymentIntents with setup_future_usage. Note: PaymentSheet doesn't display saved SEPA Debit payment methods yet. +* [Added] Add removeSavedPaymentMethodMessage to PaymentSheet.Configuration and CustomerSheet.Configuration. + +### Identity +* [Added] Supports [phone verification](https://stripe.com/docs/identity/phone) in Identity mobile SDK. + + +## 23.11.2 2023-07-24 +### PaymentSheet +* [Fixed] Update stp_icon_add@3x.png to 8bit color depth (Thanks @jszumski) + +### CustomerSheet +* [Fixed] Ability to removing payment method immediately after adding it. +* [Fixed] Re-init addPaymentMethodViewController after adding payment method to allow for adding another payment method + +## 23.11.1 2023-07-18 +### PaymentSheet +* [Fixed] Fixed various bugs in Link private beta. + +## 23.11.0 2023-07-17 +### CustomerSheet +* [Changed] Breaking interface change for `CustomerSheetResult`. `CustomerSheetResult.canceled` now has a nullable associated value signifying that there is no selected payment method. Please use both `.canceled(StripeOptionSelection?)` and `.selected(PaymentOptionSelection?)` to update your UI to show the latest selected payment method. + +## 23.10.0 2023-07-10 +### Payments +* [Fixed] A bug where `mandate_data` was not being properly attached to PayPal SetupIntent's. +### PaymentSheet +* [Added] You can now collect payment details before creating a PaymentIntent or SetupIntent. See [our docs](https://stripe.com/docs/payments/accept-a-payment-deferred) for more info. This integration also allows you to [confirm the Intent on the server](https://stripe.com/docs/payments/finalize-payments-on-the-server). + +## 23.9.4 2023-07-05 +### PaymentSheet +* [Added] US bank accounts are now supported when initializing with an IntentConfiguration. + +## 23.9.3 2023-06-26 +### PaymentSheet +* [Fixed] Affirm no longer requires shipping details. + +### CustomerSheet +* [Added] Added `billingDetailsCollectionConfiguration` to configure how you want to collect billing details (private beta). + +## 23.9.2 2023-06-20 +### Payments +* [Fixed] Fixed a bug causing Cash App Pay SetupIntents to incorrectly state they were canceled when they succeeded. + +### AddressElement +* [Fixed] A bug that was causing `addressViewControllerDidFinish` to return a non-nil `AddressDetails` when the user cancels out of the AddressElement when default values are provided. +* [Fixed] A bug that prevented the auto complete view from being presented when the AddressElement was created with default values. + +## 23.9.1 2023-06-12 +### PaymentSheet +* [Fixed] Fixed validating the IntentConfiguration matches the PaymentIntent/SetupIntent when it was already confirmed on the server. Note: server-side confirmation is in private beta. +### CustomerSheet +* [Fixed] Fixed bug with removing multiple saved payment methods + +## 23.9.0 2023-05-30 +### PaymentSheet +* [Changed] The private beta API for https://stripe.com/docs/payments/finalize-payments-on-the-server has changed: + * If you use `IntentConfiguration(..., confirmHandler:)`, the confirm handler now has an additional `shouldSavePaymentMethod: Bool` parameter that you should ignore. + * If you use `IntentConfiguration(..., confirmHandlerForServerSideConfirmation:)`, use `IntentConfiguration(..., confirmHandler:)` instead. Additionally, the confirm handler's first parameter is now an `STPPaymentMethod` object instead of a String id. Use `paymentMethod.stripeId` to get its id and send it to your server. +* [Fixed] Fixed PKR currency formatting. + +### CustomerSheet +* [Added] [CustomerSheet](https://stripe.com/docs/elements/customer-sheet?platform=ios) is now available (private beta) + +## 23.8.0 2023-05-08 +### Identity +* [Added] Added test mode M1 for the SDK. + +## 23.7.1 2023-05-02 +### Payments +* [Fixed] STPPaymentHandler.handleNextAction allows payment methods that are delayed or require further customer action like like SEPA Debit or OXXO. + +## 23.7.0 2023-04-24 +### PaymentSheet +* [Fixed] Fixed disabled text color, using a lower opacity version of the original color instead of the previous `.tertiaryLabel`. + +### Identity +* [Added] Added test mode for the SDK. + +## 23.6.2 2023-04-20 + +### Payments +* [Fixed] Fixed UnionPay cards appearing as invalid in some cases. + +### PaymentSheet +* [Fixed] Fixed a bug that prevents users from using SEPA Debit w/ PaymentIntents or SetupIntents and Paypal in PaymentIntent+setup_future_usage or SetupIntent. + +## 23.6.1 2023-04-17 +### All +* Xcode 13 is [no longer supported by Apple](https://developer.apple.com/news/upcoming-requirements/). Please upgrade to Xcode 14.1 or later. +### PaymentSheet +* [Fixed] Visual bug of the delete icon when deleting saved payment methods reported in [#2461](https://github.com/stripe/stripe-ios/issues/2461). + +## 23.6.0 2023-03-27 +### PaymentSheet +* [Added] Added `billingDetailsCollectionConfiguration` to configure how you want to collect billing details. See the docs [here](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=payment-sheet#billing-details-collection). + +## 23.5.1 2023-03-20 +### Payments +* [Fixed] Fixed amounts in COP being formatted incorrectly. +* [Fixed] Fixed BLIK payment bindings not handling next actions correctly. +* [Changed] Removed usage of `UIDevice.currentDevice.name`. + +### Identity +* [Added] Added a retake photo button on selfie scanning screen. + +## 23.5.0 2023-03-13 +### Payments +* [Added] API bindings support for Cash App Pay. See the docs [here](https://stripe.com/docs/payments/cash-app-pay/accept-a-payment?platform=mobile). +* [Added] Added `STPCardValidator.possibleBrands(forCard:completion:)`, which returns the list of available networks for a card. + +### PaymentSheet +* [Added] Support for Cash App Pay in PaymentSheet. + +## 23.4.2 2023-03-06 +### Identity +* [Added] ID/Address verification. + +## 23.4.1 2023-02-27 +### PaymentSheet +* [Added] Debug logging to help identify why specific payment methods are not showing up in PaymentSheet. + +### Basic Integration +* [Fixed] Race condition reported in #2302 + +## 23.4.0 2023-02-21 +### PaymentSheet +* [Added] Adds support for setting up PayPal using a SetupIntent or a PaymentIntent w/ setup_future_usage=off_session. Note: PayPal is in beta. + +## 23.3.4 2023-02-13 +### Financial Connections +* [Changed] Polished Financial Connections UI. + +## 23.3.3 2023-01-30 +### Payments +* [Changed] Updated image asset for AFFIN bank. + +### Financial Connections +* [Fixed] Double encoding of GET parameters. + +## 23.3.2 2023-01-09 +* [Changed] Using [Tuist](https://tuist.io) to generate Xcode projects. From now on, only release versions of the SDK will include Xcode project files, in case you want to build a non release revision from source, you can follow [these instructions](https://docs.tuist.io/tutorial/get-started) to generate the project files. For Carthage users, this also means that you will only be able to depend on release versions. + +### PaymentSheet +* [Added] `PaymentSheetError` now conforms to `CustomDebugStringConvertible` and has a more useful description when no payment method types are available. +* [Changed] Customers can now re-enter the autocomplete flow of `AddressViewController` by tapping an icon in the line 1 text field. + +## 23.3.1 2022-12-12 +* [Fixed] Fixed a bug where 3 decimal place currencies were not being formatted properly. + +### PaymentSheet +* [Fixed] Fixed an issue that caused animations of the card logos in the Card input field to glitch. +* [Fixed] Fixed a layout issue in the "Save my info" checkbox. + +### CardScan +* [Fixed] Fixed UX model loading from the wrong bundle. [#2078](https://github.com/stripe/stripe-ios/issues/2078) (Thanks [nickm01](https://github.com/nickm01)) + +## 23.3.0 2022-12-05 +### PaymentSheet +* [Added] Added logos of accepted card brands on Card input field. +* [Fixed] Fixed erroneously displaying the card scan button when card scanning is not available. + +### Financial Connections +* [Changed] FinancialConnectionsSheet methods now require to be called from non-extensions. +* [Changed] BankAccountToken.bankAccount was changed to an optional. + +## 23.2.0 2022-11-14 +### PaymentSheet +* [Added] Added `AddressViewController`, a customizable view controller that collects local and international addresses for your customers. See https://stripe.com/docs/elements/address-element?platform=ios. +* [Added] Added `PaymentSheet.Configuration.allowsPaymentMethodsRequiringShippingAddress`. Previously, to allow payment methods that require a shipping address (e.g. Afterpay and Affirm) in PaymentSheet, you attached a shipping address to the PaymentIntent before initializing PaymentSheet. Now, you can instead set this property to `true` and set `PaymentSheet.Configuration.shippingDetails` to a closure that returns your customers' shipping address. The shipping address will be attached to the PaymentIntent when the customer completes the checkout. +* [Fixed] Fixed user facing error messages for card related errors. +* [Fixed] Fixed `setup_future_usage` value being set when there's no customer. + +## 23.1.1 2022-11-07 +### Payments +* [Fixed] Fixed an issue with linking the StripePayments SDK in certain configurations. + +## 23.1.0 2022-10-31 +### CardScan +* [Added] Added a README.md for the `CardScanSheet` integration. + +### PaymentSheet +* [Added] Added parameters to customize the primary button and Apple Pay button labels. They can be found under `PaymentSheet.Configuration.primaryButtonLabel` and `PaymentSheet.ApplePayConfiguration.buttonType` respectively. + +## 23.0.0 2022-10-24 +### Payments +* [Changed] Reduced the size of the SDK by splitting the `Stripe` module into `StripePaymentSheet`, `StripePayments`, and `StripePaymentsUI`. Some manual changes may be required. Migration instructions are available at [https://stripe.com/docs/mobile/ios/sdk-23-migration](https://stripe.com/docs/mobile/ios/sdk-23-migration). + +|Module|Description|Compressed|Uncompressed| +|------|-----------|----------|------------| +|StripePaymentSheet|Stripe's [prebuilt payment UI](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=payment-sheet).|2.7MB|6.3MB| +|Stripe|Contains all the below frameworks, plus [Issuing](https://stripe.com/docs/issuing/cards/digital-wallets?platform=iOS) and [Basic Integration](/docs/mobile/ios/basic).|2.3MB|5.1MB| +|StripeApplePay|[Apple Pay support](/docs/apple-pay), including `STPApplePayContext`.|0.4MB|1.0MB| +|StripePayments|Bindings for the Stripe Payments API.|1.0MB|2.6MB| +|StripePaymentsUI|Bindings for the Stripe Payments API, [STPPaymentCardTextField](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=custom), STPCardFormView, and other UI elements.|1.7MB|3.9MB| + +* [Changed] The minimum iOS version is now 13.0. If you'd like to deploy for iOS 12.0, please use Stripe SDK 22.8.4. +* [Changed] STPPaymentCardTextField's `cardParams` parameter has been deprecated in favor of `paymentMethodParams`, making it easier to include the postal code from the card field. If you need to access the `STPPaymentMethodCardParams`, use `.paymentMethodParams.card`. + +### PaymentSheet +* [Fixed] Fixed a validation issue where cards expiring at the end of the current month were incorrectly treated as expired. +* [Fixed] Fixed a visual bug in iOS 16 where advancing between text fields would momentarily dismiss the keyboard. + +## 22.8.4 2022-10-12 +### PaymentSheet +* [Fixed] Use `.formSheet` modal presentation in Mac Catalyst. [#2023](https://github.com/stripe/stripe-ios/issues/2023) (Thanks [sergiocampama](https://github.com/sergiocampama)!) + +## 22.8.3 2022-10-03 +### CardScan +* [Fixed] [Garbled privacy link text in Card Scan UI](https://github.com/stripe/stripe-ios/issues/2015) + +## 22.8.2 2022-09-19 +### Identity +* [Changed] Support uploading single side documents. +* [Fixed] Fixed Xcode 14 support. +### Financial Connections +* [Fixed] Fixes an issue of returning canceled result from FinancialConnections if user taps cancel on the manual entry success screen. +### CardScan +* [Added] Added a new parameter to CardScanSheet.present() to specify if the presentation should be done animated or not. Defaults to true. +* [Changed] Changed card scan ML model loading to be async. +* [Changed] Changed minimum deployment target for card scan to iOS 13. + +## 22.8.1 2022-09-12 +### PaymentSheet +* [Fixed] Fixed potential crash when using Link in Mac Catalyst. +* [Fixed] Fixed Right-to-Left (RTL) layout issues. + +### Apple Pay +* [Fixed] Fixed an issue where `applePayContext:willCompleteWithResult:authorizationResult:handler:` may not be called in Objective-C implementations of `STPApplePayContextDelegate`. + +## 22.8.0 2022-09-06 +### PaymentSheet +* [Changed] Renamed `PaymentSheet.reset()` to `PaymentSheet.resetCustomer()`. See `MIGRATING.md` for more info. +* [Added] You can now set closures in `PaymentSheet.ApplePayConfiguration.customHandlers` to configure the PKPaymentRequest and PKPaymentAuthorizationResult during a transaction. This enables you to build support for [Merchant Tokens](https://developer.apple.com/documentation/passkit/pkpaymentrequest/3916053-recurringpaymentrequest) and [Order Tracking](https://developer.apple.com/documentation/passkit/pkpaymentorderdetails) in iOS 16. + +### Apple Pay +* [Added] You can now implement the `applePayContext(_:willCompleteWithResult:handler:)` function in your `ApplePayContextDelegate` to configure the PKPaymentAuthorizationResult during a transaction. This enables you to build support for [Order Tracking](https://developer.apple.com/documentation/passkit/pkpaymentorderdetails) in iOS 16. + +## 22.7.1 2022-08-31 +* [Fixed] Fixed Mac Catalyst support in Xcode 14. [#2001](https://github.com/stripe/stripe-ios/issues/2001) + +### PaymentSheet +* [Fixed] PaymentSheet now uses configuration.apiClient for Apple Pay instead of always using STPAPIClient.shared. +* [Fixed] Fixed a layout issue with PaymentSheet in landscape. + +## 22.7.0 2022-08-15 +### PaymentSheet +* [Fixed] Fixed a layout issue on iPad. +* [Changed] Improved Link support in custom flow (`PaymentSheet.FlowController`). + +## 22.6.0 2022-07-05 +### PaymentSheet +* [Added] PaymentSheet now supports Link payment method. +* [Changed] Change behavior of Afterpay/Clearpay: Charge in 3 for GB, FR, and ES + +### STPCardFormView +* [Changed] Postal code is no longer collected for billing addresses in Japan. + +### Identity +* [Added] The ability to capture Selfie images in the native component flow. +* [Fixed] Fixed an issue where the welcome and confirmation screens were not correctly decoding non-ascii characters. +* [Fixed] Fixed an issue where, if a manually uploaded document could not be decoded on the server, there was no way to select a new image to upload. +* [Fixed] Fixed an issue where the IdentityVerificationSheet completion block was called early when manually uploading a document image instead of using auto-capture. + +## 22.5.1 2022-06-21 +* [Fixed] Fixed an issue with `STPPaymentHandler` where returning an app redirect could cause a crash. + +## 22.5.0 2022-06-13 +### PaymentSheet +* [Added] You can now use `PaymentSheet.ApplePayConfiguration.paymentSummaryItems` to directly configure the payment summary items displayed in the Apple Pay sheet. This is useful for recurring payments. + +## 22.4.0 2022-05-23 +### PaymentSheet +* [Added] The ability to customize the appearance of the PaymentSheet using `PaymentSheet.Appearance`. +* [Added] Support for collecting payments from customers in 54 additional countries within PaymentSheet. Most of these countries are located in Africa and the Middle East. +* [Added] `affirm` and `AUBECSDebit` payment methods are now available in PaymentSheet + +## 22.3.2 2022-05-18 +### CardScan +* [Added] Added privacy text to the CardImageVerification Sheet UI + +## 22.3.1 2022-05-16 +* [Fixed] Fixed an issue where ApplePayContext failed to parse an API response if the funding source was unknown. +* [Fixed] Fixed an issue where PaymentIntent confirmation could fail when the user closes the challenge window immediately after successfully completing a challenge + +### Identity +* [Fixed] Fixed an issue where the verification flow would get stuck in a document upload loop when verifying with a passport and uploading an image manually. + +## 22.3.0 2022-05-03 + +### PaymentSheet +* [Added] `us_bank_account` PaymentMethod is now available in payment sheet + +## 22.2.0 2022-04-25 + +### Connections +* [Changed] `StripeConnections` SDK has been renamed to `StripeFinancialConnections`. See `MIGRATING.md` for more info. + +### PaymentSheet +* [Fixed] Fixed an issue where `source_cancel` API requests were being made for non-3DS payment method types. +* [Fixed] Fixed an issue where certain error messages were not being localized. +* [Added] `us_bank_account` PaymentMethod is now available in PaymentSheet. + +### Identity +* [Fixed] Minor UI fixes when using `IdentityVerificationSheet` with native components +* [Changed] Improvements to native component `IdentityVerificationSheet` document detection + +## 22.1.1 2022-04-11 + +### Identity +* [Fixed] Fixes VerificationClientSecret (Thanks [Masataka-n](https://github.com/Masataka-n)!) + +## 22.1.0 2022-04-04 +* [Changed] Localization improvements. +### Identity +* [Added] `IdentityVerificationSheet` can now be used with native iOS components. + +## 22.0.0 2022-03-28 +* [Changed] The minimum iOS version is now 12.0. If you'd like to deploy for iOS 11.0, please use Stripe SDK 21.12.0. +* [Added] `us_bank_account` PaymentMethod is now available for ACH Direct Debit payments, including APIs to collect customer bank information (requires `StripeConnections`) and verify microdeposits. +* [Added] `StripeConnections` SDK can be optionally included to support ACH Direct Debit payments. + +### PaymentSheet +* [Changed] PaymentSheet now uses light and dark mode agnostic icons for payment method types. +* [Changed] Link payment method (private beta) UX improvements. + +### Identity +* [Changed] `IdentityVerificationSheet` now has an availability requirement of iOS 14.3 on its initializer instead of the `present` method. + +## 21.13.0 2022-03-15 +* [Changed] Binary framework distribution now requires Xcode 13. Carthage users using Xcode 12 need to add the `--no-use-binaries` flag. + +### PaymentSheet +* [Fixed] Fixed potential crash when using PaymentSheet custom flow with SwiftUI. +* [Fixed] Fixed being unable to cancel native 3DS2 in PaymentSheet. +* [Fixed] The payment method icons will now use the correct colors when PaymentSheet is configured with `alwaysLight` or `alwaysDark`. +* [Fixed] A race condition when setting the `primaryButtonColor` on `PaymentSheet.Configuration`. +* [Added] PaymentSheet now supports Link (private beta). + +### CardScan +* [Added] The `CardImageVerificationSheet` initializer can now take an additional `Configuration` object. + +## 21.12.0 2022-02-14 +* [Added] We now offer a 1MB Apple Pay SDK module intended for use in an App Clip. Visit [our App Clips docs](https://stripe.com/docs/apple-pay#app-clips) for details. +* `Stripe` now requires `StripeApplePay`. See `MIGRATING.md` for more info. +* [Added] Added a convenience initializer to create an STPCardParams from an STPPaymentMethodParams. + +### PaymentSheet +* [Changed] The "save this card" checkbox in PaymentSheet is now unchecked by default in non-US countries. +* [Fixed] Fixes issue that could cause symbol name collisions when using Objective-C +* [Fixed] Fixes potential crash when using PaymentSheet with SwiftUI + +## 21.11.1 2022-01-10 +* Fixes a build warning in SPM caused by an invalid Package.swift file. + +## 21.11.0 2022-01-04 +* [Changed] The maximum `identity_document` file upload size has been increased, improving the quality of compressed images. See https://stripe.com/docs/file-upload +* [Fixed] The maximum `dispute_evidence` file upload size has been decreased to match server requirements, preventing the server from rejecting uploads that exceeded 5MB. See https://stripe.com/docs/file-upload +* [Added] PaymentSheet now supports Afterpay / Clearpay, EPS, Giropay, Klarna, Paypal (private beta), and P24. + +## 21.10.0 2021-12-14 +* Added API bindings for Klarna +* `StripeIdentity` now requires `StripeCameraCore`. See `MIGRATING.md` for more info. +* Releasing `StripeCardScan` Beta iOS SDK +* Fixes a bug where the text field would cause a crash when typing a space (U+0020) followed by pressing the backspace key on iPad. [#1907](https://github.com/stripe/stripe-ios/issues/1907) (Thanks [buhikon](https://github.com/buhikon)!) + +## 21.9.1 2021-12-02 +* Fixes a build warning caused by a duplicate NSURLComponents+Stripe.swift file. + +## 21.9.0 2021-10-18 +### PaymentSheet +This release adds several new features to PaymentSheet, our drop-in UI integration: + +#### More supported payment methods +The list of supported payment methods depends on your integration. +If you’re using a PaymentIntent, we support: +- Card +- SEPA Debit, bancontact, iDEAL, sofort + +If you’re using a PaymentIntent with `setup_future_usage` or a SetupIntent, we support: +- Card +- Apple/GooglePay + +Note: To enable SEPA Debit and sofort, set `PaymentSheet.configuration.allowsDelayedPaymentMethods` to `true` on the client. +These payment methods can't guarantee you will receive funds from your customer at the end of the checkout because they take time to settle. Don't enable these if your business requires immediate payment (e.g., an on-demand service). See https://stripe.com/payments/payment-methods-guide + +#### Pre-fill billing details +PaymentSheet collects billing details like name and email for certain payment methods. Pre-fill these fields to save customers time by setting `PaymentSheet.Configuration.defaultBillingDetails`. + +#### Save payment methods on payment +> This is currently only available for cards + Apple/Google Pay. + +PaymentSheet supports PaymentIntents with `setup_future_usage` set. This property tells us to save the payment method for future use (e.g., taking initial payment of a recurring subscription). +When set, PaymentSheet hides the 'Save this card for future use' checkbox and always saves. + +#### SetupIntent support +> This is currently only available for cards + Apple/Google Pay. + +Initialize PaymentSheet with a SetupIntent to set up cards for future use without charging. + +#### Smart payment method ordering +When a customer is adding a new payment method, PaymentSheet uses information like the customers region to show the most relevant payment methods first. + +#### Other changes +* Postal code collection for cards is now limited to US, CA, UK +* Fixed SwiftUI memory leaks [Issue #1881](https://github.com/stripe/stripe-ios/issues/1881) +* Added "hint" for error messages +* Adds many new localizations. The SDK now localizes in the following languages: bg-BG,ca-ES,cs-CZ,da,de,el-GR,en-GB,es-419,es,et-EE,fi,fil,fr-CA,fr,hr,hu,id,it,ja,ko,lt-LT,lv-LV,ms-MY,mt,nb,nl,nn-NO,pl-PL,pt-BR,pt-PT,ro-RO,ru,sk-SK,sl-SI,sv,tk,tr,vi,zh-Hans,zh-Hant,zh-HK +* `Stripe` and `StripeIdentity` now require `StripeUICore`. See `MIGRATING.md` for more info. + +## 21.8.1 2021-08-10 +* Fixes an issue with image loading when using Swift Package Manager. +* Temporarily disabled WeChat Pay support in PaymentMethods. +* The `Stripe` module now requires `StripeCore`. See `MIGRATING.md` for more info. + +## 21.8.0 2021-08-04 +* Fixes broken card scanning links. (Thanks [ricsantos](https://github.com/ricsantos)) +* Fixes accessibilityLabel for postal code field. (Thanks [romanilchyshyndepop](https://github.com/romanilchyshyndepop)) +* Improves compile time by 30% [#1846](https://github.com/stripe/stripe-ios/pull/1846) (Thanks [JonathanDowning](https://github.com/JonathanDowning)!) +* Releasing `StripeIdentity` iOS SDK for use with [Stripe Identity](https://stripe.com/identity). + +## 21.7.0 2021-07-07 +* Fixes an issue with `additionaDocument` field typo [#1833](https://github.com/stripe/stripe-ios/issues/1833) +* Adds support for WeChat Pay to PaymentMethods +* Weak-links SwiftUI [#1828](https://github.com/stripe/stripe-ios/issues/1828) +* Adds 3DS2 support for Cartes Bancaires +* Fixes an issue with camera rotation during card scanning on iPad +* Fixes an issue where PaymentSheet could cause conflicts when included in an app that also includes PanModal [#1818](https://github.com/stripe/stripe-ios/issues/1818) +* Fixes an issue with building on Xcode 13 [#1822](https://github.com/stripe/stripe-ios/issues/1822) +* Fixes an issue where overriding STPPaymentCardTextField's `brandImage()` func had no effect [#1827](https://github.com/stripe/stripe-ios/issues/1827) +* Fixes documentation typo. (Thanks [iAugux](https://github.com/iAugux)) + +## 21.6.0 2021-05-27 +* Adds `STPCardFormView`, a UI component that collects card details +* Adds 'STPRadarSession'. Note this requires additional Stripe permissions to use. + +## 21.5.1 2021-05-07 +* Fixes the `PaymentSheet` API not being public. +* Fixes an issue with missing headers. (Thanks [jctrouble](https://github.com/jctrouble)!) + +## 21.5.0 2021-05-06 +* Adds the `PaymentSheet`(https://stripe.dev/stripe-ios/docs/Classes/PaymentSheet.html) API, a prebuilt payment UI. +* Fixes Mac Catalyst support in Xcode 12.5 [#1797](https://github.com/stripe/stripe-ios/issues/1797) +* Fixes `STPPaymentCardTextField` not being open [#1768](https://github.com/stripe/stripe-ios/issues/1797) + +## 21.4.0 2021-04-08 +* Fixed warnings in Xcode 12.5. [#1772](https://github.com/stripe/stripe-ios/issues/1772) +* Fixes a layout issue when confirming payments in SwiftUI. [#1761](https://github.com/stripe/stripe-ios/issues/1761) (Thanks [mvarie](https://github.com/mvarie)!) +* Fixes a potential race condition when finalizing 3DS2 confirmations. +* Fixes an issue where a 3DS2 transaction could result in an incorrect error message when the card number is incorrect. [#1778](https://github.com/stripe/stripe-ios/issues/1778) +* Fixes an issue where `STPPaymentHandler.shared().handleNextAction` sometimes didn't return a `handleActionError`. [#1769](https://github.com/stripe/stripe-ios/issues/1769) +* Fixes a layout issue when confirming payments in SwiftUI. [#1761](https://github.com/stripe/stripe-ios/issues/1761) (Thanks [mvarie](https://github.com/mvarie)!) +* Fixes an issue with opening URLs on Mac Catalyst +* Fixes an issue where OXXO next action is mistaken for a cancel in STPPaymentHandler +* SetupIntents for iDEAL, Bancontact, EPS, and Sofort will now send the required mandate information. +* Adds support for BLIK. +* Adds `decline_code` information to STPError. [#1755](https://github.com/stripe/stripe-ios/issues/1755) +* Adds support for SetupIntents to STPApplePayContext +* Allows STPPaymentCardTextField to be subclassed. [#1768](https://github.com/stripe/stripe-ios/issues/1768) + +## 21.3.1 2021-03-25 +* Adds support for Maestro in Apple Pay on iOS 12 or later. + +## 21.3.0 2021-02-18 +* Adds support for SwiftUI in custom integration using the `STPPaymentCardTextField.Representable` View and the `.paymentConfirmationSheet()` ViewModifier. See `IntegrationTester` for usage examples. +* Removes the UIViewController requirement from STPApplePayContext, allowing it to be used in SwiftUI. +* Fixes an issue where `STPPaymentOptionsViewController` could fail to register a card. [#1758](https://github.com/stripe/stripe-ios/issues/1758) +* Fixes an issue where some UnionPay test cards were marked as invalid. [#1759](https://github.com/stripe/stripe-ios/issues/1759) +* Updates tests to run on Carthage 0.37 with .xcframeworks. + + +## 21.2.1 2021-01-29 +* Fixed an issue where a payment card text field could resize incorrectly on smaller devices or with certain languages. [#1600](https://github.com/stripe/stripe-ios/issues/1600) +* Fixed an issue where the SDK could always return English strings in certain situations. [#1677](https://github.com/stripe/stripe-ios/pull/1677) (Thanks [glaures-ioki](https://github.com/glaures-ioki)!) +* Fixed an issue where an STPTheme had no effect on the navigation bar. [#1753](https://github.com/stripe/stripe-ios/pull/1753) (Thanks [@rbenna](https://github.com/rbenna)!) +* Fixed handling of nil region codes. [#1752](https://github.com/stripe/stripe-ios/issues/1752) +* Fixed an issue preventing card scanning from being disabled. [#1751](https://github.com/stripe/stripe-ios/issues/1751) +* Fixed an issue with enabling card scanning in an app with a localized Info.plist.[#1745](https://github.com/stripe/stripe-ios/issues/1745) +* Added a missing additionalDocument parameter to STPConnectAccountIndividualVerification. +* Added support for Afterpay/Clearpay. + +## 21.2.0 2021-01-06 +* Stripe3DS2 is now open source software under the MIT License. +* Fixed various issues with bundling Stripe3DS2 in Cocoapods and Swift Package Manager. All binary dependencies have been removed. +* Fixed an infinite loop during layout on small screen sizes. [#1731](https://github.com/stripe/stripe-ios/issues/1731) +* Fixed issues with missing image assets when using Cocoapods. [#1655](https://github.com/stripe/stripe-ios/issues/1655) [#1722](https://github.com/stripe/stripe-ios/issues/1722) +* Fixed an issue which resulted in unnecessary queries to the BIN information service. +* Adds the ability to `attach` and `detach` PaymentMethod IDs to/from a CustomerContext. [#1729](https://github.com/stripe/stripe-ios/issues/1729) +* Adds support for NetBanking. + +## 21.1.0 2020-12-07 +* Fixes a crash during manual confirmation of a 3DS2 payment. [#1725](https://github.com/stripe/stripe-ios/issues/1725) +* Fixes an issue that could cause some image assets to be missing in certain configurations. [#1722](https://github.com/stripe/stripe-ios/issues/1722) +* Fixes an issue with confirming Alipay transactions. +* Re-exposes `cardNumber` parameter in `STPPaymentCardTextField`. +* Adds support for UPI. + +## 21.0.1 2020-11-19 +* Fixes an issue with some initializers not being exposed publicly following the [conversion to Swift](https://stripe.com/docs/mobile/ios/sdk-21-migration). +* Updates GrabPay integration to support synchronous updates. + +## 21.0.0 2020-11-18 +* The SDK is now written in Swift, and some manual changes are required. Migration instructions are available at [https://stripe.com/docs/mobile/ios/sdk-21-migration](https://stripe.com/docs/mobile/ios/sdk-21-migration). +* Adds full support for Apple silicon. +* Xcode 12.2 is now required. + +## 20.1.1 2020-10-23 +* Fixes an issue when using Cocoapods 1.10 and Xcode 12. [#1683](https://github.com/stripe/stripe-ios/pull/1683) +* Fixes a warning when using Swift Package Manager. [#1675](https://github.com/stripe/stripe-ios/pull/1675) + +## 20.1.0 2020-10-15 +* Adds support for OXXO. [#1592](https://github.com/stripe/stripe-ios/pull/1592) +* Applies a workaround for various bugs in Swift Package Manager. [#1671](https://github.com/stripe/stripe-ios/pull/1671) Please see [#1673](https://github.com/stripe/stripe-ios/issues/1673) for additional notes when using Xcode 12.0. +* Card scanning now works when the device's orientation is unknown. [#1659](https://github.com/stripe/stripe-ios/issues/1659) +* The expiration date field's Simplified Chinese localization has been corrected. (Thanks [cythb](https://github.com/cythb)!) [#1654](https://github.com/stripe/stripe-ios/pull/1654) + +## 20.0.0 2020-09-14 +* [Card scanning](https://github.com/stripe/stripe-ios#card-scanning) is now built into STPAddCardViewController. Card.io support has been removed. [#1629](https://github.com/stripe/stripe-ios/pull/1629) +* Shrunk the SDK from 1.3MB when compressed & thinned to 0.7MB, allowing for easier App Clips integration. [#1643](https://github.com/stripe/stripe-ios/pull/1643) +* Swift Package Manager, Apple Silicon, and Catalyst are now fully supported on Xcode 12. [#1644](https://github.com/stripe/stripe-ios/pull/1644) +* Adds support for 19-digit cards. [#1608](https://github.com/stripe/stripe-ios/pull/1608) +* Adds GrabPay and Sofort as PaymentMethod. [#1627](https://github.com/stripe/stripe-ios/pull/1627) +* Drops support for iOS 10. [#1643](https://github.com/stripe/stripe-ios/pull/1643) + +## 19.4.0 2020-08-13 +* `pkPaymentErrorForStripeError` no longer returns PKPaymentUnknownErrors. Instead, it returns the original NSError back, resulting in dismissal of the Apple Pay sheet. This means ApplePayContext dismisses the Apple Pay sheet for all errors that aren't specifically PKPaymentError types. +* `metadata` fields are no longer populated on retrieved Stripe API objects and must be fetched on your server using your secret key. If this is causing issues with your deployed app versions please reach out to [Stripe Support](https://support.stripe.com/?contact=true). These fields have been marked as deprecated and will be removed in a future SDK version. + +## 19.3.0 2020-05-28 +* Adds giropay PaymentMethod bindings [#1569](https://github.com/stripe/stripe-ios/pull/1569) +* Adds Przelewy24 (P24) PaymentMethod bindings [#1556](https://github.com/stripe/stripe-ios/pull/1556) +* Adds Bancontact PaymentMethod bindings [#1565](https://github.com/stripe/stripe-ios/pull/1565) +* Adds EPS PaymentMethod bindings [#1578](https://github.com/stripe/stripe-ios/pull/1578) +* Replaces es-AR localization with es-419 for full Latin American Spanish support and updates multiple localizations [#1549](https://github.com/stripe/stripe-ios/pull/1549) [#1570](https://github.com/stripe/stripe-ios/pull/1570) +* Fixes missing custom number placeholder in `STPPaymentCardTextField` [#1576](https://github.com/stripe/stripe-ios/pull/1576) +* Adds tabbing on external keyboard support to `STPAUBECSFormView` and correctly types it as a `UIView` instead of `UIControl` [#1580](https://github.com/stripe/stripe-ios/pull/1580) + +## 19.2.0 2020-05-01 +* Adds ability to attach shipping details when confirming PaymentIntents [#1558](https://github.com/stripe/stripe-ios/pull/1558) +* `STPApplePayContext` now provides shipping details in the `applePayContext:didCreatePaymentMethod:paymentInformation:completion:` delegate method and automatically attaches shipping details to PaymentIntents (unless manual confirmation)[#1561](https://github.com/stripe/stripe-ios/pull/1561) +* Adds support for the BECS Direct Debit payment method for Stripe users in Australia [#1547](https://github.com/stripe/stripe-ios/pull/1547) + +## 19.1.1 2020-04-28 +* Add advancedFraudSignalsEnabled property [#1560](https://github.com/stripe/stripe-ios/pull/1560) + +## 19.1.0 2020-04-15 +* Relaxes need for dob for full name connect account (`STPConnectAccountIndividualParams`). [#1539](https://github.com/stripe/stripe-ios/pull/1539) +* Adds Chinese (Traditional) and Chinese (Hong Kong) localizations [#1536](https://github.com/stripe/stripe-ios/pull/1536) +* Adds `STPApplePayContext`, a helper class for Apple Pay. [#1499](https://github.com/stripe/stripe-ios/pull/1499) +* Improves accessibility [#1513](https://github.com/stripe/stripe-ios/pull/1513), [#1504](https://github.com/stripe/stripe-ios/pull/1504) +* Adds support for the Bacs Direct Debit payment method [#1487](https://github.com/stripe/stripe-ios/pull/1487) +* Adds support for 16 digit Diners Club cards [#1498](https://github.com/stripe/stripe-ios/pull/1498) + +## 19.0.1 2020-03-24 +* Fixes an issue building with Xcode 11.4 [#1526](https://github.com/stripe/stripe-ios/pull/1526) + +## 19.0.0 2020-02-12 +* Deprecates the `STPAPIClient` `initWithConfiguration:` method. Set the `configuration` property on the `STPAPIClient` instance instead. [#1474](https://github.com/stripe/stripe-ios/pull/1474) +* Deprecates `publishableKey` and `stripeAccount` properties of `STPPaymentConfiguration`. See [MIGRATING.md](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) for more details. [#1474](https://github.com/stripe/stripe-ios/pull/1474) +* Adds explicit STPAPIClient properties on all SDK components that make API requests. These default to `[STPAPIClient sharedClient]`. This is a breaking change for some users of `stripeAccount`. See [MIGRATING.md](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) for more details. [#1469](https://github.com/stripe/stripe-ios/pull/1469) +* The user's postal code is now collected by default in countries that support postal codes. We always recommend collecting a postal code to increase card acceptance rates and reduce fraud. See [MIGRATING.md](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) for more details. [#1479](https://github.com/stripe/stripe-ios/pull/1479) + +## 18.4.0 2020-01-15 +* Adds support for Klarna Pay on Sources API [#1444](https://github.com/stripe/stripe-ios/pull/1444) +* Compresses images using `pngcrush` to reduce SDK size [#1471](https://github.com/stripe/stripe-ios/pull/1471) +* Adds support for CVC recollection in PaymentIntent confirm [#1473](https://github.com/stripe/stripe-ios/pull/1473) +* Fixes a race condition when setting `defaultPaymentMethod` on `STPPaymentOptionsViewController` [#1476](https://github.com/stripe/stripe-ios/pull/1476) + +## 18.3.0 2019-12-3 +* STPAddCardViewControllerDelegate methods previously removed in v16.0.0 are now marked as deprecated, to help migrating users [#1439](https://github.com/stripe/stripe-ios/pull/1439) +* Fixes an issue where canceling 3DS authentication could leave PaymentIntents in an inaccurate `requires_action` state [#1443](https://github.com/stripe/stripe-ios/pull/1443) +* Fixes text color for large titles [#1446](https://github.com/stripe/stripe-ios/pull/1446) +* Re-adds support for pre-selecting the last selected payment method in STPPaymentContext and STPPaymentOptionsViewController. [#1445](https://github.com/stripe/stripe-ios/pull/1445) +* Fix crash when adding/removing postal code cells [#1450](https://github.com/stripe/stripe-ios/pull/1450) + +## 18.2.0 2019-10-31 +* Adds support for creating tokens with the last 4 digits of an SSN [#1432](https://github.com/stripe/stripe-ios/pull/1432) +* Renames Standard Integration to Basic Integration + +## 18.1.0 2019-10-29 +* Adds localizations for English (Great Britain), Korean, Russian, and Turkish [#1373](https://github.com/stripe/stripe-ios/pull/1373) +* Adds support for SEPA Debit as a PaymentMethod [#1415](https://github.com/stripe/stripe-ios/pull/1415) +* Adds support for custom SEPA Debit Mandate params with PaymentMethod [#1420](https://github.com/stripe/stripe-ios/pull/1420) +* Improves postal code UI for users with mismatched regions [#1302](https://github.com/stripe/stripe-ios/issues/1302) +* Fixes a potential crash when presenting the add card view controller [#1426](https://github.com/stripe/stripe-ios/issues/1426) +* Adds offline status checking to FPX payment flows [#1422](https://github.com/stripe/stripe-ios/pull/1422) +* Adds support for push provisions for Issuing users [#1396](https://github.com/stripe/stripe-ios/pull/1396) + +## 18.0.0 2019-10-04 +* Adds support for building on macOS 10.15 with Catalyst. Use the .xcframework file attached to the release in GitHub. Cocoapods support is coming soon. [#1364](https://github.com/stripe/stripe-ios/issues/1364) +* Errors from the Payment Intents API are now localized by default. See [MIGRATING.md](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) for details. +* Adds support for FPX in Standard Integration. [#1390](https://github.com/stripe/stripe-ios/pull/1390) +* Simplified Apple Pay integration when using 3DS2. [#1386](https://github.com/stripe/stripe-ios/pull/1386) +* Improved autocomplete behavior for some STPPaymentHandler blocks. [#1403](https://github.com/stripe/stripe-ios/pull/1403) +* Fixed spurious `keyboardWillAppear` messages triggered by STPPaymentTextCard. [#1393](https://github.com/stripe/stripe-ios/pull/1393) +* Fixed an issue with non-numeric placeholders in STPPaymentTextCard. [#1394](https://github.com/stripe/stripe-ios/pull/1394) +* Dropped support for iOS 9. Please continue to use 17.0.2 if you need to support iOS 9. + +## 17.0.2 2019-09-24 +* Fixes an error that could prevent a 3D Secure 2 challenge dialog from appearing in certain situations. +* Improved VoiceOver support. [#1384](https://github.com/stripe/stripe-ios/pull/1384) +* Updated Apple Pay and Mastercard branding. [#1374](https://github.com/stripe/stripe-ios/pull/1374) +* Updated the Standard Integration example app to use automatic confirmation. [#1363](https://github.com/stripe/stripe-ios/pull/1363) +* Added support for collecting email addresses and phone numbers from Apple Pay. [#1372](https://github.com/stripe/stripe-ios/pull/1372) +* Introduced support for FPX payments. (Invite-only Beta) [#1375](https://github.com/stripe/stripe-ios/pull/1375) + +## 17.0.1 2019-09-09 +* Cancellation during the 3DS2 flow will no longer cause an unexpected error. [#1353](https://github.com/stripe/stripe-ios/pull/1353) +* Large Title UIViewControllers will no longer have a transparent background in iOS 13. [#1362](https://github.com/stripe/stripe-ios/pull/1362) +* Adds an `availableCountries` option to STPPaymentConfiguration, allowing one to limit the list of countries in the address entry view. [#1327](https://github.com/stripe/stripe-ios/pull/1327) +* Fixes a crash when using card.io. [#1357](https://github.com/stripe/stripe-ios/pull/1357) +* Fixes an issue with birthdates when creating a Connect account. [#1361](https://github.com/stripe/stripe-ios/pull/1361) +* Updates example code to Swift 5. [#1354](https://github.com/stripe/stripe-ios/pull/1354) +* The default value of `[STPTheme translucentNavigationBar]` is now `YES`. [#1367](https://github.com/stripe/stripe-ios/pull/1367) + +## 17.0.0 2019-09-04 +* Adds support for iOS 13, including Dark Mode and minor bug fixes. [#1307](https://github.com/stripe/stripe-ios/pull/1307) +* Updates API version from 2015-10-12 to 2019-05-16 [#1254](https://github.com/stripe/stripe-ios/pull/1254) + * Adds `STPSourceRedirectStatusNotRequired` to `STPSourceRedirectStatus`. Previously, optional redirects were marked as `STPSourceRedirectStatusSucceeded`. + * Adds `STPSourceCard3DSecureStatusRecommended` to `STPSourceCard3DSecureStatus`. + * Removes `STPLegalEntityParams`. Initialize an `STPConnectAccountParams` with an `individual` or `company` dictionary instead. See https://stripe.com/docs/api/tokens/create_account#create_account_token-account +* Changes the `STPPaymentContextDelegate paymentContext:didCreatePaymentResult:completion:` completion block type to `STPPaymentStatusBlock`, to let you inform the context that the user canceled. +* Adds initial support for WeChat Pay. [#1326](https://github.com/stripe/stripe-ios/pull/1326) +* The user's billing address will now be included when creating a PaymentIntent from an Apple Pay token. [#1334](https://github.com/stripe/stripe-ios/pull/1334) + + +## 16.0.7 2019-08-23 +* Fixes STPThreeDSUICustomization not initializing defaults correctly. [#1303](https://github.com/stripe/stripe-ios/pull/1303) +* Fixes STPPaymentHandler treating post-authentication errors as authentication errors [#1291](https://github.com/stripe/stripe-ios/pull/1291) +* Removes preferredStatusBarStyle from STPThreeDSUICustomization, see STPThreeDSNavigationBarCustomization.barStyle instead [#1308](https://github.com/stripe/stripe-ios/pull/1308) + +## 16.0.6 2019-08-13 +* Adds a method to STPAuthenticationContext allowing you to configure the SFSafariViewController presented for web-based authentication. +* Adds STPAddress initializer that takes STPPaymentMethodBillingDetails. [#1278](https://github.com/stripe/stripe-ios/pull/1278) +* Adds convenience method to populate STPUserInformation with STPPaymentMethodBillingDetails. [#1278](https://github.com/stripe/stripe-ios/pull/1278) +* STPShippingAddressViewController prefills billing address for PaymentMethods too now, not just Card. [#1278](https://github.com/stripe/stripe-ios/pull/1278) +* Update libStripe3DS2.a to avoid a conflict with Firebase. [#1293](https://github.com/stripe/stripe-ios/issues/1293) + +## 16.0.5 2019-08-09 +* Fixed an compatibility issue when building with certain Cocoapods configurations. [#1288](https://github.com/stripe/stripe-ios/issues/1288) + +## 16.0.4 2019-08-08 +* Improved compatibility with other OpenSSL-using libraries. [#1265](https://github.com/stripe/stripe-ios/issues/1265) +* Fixed compatibility with Xcode 10.1. [#1273](https://github.com/stripe/stripe-ios/issues/1273) +* Fixed an issue where STPPaymentContext could be left in a bad state when cancelled. [#1284](https://github.com/stripe/stripe-ios/pull/1284) + +## 16.0.3 2019-08-01 +* Changes to code obfuscation, resolving an issue with App Store review [#1269](https://github.com/stripe/stripe-ios/pull/1269) +* Adds Apple Pay support to STPPaymentHandler [#1264](https://github.com/stripe/stripe-ios/pull/1264) + +## 16.0.2 2019-07-29 +* Adds API to let users set a default payment option for Standard Integration [#1252](https://github.com/stripe/stripe-ios/pull/1252) +* Removes querying the Advertising Identifier (IDFA). +* Adds customizable UIStatusBarStyle to STDSUICustomization. + +## 16.0.1 2019-07-25 +* Migrates Stripe3DS2.framework to libStripe3DS2.a, resolving an issue with App Store validation. [#1246](https://github.com/stripe/stripe-ios/pull/1246) +* Fixes a crash in STPPaymentHandler. [#1244](https://github.com/stripe/stripe-ios/pull/1244) + +## 16.0.0 2019-07-18 +* Migrates STPPaymentCardTextField.cardParams property type from STPCardParams to STPPaymentMethodCardParams +* STPAddCardViewController: + * Migrates addCardViewController:didCreateSource:completion: and addCardViewController:didCreateToken:completion: to addCardViewController:didCreatePaymentMethod:completion + * Removes managedAccountCurrency property - there’s no equivalent parameter necessary for PaymentMethods. +* STPPaymentOptionViewController now shows, adds, removes PaymentMethods instead of Source/Tokens. +* STPCustomerContext, STPBackendAPIAdapter: + * Removes selectDefaultCustomerSource:completion: - Users must explicitly select their Payment Method of choice. + * Migrates detachSourceFromCustomer:completion:, attachSourceToCustomer:completion to detachPaymentMethodFromCustomer:completion:, attachPaymentMethodToCustomer:completion: + * Adds listPaymentMethodsForCustomerWithCompletion: - the Customer object doesn’t contain attached Payment Methods; you must fetch it from the Payment Methods API. +* STPPaymentContext now uses the new Payment Method APIs listed above instead of Source/Token, and returns the reworked STPPaymentResult containing a PaymentMethod. +* Migrates STPPaymentResult.source to paymentMethod of type STPPaymentMethod +* Deprecates STPPaymentIntentAction* types, replaced by STPIntentAction*. [#1208](https://github.com/stripe/stripe-ios/pull/1208) + * Deprecates `STPPaymentIntentAction`, replaced by `STPIntentAction` + * Deprecates `STPPaymentIntentActionType`, replaced by `STPIntentActionType` + * Deprecates `STPPaymentIntentActionRedirectToURL`, replaced by `STPIntentActionTypeRedirectToURL` +* Adds support for SetupIntents. See https://stripe.com/docs/payments/cards/saving-cards#saving-card-without-payment +* Adds support for 3DS2 authentication. See https://stripe.com/docs/mobile/ios/authentication + +## 15.0.1 2019-04-16 +* Adds configurable support for JCB (Apple Pay). [#1158](https://github.com/stripe/stripe-ios/pull/1158) +* Updates sample apps to use `PaymentIntents` and `PaymentMethods` where available. [#1159](https://github.com/stripe/stripe-ios/pull/1159) +* Changes `STPPaymentMethodCardParams` `expMonth` and `expYear` property types to `NSNumber *` to fix a bug using Apple Pay. [#1161](https://github.com/stripe/stripe-ios/pull/1161) + +## 15.0.0 2019-3-19 +* Renames all former references to 'PaymentMethod' to 'PaymentOption'. See [MIGRATING.md](/MIGRATING.md) for more details. [#1139](https://github.com/stripe/stripe-ios/pull/1139) + * Renames `STPPaymentMethod` to `STPPaymentOption` + * Renames `STPPaymentMethodType` to `STPPaymentOptionType` + * Renames `STPApplePaymentMethod` to `STPApplePayPaymentOption` + * Renames `STPPaymentMethodTuple` to `STPPaymentOptionTuple` + * Renames `STPPaymentMethodsViewController` to `STPPaymentOptionsViewController` + * Renames all properties, methods, comments referencing 'PaymentMethod' to 'PaymentOption' +* Rewrites `STPaymentMethod` and `STPPaymentMethodType` to match the [Stripe API](https://stripe.com/docs/api/payment_methods/object). [#1140](https://github.com/stripe/stripe-ios/pull/1140). +* Adds `[STPAPI createPaymentMethodWithParams:completion:]`, which creates a PaymentMethod. [#1141](https://github.com/stripe/stripe-ios/pull/1141) +* Adds `paymentMethodParams` and `paymentMethodId` to `STPPaymentIntentParams`. You can now confirm a PaymentIntent with a PaymentMethod. [#1142](https://github.com/stripe/stripe-ios/pull/1142) +* Adds `paymentMethodTypes` to `STPPaymentIntent`. +* Deprecates several Source-named properties, based on changes to the [Stripe API](https://stripe.com/docs/upgrades#2019-02-11). [#1146](https://github.com/stripe/stripe-ios/pull/1146) + * Deprecates `STPPaymentIntentParams.saveSourceToCustomer`, replaced by `savePaymentMethod` + * Deprecates `STPPaymentIntentsStatusRequiresSource`, replaced by `STPPaymentIntentsStatusRequiresPaymentMethod` + * Deprecates `STPPaymentIntentsStatusRequiresSourceAction`, replaced by `STPPaymentIntentsStatusRequiresAction` + * Deprecates `STPPaymentIntentSourceAction`, replaced by `STPPaymentIntentAction` + * Deprecates `STPPaymentSourceActionAuthorizeWithURL`, replaced by `STPPaymentActionRedirectToURL` + * Deprecates `STPPaymentIntent.nextSourceAction`, replaced by `nextAction` +* Added new localizations for the following languages [#1050](https://github.com/stripe/stripe-ios/pull/1050) + * Danish + * Spanish (Argentina/Latin America) + * French (Canada) + * Norwegian + * Portuguese (Brazil) + * Portuguese (Portugal) + * Swedish +* Deprecates `STPEphemeralKeyProvider`, replaced by `STPCustomerEphemeralKeyProvider`. We now allow for ephemeral keys that are not customer [#1131](https://github.com/stripe/stripe-ios/pull/1131) +* Adds CVC image for Amex cards [#1046](https://github.com/stripe/stripe-ios/pull/1046) +* Fixed `STPPaymentCardTextField.nextFirstResponderField` to never return nil [#1059](https://github.com/stripe/stripe-ios/pull/1059) +* Improves return key functionality for `STPPaymentCardTextField`, `STPAddCardViewController` [#1059](https://github.com/stripe/stripe-ios/pull/1059) +* Add postal code support for Saudi Arabia [#1127](https://github.com/stripe/stripe-ios/pull/1127) +* CVC field updates validity if card number/brand change [#1128](https://github.com/stripe/stripe-ios/pull/1128) + +## 14.0.0 2018-11-14 +* Changes `STPPaymentCardTextField`, which now copies the `cardParams` property. See [MIGRATING.md](/MIGRATING.md) for more details. [#1031](https://github.com/stripe/stripe-ios/pull/1031) +* Renames `STPPaymentIntentParams.returnUrl` to `STPPaymentIntentParams.returnURL`. [#1037](https://github.com/stripe/stripe-ios/pull/1037) +* Removes `STPPaymentIntent.returnUrl` and adds `STPPaymentIntent.nextSourceAction`, based on changes to the [Stripe API](https://stripe.com/docs/upgrades#2018-11-08). [#1038](https://github.com/stripe/stripe-ios/pull/1038) +* Adds `STPVerificationParams.document_back` property. [#1017](https://github.com/stripe/stripe-ios/pull/1017) +* Fixes bug in `STPPaymentMethodsViewController` where selected payment method changes back if it wasn't dismissed in the `didFinish` delegate method. [#1020](https://github.com/stripe/stripe-ios/pull/1020) + +## 13.2.0 2018-08-14 +* Adds `STPPaymentMethod` protocol implementation for `STPSource`. You can now call `image`/`templatedImage`/`label` on a source. [#976](https://github.com/stripe/stripe-ios/pull/976) +* Fixes crash in `STPAddCardViewController` with some prefilled billing addresses [#1004](https://github.com/stripe/stripe-ios/pull/1004) +* Fixes `STPPaymentCardTextField` layout issues on small screens [#1009](https://github.com/stripe/stripe-ios/pull/1009) +* Fixes hidden text fields in `STPPaymentCardTextField` from being read by VoiceOver [#1012](https://github.com/stripe/stripe-ios/pull/1012) +* Updates example app to add client-side metadata `charge_request_id` to requests to `example-ios-backend` [#1008](https://github.com/stripe/stripe-ios/pull/1008) + +## 13.1.0 2018-07-13 +* Adds `STPPaymentIntent` to support PaymentIntents. [#985](https://github.com/stripe/stripe-ios/pull/985), [#986](https://github.com/stripe/stripe-ios/pull/986), [#987](https://github.com/stripe/stripe-ios/pull/987), [#988](https://github.com/stripe/stripe-ios/pull/988) +* Reduce `NSURLSession` memory footprint. [#969](https://github.com/stripe/stripe-ios/pull/969) +* Fixes invalid JSON error when deleting `Card` from a `Customer`. [#992](https://github.com/stripe/stripe-ios/pull/992) + +## 13.0.3 2018-06-11 +* Fixes payment method label overlapping the checkmark, for Amex on small devices [#952](https://github.com/stripe/stripe-ios/pull/952) +* Adds EPS and Multibanco support to `STPSourceParams` [#961](https://github.com/stripe/stripe-ios/pull/961) +* Adds `STPBillingAddressFieldsName` option to `STPBillingAddressFields` [#964](https://github.com/stripe/stripe-ios/pull/964) +* Fixes crash in `STPColorUtils.perceivedBrightnessForColor` [#954](https://github.com/stripe/stripe-ios/pull/954) +* Applies recommended project changes for Xcode 9.4 [#963](https://github.com/stripe/stripe-ios/pull/963) +* Fixes `[Stripe handleStripeURLCallbackWithURL:url]` incorrectly returning `NO` [#962](https://github.com/stripe/stripe-ios/pull/962) + +## 13.0.2 2018-05-24 +* Makes iDEAL `name` parameter optional, also accepts empty string as `nil` [#940](https://github.com/stripe/stripe-ios/pull/940) +* Adjusts scroll view content offset behavior when focusing on a text field [#943](https://github.com/stripe/stripe-ios/pull/943) + +## 13.0.1 2018-05-17 +* Fixes an issue in `STPRedirectContext` causing some redirecting sources to fail in live mode due to prematurely dismissing the `SFSafariViewController` during the initial redirects. [#937](https://github.com/stripe/stripe-ios/pull/937) + +## 13.0.0 2018-04-26 +* Removes Bitcoin source support. See MIGRATING.md. [#931](https://github.com/stripe/stripe-ios/pull/931) +* Adds Masterpass support to `STPSourceParams` [#928](https://github.com/stripe/stripe-ios/pull/928) +* Adds community submitted Norwegian (nb) translation. Thank @Nailer! +* Fixes example app usage of localization files (they were not able to be tested in Finnish and Norwegian before) +* Silences STPAddress deprecation warnings we ignore to stay compatible with older iOS versions +* Fixes "Card IO" link in full SDK reference [#913](https://github.com/stripe/stripe-ios/pull/913) + +## 12.1.2 2018-03-16 +* Updated the "62..." credit card number BIN range to show a UnionPay icon + +## 12.1.1 2018-02-22 +* Fix issue with apple pay token creation in PaymentContext, introduced by 12.1.0. [#899](https://github.com/stripe/stripe-ios/pull/899) +* Now matches clang static analyzer settings with Cocoapods, so you won't see any more analyzer issues. [#897](https://github.com/stripe/stripe-ios/pull/897) + +## 12.1.0 2018-02-05 +* Adds `createCardSources` to `STPPaymentConfiguration`. If you enable this option, when your user adds a card in the SDK's UI, a card source will be created and attached to their Stripe Customer. If this option is disabled (the default), a card token is created. For more information on card sources, see https://stripe.com/docs/sources/cards + +## 12.0.1 2018-01-31 +* Adding Visa Checkout support to `STPSourceParams` [#889](https://github.com/stripe/stripe-ios/pull/889) + +## 12.0.0 2018-01-16 +* Minimum supported iOS version is now 9.0. + * If you need to support iOS 8, the last supported version is [11.5.0](https://github.com/stripe/stripe-ios/releases/tag/v11.5.0) +* Minimum supported Xcode version is now 9.0 +* `AddressBook` framework support has been removed. +* `STPRedirectContext` will no longer retain itself for the duration of the redirect, you must explicitly maintain a reference to it yourself. [#846](https://github.com/stripe/stripe-ios/pull/846) +* `STPPaymentConfiguration.requiredShippingAddress` now is a set of `STPContactField` objects instead of a `PKAddressField` bitmask. [#848](https://github.com/stripe/stripe-ios/pull/848) +* See MIGRATING.md for more information on any of the previously mentioned breaking API changes. +* Pre-built view controllers now layout properly on iPhone X in landscape orientation, respecting `safeAreaInsets`. [#854](https://github.com/stripe/stripe-ios/pull/854) +* Fixes a bug in `STPAddCardViewController` that prevented users in countries without postal codes from adding a card when `requiredBillingFields = .Zip`. [#853](https://github.com/stripe/stripe-ios/pull/853) +* Fixes a bug in `STPPaymentCardTextField`. When completely filled out, it ignored calls to `becomeFirstResponder`. [#855](https://github.com/stripe/stripe-ios/pull/855) +* `STPPaymentContext` now has a `largeTitleDisplayMode` property, which you can use to control the title display mode in the navigation bar of our pre-built view controllers. [#849](https://github.com/stripe/stripe-ios/pull/849) +* Fixes a bug where `STPPaymentContext`'s `retryLoading` method would not re-retrieve the customer object, even after calling `STPCustomerContext`'s `clearCachedCustomer` method. [#863](https://github.com/stripe/stripe-ios/pull/863) +* `STPPaymentContext`'s `retryLoading` method will now always attempt to retrieve a new customer object, regardless of whether a cached customer object is available. Previously, this method was only intended for recovery from a loading error; if a customer had already been retrieved, `retryLoading` would do nothing. [#863](https://github.com/stripe/stripe-ios/pull/863) +* `STPCustomerContext` has a new property: `includeApplePaySources`. It is turned off by default. [#864](https://github.com/stripe/stripe-ios/pull/864) +* Adds `UITextContentType` support. This turns on QuickType suggestions for the name, email, and address fields; and uses a better keyboard for Payment Card fields. [#870](https://github.com/stripe/stripe-ios/pull/870) +* Fixes a bug that prevented redirects to the 3D Secure authentication flow when it was optional. [#878](https://github.com/stripe/stripe-ios/pull/878) +* `STPPaymentConfiguration` now has a `stripeAccount` property, which can be used to make API requests on behalf of a Connected account. [#875](https://github.com/stripe/stripe-ios/pull/875) +* Adds `- [STPAPIClient createTokenWithConnectAccount:completion:]`, which creates Tokens for Connect Accounts: (optionally) accepting the Terms of Service, and sending information about the legal entity. [#876](https://github.com/stripe/stripe-ios/pull/876) +* Fixes an iOS 11 bug in `STPPaymentCardTextField` that blocked tapping on the number field while editing the expiration or CVC on narrow devices (4" screens). [#883](https://github.com/stripe/stripe-ios/pull/883) + +## 11.5.0 2017-11-09 +* Adds a new helper method to `STPSourceParams` for creating reusable Alipay sources. [#811](https://github.com/stripe/stripe-ios/pull/811) +* Silences spurious availability warnings when using Xcode9 [#823](https://github.com/stripe/stripe-ios/pull/823) +* Auto capitalizes currency code when using `paymentRequestWithMerchantIdentifier ` to improve compatibility with iOS 11 `PKPaymentAuthorizationViewController` [#829](https://github.com/stripe/stripe-ios/pull/829) +* Fixes a bug in `STPRedirectContext` which caused `SFSafariViewController`-based redirects to incorrectly dismiss when switching apps. [#833](https://github.com/stripe/stripe-ios/pull/833) +* Fixes a bug that incorrectly offered users the option to "Use Billing Address" on the shipping address screen when there was no existing billing address to fill in. [#834](https://github.com/stripe/stripe-ios/pull/834) + +## 11.4.0 2017-10-20 +* Restores `[STPCard brandFromString:]` method which was marked as deprecated in a recent version [#801](https://github.com/stripe/stripe-ios/pull/801) +* Adds `[STPBankAccount metadata]` and `[STPCard metadata]` read-only accessors and improves annotation for `[STPSource metadata]` [#808](https://github.com/stripe/stripe-ios/pull/808) +* Un-deprecates `STPBackendAPIAdapter` and all associated methods. [#813](https://github.com/stripe/stripe-ios/pull/813) +* The `STPBackendAPIAdapter` protocol now includes two optional methods, `detachSourceFromCustomer` and `updateCustomerWithShipping`. If you've implemented a class conforming to `STPBackendAPIAdapter`, you may add implementations of these methods to support deleting cards from a customer and saving shipping info to a customer. [#813](https://github.com/stripe/stripe-ios/pull/813) +* Adds the ability to set custom footers on view controllers managed by the SDK. [#792](https://github.com/stripe/stripe-ios/pull/792) +* `STPPaymentMethodsViewController` will now display saved card sources in addition to saved card tokens. [#810](https://github.com/stripe/stripe-ios/pull/810) +* Fixes a bug where certain requests would return a generic failed to parse response error instead of the actual API error. [#809](https://github.com/stripe/stripe-ios/pull/809) + +## 11.3.0 2017-09-13 +* Adds support for creating `STPSourceParams` for P24 source [#779](https://github.com/stripe/stripe-ios/pull/779) +* Adds support for native app-to-app Alipay redirects [#783](https://github.com/stripe/stripe-ios/pull/783) +* Fixes crash when `paymentContext.hostViewController` is set to a `UINavigationController` [#786](https://github.com/stripe/stripe-ios/pull/786) +* Improves support and compatibility with iOS 11 + * Explicitly disable code coverage generation for compatibility with Carthage in Xcode 9 [#795](https://github.com/stripe/stripe-ios/pull/795) + * Restore use of native "Back" buttons [#789](https://github.com/stripe/stripe-ios/pull/789) +* Changes and fixes methods on `STPCard`, `STPCardParams`, `STPBankAccount`, and `STPBankAccountParams` to bring card objects more in line with the rest of the API. See MIGRATING for further details. + * `STPCard` and `STPCardParams` [#760](https://github.com/stripe/stripe-ios/pull/760) + * `STPBankAccount` and `STPBankAccountParams` [#761](https://github.com/stripe/stripe-ios/pull/761) +* Adds nullability annotations to `STPPaymentMethod` protocol [#753](https://github.com/stripe/stripe-ios/pull/753) +* Improves the `[STPAPIResponseDecodable allResponseFields]` by removing all instances of `[NSNull null]` including ones that are nested. See MIGRATING.md. [#747](https://github.com/stripe/stripe-ios/pull/747) + +## 11.2.0 2017-07-27 +* Adds an option to allow users to delete payment methods from the `STPPaymentMethodsViewController`. Enabled by default but can disabled using the `canDeletePaymentMethods` property of `STPPaymentConfiguration`. + * Screenshots: https://user-images.githubusercontent.com/28276156/28131357-7a353474-66ee-11e7-846c-b38277d111fd.png +* Adds a postal code field to `STPPaymentCardTextField`, configurable with `postalCodeEntryEnabled` and `postalCodePlaceholder`. Disabled by default. +* `STPCustomer`'s `shippingAddress` property is now correctly annotated as nullable. +* Removed `STPCheckoutUnknownError`, `STPCheckoutTooManyAttemptsError`, and `STPCustomerContextMissingKeyProviderError`. These errors will no longer occur. + +## 11.1.0 2017-07-12 +* Adds stripeAccount property to `STPAPIClient`, set this to perform API requests on behalf of a connected account +* Fixes the `routingNumber` property of `STPBankAccount` so that it is populated when the information is available +* Adds iOS Objective-C Style Guide + +## 11.0.0 2017-06-27 +* We've greatly simplified the integration for `STPPaymentContext`. See MIGRATING.md. +* As part of this new integration, we've added a new class, `STPCustomerContext`, which will automatically prefetch your customer and cache it for a brief interval. We recommend initializing your `STPCustomerContext` before your user enters your checkout flow so their payment methods are loaded in advance. If in addition to using `STPPaymentContext`, you create a separate `STPPaymentMethodsViewController` to let your customer manage their payment methods outside of your checkout flow, you can use the same instance of `STPCustomerContext` for both. +* We've added a `shippingAddress` property to `STPUserInformation`, which you can use to pre-fill your user's shipping information. +* `STPPaymentContext` will now save your user's shipping information to their Stripe customer object. Shipping information will automatically be pre-filled from the customer object for subsequent checkouts. +* Fixes nullability annotation for `[STPFile stringFromPurpose:]`. See MIGRATING.md. +* Adds description implementations to all public models, for easier logging and debugging. +* The card autofill via SMS feature of `STPPaymentContext` has been removed. See MIGRATING.md. + +## 10.2.0 2017-06-19 +* We've added a `paymentCountry` property to `STPPaymentContext`. This affects the countryCode of Apple Pay payments, and defaults to "US". You should set this to the country your Stripe account is in. +* `paymentRequestWithMerchantIdentifier:` has been deprecated. See MIGRATING.md +* If the card.io framework is present in your app, `STPPaymentContext` and `STPAddCardViewController` will show a "scan card" button. +* `STPAddCardViewController` will now attempt to auto-fill the users city and state from their entered Zip code (United States only) +* Polling for source object updates is deprecated. Check https://stripe.com/docs for the latest best practices on how to integrate with the sources API using webhooks. +* Fixes a crash in `STPCustomerDeserializer` when both data and error are nil. +* `paymentMethodsViewController:didSelectPaymentMethod:` is now optional. +* Updates the example apps to use Alamofire. + +## 10.1.0 2017-05-05 +* Adds STPRedirectContext, a helper class for handling redirect sources. +* STPAPIClient now supports tokenizing a PII number and uploading images. +* Updates STPPaymentCardTextField's icons to match Elements on the web. When the card number is invalid, the field will now display an error icon. +* The alignment of the new brand icons has changed to match the new CVC and error icons. If you use these icons via `STPImageLibrary`, you may need to adjust your layout. +* STPPaymentCardTextField's isValid property is now KVO-observable. +* When creating STPSourceParams for a SEPA debit source, address fields are now optional. +* `STPPaymentMethodsViewControllerDelegate` now has a separate `paymentMethodsViewControllerDidCancel:` callback, differentiating from successful method selections. You should make sure to also dismiss the view controller in that callback +* Because collecting some basic data on tokenization helps us detect fraud, we've removed the ability to disable analytics collection using `[Stripe disableAnalytics]`. + +## 10.0.1 2017-03-16 +* Fixes a bug where card sources didn't include the card owner's name. +* Fixes an issue where STPPaymentMethodsViewController didn't reload after adding a new payment method. + +## 10.0.0 2017-03-06 +* Adds support for creating, retrieving, and polling Sources. You can enable any payment methods available to you in the Dashboard. + * https://stripe.com/docs/mobile/ios/sources + * https://dashboard.stripe.com/account/payments/settings +* Updates the Objective-C example app to include example integrations using several different payment methods. +* Updates `STPCustomer` to include `STPSource` objects in its `sources` array if a customer has attached sources. +* Removes methods deprecated in Version 6.0. +* Fixes property declarations missing strong/nullable identifiers. + +## 9.4.0 2017-02-03 +* Adds button to billing/shipping entry screens to fill address information from the other one. +* Fixes and unifies view controller behavior around theming and nav bars. +* Adds month validity check to `validationStateForExpirationYear` +* Changes some Apple Pay images to better conform to official guidelines. +* Changes STPPaymentCardTextField's card number placeholder to "4242..." +* Updates STPPaymentCardTextField's CVC placeholder so that it changes to "CVV" for Amex cards + +## 9.3.0 2017-01-05 +* Fixes a regression introduced in v9.0.0 in which color in STPTheme is used as the background color for UINavigationBar + * Note: This will cause navigation bar theming to work properly as described in the Stripe iOS docs, but you may need to audit your custom theme settings if you based them on the actual behavior of 9.0-9.2 +* If the navigation bar has a theme different than the view controller's theme, STP view controllers will use the bar's theme to style it's UIBarButtonItems +* Adds a fallback to using main bundle for localized strings lookup if locale is set to a language the SDK doesn't support +* Adds method to get a string of a card brand from `STPCardBrand` +* Updated description of how to run tests in README +* Fixes crash when user cancels payment before STPBackendAPIAdapter methods finish +* Fixes bug where country picker wouldn't update when first selected. + + +## 9.2.0 2016-11-14 +* Moves FBSnapshotTestCase dependency to Cartfile.private. No changes if you are not using Carthage. +* Adds prebuilt UI for collecting shipping information. + +## 9.1.0 2016-11-01 +* Adds localized strings for 7 languages: de, es, fr, it, ja, nl, zh-Hans. +* Slight redesign to card/billing address entry screen. +* Improved internationalization for State/Province/County address field. +* Adds new Mastercard 2-series BIN ranges. +* Fixes an issue where callbacks may be run on the wrong thread. +* Fixes UIAppearance compatibility in STPPaymentCardTextField. +* Fixes a crash when changing application language via an Xcode scheme. + +## 9.0.0 2016-10-04 +* Change minimum requirements to iOS 8 and Xcode 8 +* Adds "app extension API only" support. +* Updates Swift example app to Swift 3 +* Various fixes to ObjC example app + +## 8.0.7 2016-09-15 +* Add ability to set currency for managed accounts when adding card +* Fix broken links for Privacy Policy/Terms of Service for Remember Me feature +* Sort countries in picker alphabetically by name instead of ISO code +* Make "County" field optional on billing address screen. +* PKPayment-related methods are now annotated as available in iOS8+ only +* Optimized speed of input sanitation methods (thanks @kballard!) + +## 8.0.6 2016-09-01 +* Improved internationalization on billing address forms + * Users in countries that don't use postal codes will no longer see that field. + * The country field is now auto filled in with the phone's region + * Changing the selected country will now live update other fields on the form (such as State/County or Zip/Postal Code). +* Fixed an issue where certain Cocoapods configurations could result in Stripe resource files being used in place of other frameworks' or the app's resources. +* Fixed an issue where when using Apple Pay, STPPaymentContext would fire two `didFinishWithStatus` messages. +* Fixed the `deviceSupportsApplePay` method to also check for Discover cards. +* Removed keys from Stripe.bundle's Info.plist that were causing iTunes Connect to sometimes error on app submission. + +## 8.0.5 2016-08-26 +* You can now optionally use an array of PKPaymentSummaryItems to set your payment amount, if you would like more control over how Apple Pay forms are rendered. +* Updated credit card and Apple Pay icons. +* Fixed some images not being included in the resources bundle target. +* Non-US locales now have an alphanumeric keyboard for postal code entry. +* Modals now use UIModalPresentationStyleFormSheet. +* Added more accessibility labels. +* STPPaymentCardTextField now conforms to UIKeyInput (thanks @theill). + +## 8.0.4 2016-08-01 +* Fixed an issue with Apple Pay payments not using the correct currency. +* View controllers now update their status bar and scroll view indicator styles based on their theme. +* SMS code screen now offers to paste copied codes. + +## 8.0.3 2016-07-25 +* Fixed an issue with some Cocoapods installations + +## 8.0.2 2016-07-09 +* Fixed an issue with custom theming of Stripe UI + +## 8.0.1 2016-07-06 +* Fixed error handling in STPAddCardViewController + +## 8.0.0 2016-06-30 +* Added prebuilt UI for collecting and managing card information. + +## 7.0.2 2016-05-24 +* Fixed an issue with validating certain Visa cards. + +## 7.0.1 2016-04-29 +* Added Discover support for Apple Pay +* Add the now-required `accountHolderName` and `accountHolderType` properties to STPBankAccountParams +* We now record performance metrics for the /v1/tokens API - to disable this behavior, call [Stripe disableAnalytics]. +* You can now demo the SDK more easily by running `pod try stripe`. +* This release also removes the deprecated Checkout functionality from the SDK. + +## 6.2.0 2016-02-05 +* Added an `additionalAPIParameters` field to STPCardParams and STPBankAccountParams for sending additional values to the API - useful for beta features. Similarly, added an `allResponseFields` property to STPToken, STPCard, and STPBankAccount for accessing fields in the response that are not yet reflected in those classes' @properties. + +## 6.1.0 2016-01-21 +* Renamed card on STPPaymentCardTextField to cardParams. +* You can now set an STPPaymentCardTextField's contents programmatically by setting cardParams to an STPCardParams object. +* Added delegate methods for responding to didBeginEditing events in STPPaymentCardTextField. +* Added a UIImage category for accessing our card icon images +* Fixed deprecation warnings for deployment targets >= iOS 9.0 + +## 6.0.0 2015-10-19 +* Splits logic in STPCard into 2 classes - STPCard and STPCardParams. STPCardParams is for making requests to the Stripe API, while STPCard represents the response (you'll almost certainly want just to replace any usage of STPCard in your app with STPCardParams). This also applies to STPBankAccount and the newly-created STPBankAccountParams. +* Version 6.0.1 fixes a minor Cocoapods issue. + +## 5.1.0 2015-08-17 +* Adds STPPaymentCardTextField, a new version of github.com/stripe/PaymentKit featuring many bugfixes. It's useful if you need a pre-built credit card entry form. +* Adds the currency param to STPCard for those using managed accounts & debit card payouts. +* Versions 5.1.1 and 5.1.2 fix minor issues with CocoaPods installation +* Version 5.1.3 contains bug fixes for STPPaymentCardTextField. +* Version 5.1.4 improves compatibility with iOS 9. + +## 5.0.0 2015-08-06 +* Fix an issue with Carthage installation +* Fix an issue with CocoaPods frameworks +* Deprecate native Stripe Checkout + +## 4.0.1 2015-05-06 +* Fix a compiler warning +* Versions 4.0.1 and 4.0.2 fix minor issues with CocoaPods and Carthage installation. + +## 4.0.0 2015-05-06 +* Remove STPPaymentPresenter +* Support for latest ApplePayStubs +* Add nullability annotations to improve Swift support (note: this now requires Swift 1.2) +* Bug fixes + +## 3.1.0 2015-01-19 +* Add support for native Stripe Checkout, as well as STPPaymentPresenter for automatically using Checkout as a fallback for Apple Pay +* Add OSX support, including Checkout +* Add framework targets and Carthage support +* It's safe to remove the STRIPE_ENABLE_APPLEPAY compiler flag after this release. + +## 3.0.0 2015-01-05 +* Migrate code into STPAPIClient +* Add 'brand' and 'funding' properties to STPCard + +## 2.2.2 2014-11-17 +* Add bank account tokenization methods + +## 2.2.1 2014-10-27 +* Add billing address fields to our Apple Pay API +* Various bug fixes and code improvements + +## 2.2.0 2014-10-08 +* Move Apple Pay testing functionality into a separate project, ApplePayStubs. For more info, see github.com/stripe/ApplePayStubs. +* Improve the provided example app + +## 2.1.0 2014-10-07 +* Remove token retrieval API method +* Refactor functional tests to use new XCTestCase functionality + +## 2.0.3 2014-09-24 +* Group ApplePay code in a CocoaPods subspec + +## 2.0.2 2014-09-24 +* Move ApplePay code behind a compiler flag to avoid warnings from Apple when accidentally including it + +## 2.0.1 2014-09-18 +* Fix some small bugs related to ApplePay and iOS8 + +## 2.0 2014-09-09 +* Add support for native payments via Pay + +## 1.2 2014-08-21 +* Removed PaymentKit as a dependency. If you'd like to use it, you may still do so by including it separately. +* Removed STPView. PaymentKit provides a near-identical version of this functionality if you need to migrate. +* Improve example project +* Various code fixes + +## 1.1.4 2014-05-22 +* Fixed an issue where tokenization requests would fail under iOS 6 due to SSL certificate verification + +## 1.1.3 2014-05-12 +* Send some basic version and device details with requests for debugging. +* Added -description to STPToken +* Fixed some minor code nits +* Modernized code + +## 1.1.2 2014-04-21 +* Added test suite for SSL certificate expiry/revocation +* You can now set STPView's delegate from Interface Builder + +## 1.1.1 2014-04-14 +* API methods now verify the server's SSL certificate against a preset blacklist. +* Fixed some bugs with SSL verification. +* Note: This version now requires the `Security` framework. You will need to add this to your app if you're not using CocoaPods. + +## 1.0.4 2014-03-24 + +* Upgraded tests from OCUnit to XCTest +* Fixed an issue with the SenTestingKit dependency +* Removed some dead code + +## 1.0.3 2014-03-21 + +* Fixed: Some example files had target memberships set for StripeiOS and iOSTest. +* Fixed: The example publishable key was expired. +* Fixed: Podspec did not pass linting. +* Some fixes for 64-bit. +* Many improvements to the README. +* Fixed example under iOS 7 +* Some source code cleaning and modernization. + +## 1.0.2 2013-09-09 + +* Add exceptions for null successHandler and errorHandler. +* Added the ability to POST the created token to a URL. +* Made STPCard properties nonatomic. +* Moved PaymentKit to be a submodule; added to Podfile as a dependency. +* Fixed some warnings caught by the static analyzer (thanks to jcjimenez!) + +## 1.0.1 2012-11-16 + +* Add CocoaPods support +* Change directory structure of bindings to make it easier to install + +## 1.0.0 2012-11-16 + +* Initial release + +Special thanks to: Todd Heasley, jcjimenez. diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 00000000..c84a1b5b --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,308 @@ +## Migration Guides +### Migrating from versions < 23.0.0 +* The `Stripe` module is now split between `StripePaymentSheet`, `StripePayments`, and `StripePaymentsUI`. Some manual changes may be required. Migration instructions are available at [https://stripe.com/docs/mobile/ios/sdk-23-migration](https://stripe.com/docs/mobile/ios/sdk-23-migration). +* [Changed] If you use PaymentSheet, you must now `import StripePaymentSheet`. PaymentSheet users no longer need to import the `Stripe` module. +* [Changed] The minimum iOS version is now 13.0. If you'd like to deploy for iOS 12.0, please use Stripe SDK 22.8.4. +* [Changed] STPPaymentCardTextField's `cardParams` parameter has been deprecated in favor of `paymentMethodParams`, making it easier to include the postal code from the card field. If you need to access the `STPPaymentMethodCardParams`, use `.paymentMethodParams.card`. + * Note that `.paymentMethodParams` returns a copy, so `paymentMethodParams.card` should not be set directly. If you need to set the card information, set `.paymentMethodParams` to a new STPPaymentMethodParams: +``` +cardField.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) +``` + +### Migrating from versions < 22.8.0 +* `PaymentSheet.reset()` has been renamed to `PaymentSheet.resetCustomer()`. If calling the former method, follow the warning in Xcode and apply the suggested fix-it. + +### Migrating from versions < 22.2.0 +* `StripeConnections` SDK has been renamed to `StripeFinancialConnections`. If you included `StripeConnections` to support ACH Direct Debit payments, you will need to rename the dependency to `StripeFinancialConnections`. If you are manually installing `StripeConnections`, you will need to remove the old `StripeConnections.xcframework` and include the new `StripeFinancialConnections.xcframework`, which can be found in the [release assets](https://github.com/stripe/stripe-ios/releases/tag/22.2.0) for version 22.2.0 of the SDK. + +### Migrating from versions < 22.0.0 +* The minimum iOS version is now 12.0. If you'd like to deploy for iOS 11.0, please use Stripe SDK 21.12.0. +* `IdentityVerificationSheet` now has an availability requirement of iOS 14.3 on its initializer instead of the `present` method. If your app supports iOS versions < 14.3, you will need to add an availability check for iOS 14.3 before initializing the sheet. + +### Migrating from versions < 21.12.0 +* `Stripe` now requires `StripeApplePay`. If you are manually installing `Stripe`, you will need to include `StripeApplePay.xcframework`, which can be found in the [release assets](https://github.com/stripe/stripe-ios/releases/tag/21.12.0) for version 21.12.0 of the SDK. If you are using CocoaPods or Swift Package Manager, these dependencies will be imported automatically. + +### Migrating from versions < 21.10.0 +* `StripeIdentity` now requires `StripeCameraCore`. If you are manually installing `StripeIdentity`, you will need to include `StripeCameraCore.xcframework`, which can be found in the [release assets](https://github.com/stripe/stripe-ios/releases/tag/21.10.0) for version 21.10.0 of the SDK. If you are using CocoaPods or Swift Package Manager, these dependencies will be imported automatically. + +### Migrating from versions < 21.9.0 +* `Stripe` and `StripeIdentity` now require `StripeUICore`. If you are manually installing `Stripe` or `StripeIdentity`, you will need to include `StripeUICore.xcframework`, which can be found in the [release assets](https://github.com/stripe/stripe-ios/releases/tag/21.9.0) for version 21.9.0 of the SDK. If you are using CocoaPods or Swift Package Manager, these dependencies will be imported automatically. + +### Migrating from versions < 21.8.1 +* The `Stripe` module now requires `StripeCore`. If you are manually installing the Stripe SDK, you will need to include `StripeCore.xcframework`, which can be found in the [release assets](https://github.com/stripe/stripe-ios/releases/tag/21.8.1) for version 21.8.1 of the SDK. If you are using CocoaPods or Swift Package Manager, these dependencies will be imported automatically. + +### Migrating from versions < 21.4.0 +* STPPaymentHandler now presents its SFSafariViewController using the `.overFullScreen` presentation style by default. To select a different style, implement the `STPAuthenticationContext.configureSafariViewController(_:)` function in your `STPAuthenticationContext`. + +### Migrating from versions < 21.2.0 +* Stripe3DS2 is now a separate component for Carthage users. You must embed both Stripe.xcframework and Stripe3DS2.xcframework in your app. + +### Migrating from versions < 21.0.0 +* The SDK is now written in Swift, and some manual changes are required. Migration instructions are available at [https://stripe.com/docs/mobile/ios/sdk-21-migration](https://stripe.com/docs/mobile/ios/sdk-21-migration). + +### Migrating from versions < 20.1.0 +* Swift Package Manager users may need to remove and re-add Stripe from the `Frameworks, Libraries, and Embedded Content` section of your target's settings after updating. +* Swift Package Manager users with Xcode 12.0 may need to use a [workaround](https://github.com/stripe/stripe-ios/issues/1673) for a code signing issue. This is fixed in Xcode 12.2. + +### Migrating from versions < 20.0.0 +* The minimum iOS version is now 11.0. If you'd like to deploy for iOS 10.0, please use Stripe SDK 19.4.0. +* Card.io is no longer supported. To enable our built-in [card scanning](https://github.com/stripe/stripe-ios#card-scanning) beta, set the `cardScanningEnabled` flag on STPPaymentConfiguration. +* Catalyst support is out of beta, and now requires Swift Package Manager with Xcode 12 or Cocoapods 1.10. + +### Migrating from versions < 19.4.0 +* `metadata` fields are no longer populated on retrieved Stripe API objects and must be fetched on your server using your secret key. If this is causing issues with your deployed app versions please reach out to [Stripe Support](https://support.stripe.com/?contact=true). These fields have been marked as deprecated and will be removed in a future SDK version. + +### Migrating from versions < 19.3.0 +* `STPAUBECSFormView` now inherits from `UIView` instead of `UIControl` + +### Migrating from versions < 19.2.0 +* The `STPApplePayContext` 'applePayContext:didCreatePaymentMethod:completion:` delegate method now includes paymentInformation: 'applePayContext:didCreatePaymentMethod:paymentInformation:completion:`. + + +### Migrating from versions < 19.0.0 +#### `publishableKey` and `stripeAccount` changes +* Deprecates `publishableKey` and `stripeAccount` properties of `STPPaymentConfiguration`. + * If you used `STPPaymentConfiguration.sharedConfiguration` to set `publishableKey` and/or `stripeAccount`, use `STPAPIClient.sharedClient` instead. + * If you passed a STPPaymentConfiguration instance to an SDK component, you should instead create an STPAPIClient, set publishableKey on it, and set the SDK component's APIClient property. +* The SDK now uses `STPAPIClient.sharedClient` to make API requests by default. + +This changes the behavior of the following classes, which previously used API client instances configured from `STPPaymentConfiguration.shared`: `STPCustomerContext`, `STPPaymentOptionsViewController`, `STPAddCardViewController`, `STPPaymentContext`, `STPPinManagementService`, `STPPushProvisioningContext`. + +You are affected by this change if: + +1. You use `stripeAccount` to work with your Connected accounts +2. You use one of the above affected classes +3. You set different `stripeAccount` values on `STPPaymentConfiguration` and `STPAPIClient`, i.e. `STPPaymentConfiguration.shared.stripeAccount != STPAPIClient.shared.stripeAccount` + +If all three of the above conditions are true, you must update your integration! The SDK used to send `STPPaymentConfiguration.shared.stripeAccount`, and will now send `STPAPIClient.shared.stripeAccount`. + +For example, if you are a Connect user who stores Payment Methods on your platform, but clones PaymentMethods to a connected account and creates direct charges on that connected account i.e. if: + +1. You never set `STPPaymentConfiguration.shared.stripeAccount` +2. You set `STPAPIClient.shared.stripeAccount` + +We recommend you do the following: + +``` + // By default, you don't want the SDK to pass stripeAccount + STPAPIClient.shared().publishableKey = "pk_platform" + STPAPIClient.shared().stripeAccount = nil + + // You do want the SDK to pass stripeAccount when it makes payments directly on your connected account, so + // you create a separate APIClient instance... + let connectedAccountAPIClient = STPAPIClient(publishableKey: "pk_platform") + + // ...set stripeAccount on it... + connectedAccountAPIClient.stripeAccount = "your connected account's id" + + // ...and either set the relevant SDK components' apiClient property to your custom APIClient instance: + STPPaymentHandler.shared().apiClient = connectedAccountAPIClient // e.g. if you are using PaymentIntents + + // ...or use it directly to make API requests with `stripeAccount` set: + connectedAccountAPIClient.createToken(withCard:...) // e.g. if you are using Tokens + Charges +``` +#### Postal code changes +* The user's postal code is now collected by default in countries that support postal codes. We always recommend collecting a postal code to increase card acceptance rates and reduce fraud. To disable this behavior: + * For STPPaymentContext and other full-screen UI, set your `STPPaymentConfiguration`'s `.requiredBillingAddressFields` to `STPBillingAddressFieldsNone`. + * For a PKPaymentRequest, set `.requiredBillingContactFields` to an empty set. If your app supports iOS 10, also set `.requiredBillingAddressFields` to `PKAddressFieldNone`. + * For STPPaymentCardView, set `.postalCodeEntryEnabled` to `NO`. +* Users may now enter spaces, dashes, and uppercase letters into the postal code field in situations where the user has not explicitly selected a country. This allows users with non-US addreses to enter their postal code. +* `STPBillingAddressFieldsZip` has been renamed to `STPBillingAddressFieldsPostalCode`. +#### Localization changes +* All [Stripe Error messages](https://stripe.com/docs/api/errors#errors-message) are now localized + based on the device locale. + + For example, when retrieving a SetupIntent with a nonexistent `id` + when the device locale is set to `Locale.JAPAN`, the error message will now be localized. + ``` + // before - English + "No such setupintent: seti_invalid123" + + // after - Japanese + "そのような setupintent はありません : seti_invalid123" + ``` + +### Migrating from versions < 18.0.0 +* Some error messages from the Payment Intents API are now localized to the user's display language. If your application's logic depends on specific `message` strings from the Stripe API, please use the error [`code`](https://stripe.com/docs/error-codes) instead. +* `STPPaymentResult` may contain a `paymentMethodParams` instead of a `paymentMethod` when using single-use payment methods such as FPX. Because of this, `STPPaymentResult.paymentMethod` is now nullable. Instead of setting the `paymentMethodId` manually on your `paymentIntentParams`, you may now call `paymentIntentParams.configure(with result: STPPaymentResult)`: +``` +// 17.0.0 +paymentIntentParams.paymentMethodId = paymentResult.paymentMethod.stripeId + +// 18.0.0 +paymentIntentParams.configure(with: paymentResult) +``` +* `STPPaymentOptionTypeAll` has been renamed to `STPPaymentOptionTypeDefault`. This option will not include FPX or future optional payment methods. +* The minimum iOS version is now 10.0. If you'd like to deploy for iOS 9.0, please use Stripe SDK 17.0.2. + +### Migrating from versions < 17.0.0 +* The API version has been updated from 2015-10-12 to 2019-05-16. CHANGELOG.md has details on the changes made, which includes breaking changes for `STPConnectAccountParams` users. Your backend Stripe API version should be sufficiently decoupled from the SDK's so that keeping their versions in sync is not required, and no further action is required to migrate to this version of the SDK. +* For STPPaymentContext users: the completion block type in `paymentContext:didCreatePaymentResult:completion:` has changed to `STPPaymentStatusBlock`, to let you inform the context that the user has cancelled. + +### Migrating from versions < 16.0.0 +* The following have been migrated from Source/Token to PaymentMethod. If you have integrated with any of these things, you must also migrate to PaymentMethod and the Payment Intent API. See https://stripe.com/docs/payments/payment-intents/migration. See CHANGELOG.md for more details. + * UI components + * STPPaymentCardTextField + * STPAddCardViewController + * STPPaymentOptionsViewController + * PaymentContext + * STPPaymentContext + * STPCustomerContext + * STPBackendAPIAdapter + * STPPaymentResult + * Standard Integration example project +* `STPPaymentIntentAction*` types have been renamed to `STPIntentAction*`. Xcode should offer a deprecation warning & fix-it to help you migrate. +* `STPPaymentHandler` supports 3DS2 authentication, and is recommended instead of `STPRedirectContext`. See https://stripe.com/docs/mobile/ios/authentication + +### Migrating from versions < 15.0.0 +* "PaymentMethod" has a new meaning: https://stripe.com/docs/api/payment_methods/object. All things referring to "PaymentMethod" have been renamed to "PaymentOption" (see CHANGELOG.md for the full list). `STPPaymentMethod` and `STPPaymentMethodType` have been rewritten to match this new API object. +* PaymentMethod succeeds Source as the recommended way to charge customers. In this vein, several 'Source'-named things have been deprecated, and replaced with 'PaymentMethod' equivalents. For example, `STPPaymentIntentsStatusRequiresSource` is replaced by `STPPaymentIntentsStatusRequiresPaymentMethod` (see CHANGELOG.md for the full list). Following the deprecation warnings & fix-its will be enough to migrate your code - they've simply been renamed, and will continue to work for Source-based flows. + +### Migrating from versions < 14.0.0 +* `STPPaymentCardTextField` now copies the `STPCardParams` object when setting/getting the `cardParams` property, instead of sharing the object with the caller. + * Changes to the `STPCardParams` object after setting `cardParams` no longer mutate the object held by the `STPPaymentCardTextField` + * Changes to the object returned by `STPPaymentCardTextField.cardParams` no longer mutate the object held by the `STPPaymentCardTextField` + * This is a breaking change for code like: `paymentCardTextField.cardParams.name = @"Jane Doe";` +* `STPPaymentIntentParams.returnUrl` has been renamed to `STPPaymentIntentParams.returnURL`. Xcode should offer a deprecation warning & fix-it to help you migrate. +* `STPPaymentIntent.returnUrl` has been removed, because it's no longer a property of the PaymentIntent. When the PaymentIntent status is `.requiresSourceAction`, and the `nextSourceAction.type` is `.authorizeWithURL`, you can find the return URL at `nextSourceAction.authorizeWithURL.returnURL`. + +### Migrating from versions < 13.1.0 + * The SDK now supports PaymentIntents with `STPPaymentIntent`, which use `STPRedirectContext` in the same way that `STPSource` does + * `STPRedirectContextCompletionBlock` has been renamed to `STPRedirectContextSourceCompletionBlock`. It has the same signature, and Xcode should offer a deprecation warning & fix-it to help you migrate. + +### Migrating from versions < 13.0.0 +* Remove Bitcoin source support because Stripe no longer processes Bitcoin payments: https://stripe.com/blog/ending-bitcoin-support + * Sources can no longer have a "STPSourceTypeBitcoin" source type. These sources will now be interpreted as "STPSourceTypeUnknown". + * You can no longer `createBitcoinParams`. Please use a different payment method. + +### Migrating from versions < 12.0.0 +* The SDK now requires iOS 9+ and Xcode version 9+. If you need to support iOS 8 or Xcode 8, the last supported version is [11.5.0](https://github.com/stripe/stripe-ios/releases/tag/v11.5.0) +* `STPPaymentConfiguration.requiredShippingAddress` now is a set of `STPContactField` objects instead of a `PKAddressField` bitmask. + * Most of the previous `PKAddressField` constants have matching `STPContactField` constants. To convert your code, switch to passing in a set of the matching constants + * Example: `(PKAddressField)(PKAddressFieldName|PKAddressFieldPostalAddress)` becomes `[NSSet setwithArray:@[STPContactFieldName, STPContactFieldPostalAddress]]`) + * Anywhere you were using `PKAddressFieldNone` you can now simply pass in `nil` + * If you were using `PKAddressFieldAll`, you must switch to manually listing all the fields that you want. + * The new constants also correspond to and work similarly to Apple's new `PKContactField` values. +* `AddressBook` framework support has been removed. If you were using AddressBook related functionality, you must switch over to using the `Contacts` framework. +* `STPRedirectContext` will no longer retain itself for the duration of the redirect. If you were relying on this functionality, you must change your code to explicitly maintain a reference to it. + +### Migrating from versions < 11.4.0 +* The `STPBackendAPIAdapter` protocol and all associated methods are no longer deprecated. We still recommend using `STPCustomerContext` to update a Stripe customer object on your behalf, rather than using your own implementation of `STPBackendAPIAdapter`. + +### Migrating from versions < 11.3.0 +* Changes to `STPCard`, `STPCardParams`, `STPBankAccount`, and `STPBankAccountParams` + * `STPCard` no longer subclasses from `STPCardParams`. You must now specifically create `STPCardParams` objects to create new tokens. + * `STPBankAccount` no longer subclasses from `STPBankAccountParams`. + * You can no longer directly create `STPCard` objects, you should only use ones that have been decoded from Stripe API responses via `STPAPIClient`. + * All `STPCard` and `STPBankAccount` properties have been made readonly. + * Broken out individual address properties on `STPCard` and `STPCardParams` have been deprecated in favor of the grouped `address` property. +* The value of `[STPAPIResponseDecodable allResponseFields]` is now completely (deeply) filtered to not contain any instances of `[NSNull null]`. Previously, only `[NSNull null]` one level deep (shallow) were removed. + +### Migrating from versions < 11.2.0 +* `STPCustomer`'s `shippingAddress` property is now correctly annotated as nullable. Its type is an optional (`STPAddress?`) in Swift. + +### Migrating from versions < 11.0.0 +- We've greatly simplified the integration for `STPPaymentContext`. In order to migrate to the new `STPPaymentContext` integration using ephemeral keys, you'll need to: + 1. On your backend, add a new endpoint that creates an ephemeral key for the Stripe customer associated with your user, and returns its raw JSON. Note that you should _not_ remove the 3 endpoints you added for your initial PaymentContext integration until you're ready to drop support for previous versions of your app. + 2. In your app, make your API client class conform to `STPEphemeralKeyProvider` by adding a method that requests an ephemeral key from the endpoint you added in (1). + 3. In your app, remove any references to `STPBackendAPIAdapter`. Your API client class will no longer need to conform to `STPBackendAPIAdapter`, and you can delete the `retrieveCustomer`, `attachSourceToCustomer`, and `selectDefaultCustomerSource` methods. + 4. Instead of using the initializers for `STPPaymentContext` or `STPPaymentMethodsViewController` that take an `STPBackendAPIAdapter` parameter, you should use the new initializers that take an `STPCustomerContext` parameter. You'll need to set up your instance of `STPCustomerContext` using the key provider you set up in (2). +- For a more detailed overview of the new integration, you can refer to our tutorial at https://stripe.com/docs/mobile/ios/standard +- `[STPFile stringFromPurpose:]` now returns `nil` for `STPFilePurposeUnknown`. Will return a non-nil value for all other `STPFilePurpose`. +- We've removed the `email` and `phone` properties in `STPUserInformation`. You can pre-fill this information in the shipping form using the new `shippingAddress` property. +- The SMS card fill feature has been removed from `STPPaymentContext`, as well as the associated `smsAutofillDisabled` configuration option (ie it will now always behave as if it is disabled). + +### Migrating from versions < 10.2.0 +- `paymentRequestWithMerchantIdentifier:` has been deprecated. You should instead use `paymentRequestWithMerchantIdentifier:country:currency:`. Apple Pay is now available in many countries and currencies, and you should use the appropriate values for your business. +- We've added a `paymentCountry` property to `STPPaymentContext`. This affects the countryCode of Apple Pay payments, and defaults to "US". You should set this to the country your Stripe account is in. +- Polling for source object updates is deprecated. Check https://stripe.com/docs for the latest best practices on how to integrate with the sources API using webhooks. +- `paymentMethodsViewController:didSelectPaymentMethod:` is now optional. If you have an empty implementation of this method, you can remove it. + +### Migrating from versions < 10.1.0 + +- STPPaymentMethodsViewControllerDelegate now has a separate `paymentMethodsViewControllerDidCancel:` callback, differentiating from successful method selections. You should make sure to also dismiss the view controller in that callback. + +### Migrating from versions < 10.0 + +- Methods deprecated in Version 6.0 have now been removed. +- The `STPSource` protocol has been renamed `STPSourceProtocol`. +- `STPSource` is now a model object representing a source from the Stripe API. https://stripe.com/docs/sources +- `STPCustomer` will now include `STPSource` objects in its `sources` array if a customer has attached sources. +- `STPErrorCode` and `STPCardErrorCode` are now first class Swift enums (before, their types were `Int` and `String`, respectively) + +### Migrating from versions < 9.0 + +Version 9.0 drops support for iOS 7.x and Xcode 7.x. If you need to support iOS or Xcode versions below 8.0, the last compatible Stripe SDK release is version 8.0.7. + +### Migrating from versions < 6.0 + +6.0 moves most of the contents of `STPCard` into a new class, `STPCardParams`, which represents a request to the Stripe API. `STPCard` now only refers to responses from the Stripe API. Most apps should be able to simply replace all usage of `STPCard` with `STPCardParams` - you should only use `STPCard` if you're dealing with an API response, e.g. a card attached to an `STPToken`. This renaming has been done in a way that will avoid breaking changes, although using `STPCard`s to make requests to the Stripe API will produce deprecation warnings. + +### Migrating from versions < 5.0 + +5.0 deprecates our native Stripe Checkout adapters. If you were using these, we recommend building your own credit card form instead. If you need help with this, please contact support@stripe.com. + +### Migrating from versions < 3.0 + +Before version 3.0, most token-creation methods were class methods on the `Stripe` class. These are now all instance methods on the `STPAPIClient` class. Where previously you might write +```objective-c +[Stripe createTokenWithCard:card publishableKey:myPublishableKey completion:completion]; +``` +you would now instead write +```objective-c +STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:myPublishableKey]; +[client createTokenWithCard:card completion:completion]; +``` +This version also made several helper classes, including `STPAPIConnection` and `STPUtils`, private. You should remove any references to them from your code (most apps shouldn't have any). + +## Migrating from versions < 1.2 + +Versions of Stripe-iOS prior to 1.2 included a class called `STPView`, which provided a pre-built credit card form. This functionality has been moved from Stripe-iOS to PaymentKit, a separate project. If you were using `STPView` prior to version 1.2, migrating is simple: + +1. Add PaymentKit to your project, as explained on its [project page](https://github.com/stripe/PaymentKit). +2. Replace any references to `STPView` with a `PTKView` instead. Similarly, any classes that implement `STPViewDelegate` should now instead implement the equivalent `PTKViewDelegate` methods. Note that unlike `STPView`, `PTKView` does not take a Stripe API key in its constructor. +3. To submit the credit card details from your `PTKView` instance, where you would previously call `createToken` on your `STPView`, replace that with the following code (assuming `self.paymentView` is your `PTKView` instance): + +```objective-c +if (![self.paymentView isValid]) { + return; +} +STPCard *card = [[STPCard alloc] init]; +card.number = self.paymentView.card.number; +card.expMonth = self.paymentView.card.expMonth; +card.expYear = self.paymentView.card.expYear; +card.cvc = self.paymentView.card.cvc; +STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:publishableKey]; +[client createTokenWithCard:card completion:^(STPToken *token, NSError *error) { + if (error) { + // handle the error as you did previously + } else { + // submit the token to your payment backend as you did previously + } +}]; +``` + +## Misc. notes + +### Handling errors + +See [our documentation](https://docs.stripe.com/error-codes) for a list of error codes that may be returned from the Stripe API. + +### Validating STPCards + +You have a few options for handling validation of credit card data on the client, depending on what your application does. Client-side validation of credit card data is not required since our API will correctly reject invalid card information, but can be useful to validate information as soon as a user enters it, or simply to save a network request. + +The simplest thing you can do is to populate an `STPCard` object and, before sending the request, call `- (BOOL)validateCardReturningError:` on the card. This validates the entire card object, but is not useful for validating card properties one at a time. + +To validate `STPCard` properties individually, you should use the following: + +```objective-c + - (BOOL)validateNumber:error: + - (BOOL)validateCvc:error: + - (BOOL)validateExpMonth:error: + - (BOOL)validateExpYear:error: +``` + +These methods follow the validation method convention used by [key-value validation](http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/KeyValueCoding/Articles/Validation.html). So, you can use these methods by invoking them directly, or by calling `[card validateValue:forKey:error]` for a property on the `STPCard` object. + +When using these validation methods, you will want to set the property on your card object when a property does validate before validating the next property. This allows the methods to use existing properties on the card correctly to validate a new property. For example, validating `5` for the `expMonth` property will return YES if no `expYear` is set. But if `expYear` is set and you try to set `expMonth` to 5 and the combination of `expMonth` and `expYear` is in the past, `5` will not validate. The order in which you call the validate methods does not matter for this though. diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..d351d0ff --- /dev/null +++ b/Package.swift @@ -0,0 +1,157 @@ +// swift-tools-version:5.7 +import PackageDescription + +let package = Package( + name: "Stripe", + defaultLocalization: "en", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "Stripe", + targets: ["Stripe"] + ), + .library( + name: "StripePayments", + targets: ["StripePayments"] + ), + .library( + name: "StripePaymentsUI", + targets: ["StripePaymentsUI"] + ), + .library( + name: "StripePaymentSheet", + targets: ["StripePaymentSheet"] + ), + .library( + name: "StripeApplePay", + targets: ["StripeApplePay"] + ), + .library( + name: "StripeIdentity", + targets: ["StripeIdentity"] + ), + .library( + name: "StripeCardScan", + targets: ["StripeCardScan"] + ), + .library( + name: "StripeFinancialConnections", + targets: ["StripeFinancialConnections"] + ) + ], + targets: [ + .target( + name: "Stripe", + dependencies: ["Stripe3DS2", "StripeCore", "StripeApplePay", "StripeUICore", "StripePayments", "StripePaymentsUI"], + path: "Stripe/StripeiOS", + exclude: ["Info.plist"], + resources: [ + .process("Resources/StripeiOS.xcassets"), + .process("PrivacyInfo.xcprivacy") + ] + ), + .target( + name: "Stripe3DS2", + path: "Stripe3DS2/Stripe3DS2", + exclude: ["Info.plist", "Resources/CertificateFiles", "include/Stripe3DS2-Prefix.pch"], + resources: [ + .process("Resources"), + .process("PrivacyInfo.xcprivacy") + ], + cSettings: [ + .headerSearchPath(".") + ] + ), + .target( + name: "StripeCameraCore", + dependencies: ["StripeCore"], + path: "StripeCameraCore/StripeCameraCore", + exclude: ["Info.plist"] + ), + .target( + name: "StripeCore", + path: "StripeCore/StripeCore", + exclude: ["Info.plist"], + resources: [ + .process("PrivacyInfo.xcprivacy") + ] + ), + .target( + name: "StripeApplePay", + dependencies: ["StripeCore"], + path: "StripeApplePay/StripeApplePay", + exclude: ["Info.plist"] + ), + .target( + name: "StripeIdentity", + dependencies: ["StripeCore", "StripeUICore", "StripeCameraCore"], + path: "StripeIdentity/StripeIdentity", + exclude: ["Info.plist"], + resources: [ + .process("Resources/Images") + ] + ), + .target( + name: "StripeCardScan", + dependencies: ["StripeCore"], + path: "StripeCardScan/StripeCardScan", + exclude: ["Info.plist"], + resources: [ + .copy("Resources/CompiledModels/UxModel.mlmodelc"), + .copy("Resources/CompiledModels/SSDOcr.mlmodelc") + ] + ), + .target( + name: "StripeUICore", + dependencies: ["StripeCore"], + path: "StripeUICore/StripeUICore", + exclude: ["Info.plist"], + resources: [ + .process("Resources/StripeUICore.xcassets"), + .process("Resources/JSON") + ] + ), + .target( + name: "StripePayments", + dependencies: ["StripeCore", "Stripe3DS2"], + path: "StripePayments/StripePayments", + exclude: ["Info.plist"], + resources: [ + .process("Resources") + ] + ), + .target( + name: "StripePaymentsUI", + dependencies: ["StripeCore", "Stripe3DS2", "StripePayments", "StripeUICore"], + path: "StripePaymentsUI/StripePaymentsUI", + exclude: ["Info.plist"], + resources: [ + .process("Resources/StripePaymentsUI.xcassets"), + .process("Resources/JSON") + ] + ), + .target( + name: "StripePaymentSheet", + dependencies: ["StripePaymentsUI", "StripeApplePay", "StripePayments", "StripeCore", "StripeUICore"], + path: "StripePaymentSheet/StripePaymentSheet", + exclude: ["Info.plist"], + resources: [ + .process("Resources/StripePaymentSheet.xcassets"), + .process("Resources/JSON"), + .process("PrivacyInfo.xcprivacy") + ] + ), + .target( + name: "StripeFinancialConnections", + dependencies: ["StripeCore", "StripeUICore"], + path: "StripeFinancialConnections/StripeFinancialConnections", + exclude: ["Info.plist"], + resources: [ + .process("Resources/Images"), + .process("PrivacyInfo.xcprivacy") + ] + ) + ] +) diff --git a/README.md b/README.md index 4fea3738..10ac30ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,156 @@ -# stripe-ios-spm -This is a lightweight Swift Package Manager mirror for the [Stripe iOS SDK](https://github.com/stripe/stripe-ios). It offers [tagged source releases](https://github.com/stripe/stripe-ios-spm/releases) for each version of the SDK. +# Stripe iOS SDK -Please file issues on the [`stripe-ios` issues page](https://github.com/stripe/stripe-ios/issues). +[![CocoaPods](https://img.shields.io/cocoapods/v/Stripe.svg?style=flat)](http://cocoapods.org/?q=author%3Astripe%20name%3Astripe) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![License](https://img.shields.io/cocoapods/l/Stripe.svg?style=flat)](https://github.com/stripe/stripe-ios/blob/master/LICENSE) +[![Platform](https://img.shields.io/cocoapods/p/Stripe.svg?style=flat)](https://github.com/stripe/stripe-ios#) + +> [!TIP] +> Want to chat live with Stripe engineers? Join us on our [Discord server](https://stripe.com/go/developer-chat). + +The Stripe iOS SDK makes it quick and easy to build an excellent payment experience in your iOS app. We provide powerful and customizable UI screens and elements that can be used out-of-the-box to collect your users' payment details. We also expose the low-level APIs that power those UIs so that you can build fully custom experiences. + +Get started with our [📚 integration guides](https://stripe.com/docs/payments/accept-a-payment?platform=ios) and [example projects](#examples), or [📘 browse the SDK reference](https://stripe.dev/stripe-ios/docs/index.html). + +> Updating to a newer version of the SDK? See our [migration guide](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) and [changelog](https://github.com/stripe/stripe-ios/blob/master/CHANGELOG.md). + +Table of contents +================= + + + * [Features](#features) + * [Releases](#releases) + * [Requirements](#requirements) + * [Getting started](#getting-started) + * [Integration](#integration) + * [Examples](#examples) + * [Building from source](#building-from-source) + * [Card scanning](#card-scanning) + * [Contributing](#contributing) + * [Migrating](#migrating-from-older-versions) + * [Code Stye](#code-style) + * [Licenses](#licenses) + + + +## Features + +**Simplified security**: We make it simple for you to collect sensitive data such as credit card numbers and remain [PCI compliant](https://stripe.com/docs/security#pci-dss-guidelines). This means the sensitive data is sent directly to Stripe instead of passing through your server. For more information, see our [integration security guide](https://stripe.com/docs/security). + +**Apple Pay**: [StripeApplePay](StripeApplePay/README.md) provides a [seamless integration with Apple Pay](https://stripe.com/docs/apple-pay). + +**SCA-ready**: The SDK automatically performs native [3D Secure authentication](https://stripe.com/docs/payments/3d-secure) if needed to comply with [Strong Customer Authentication](https://stripe.com/docs/strong-customer-authentication) regulation in Europe. + +**Native UI**: We provide native screens and elements to collect payment details. For example, [PaymentSheet](https://stripe.com/docs/payments/accept-a-payment?platform=ios) is a prebuilt UI that combines all the steps required to pay - collecting payment details, billing details, and confirming the payment - into a single sheet that displays on top of your app. + +PaymentSheet + +**Stripe API**: [StripePayments](StripePayments/README.md) provides [low-level APIs](https://stripe.dev/stripe-ios/docs/Classes/STPAPIClient.html) that correspond to objects and methods in the Stripe API. You can build your own entirely custom UI on top of this layer, while still taking advantage of utilities like [STPCardValidator](https://stripe.dev/stripe-ios/docs/Classes/STPCardValidator.html) to validate your user’s input. + +**Card scanning**: We support card scanning on iOS 13 and higher. See our [Card scanning](#card-scanning) section. + +**App Clips**: The `StripeApplePay` module provides a [lightweight SDK for offering Apple Pay in an App Clip](https://stripe.com/docs/apple-pay#app-clips). + +**Localized**: We support the following localizations: Bulgarian, Catalan, Chinese (Hong Kong), Chinese (Simplified), Chinese (Traditional), Croatian, Czech, Danish, Dutch, English (US), English (United Kingdom), Estonian, Filipino, Finnish, French, French (Canada), German, Greek, Hungarian, Indonesian, Italian, Japanese, Korean, Latvian, Lithuanian, Malay, Maltese, Norwegian Bokmål, Norwegian Nynorsk (Norway), Polish, Portuguese, Portuguese (Brazil), Romanian, Russian, Slovak, Slovenian, Spanish, Spanish (Latin America), Swedish, Turkish, Thai and Vietnamese. + +**Identity**: Learn about our [Stripe Identity iOS SDK](StripeIdentity/README.md) to verify the identity of your users. + +#### Recommended usage + +If you're selling digital products or services that will be consumed within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use Apple's in-app purchase APIs. See the [App Store review guidelines](https://developer.apple.com/app-store/review/guidelines/#payments) for more information. For all other scenarios you can use this SDK to process payments via Stripe. + +#### Privacy + +The Stripe iOS SDK collects data to help us improve our products and prevent fraud. This data is never used for advertising and is not rented, sold, or given to advertisers. Our full privacy policy is available at [https://stripe.com/privacy](https://stripe.com/privacy). + +For help with Apple's App Privacy Details form in App Store Connect, visit [Stripe iOS SDK Privacy Details](https://support.stripe.com/questions/stripe-ios-sdk-privacy-details). + +## Modules +|Module|Description|Compressed|Uncompressed| +|------|-----------|----------|------------| +|StripePaymentSheet|Stripe's [prebuilt payment UI](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=payment-sheet).|2.7MB|6.3MB| +|Stripe|Contains all the below frameworks, plus [Issuing](https://stripe.com/docs/issuing/cards/digital-wallets?platform=iOS) and [Basic Integration](https://stripe.com/docs/mobile/ios/basic).|2.3MB|5.1MB| +|StripeApplePay|[Apple Pay support](/docs/apple-pay), including `STPApplePayContext`.|0.4MB|1.0MB| +|StripePayments|Bindings for the Stripe Payments API.|1.0MB|2.6MB| +|StripePaymentsUI|Bindings for the Stripe Payments API, [STPPaymentCardTextField](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=custom), STPCardFormView, and other UI elements.|1.7MB|3.9MB| + +## Releases + +We support Cocoapods, Carthage, and Swift Package Manager. + +If you link the library manually, use a version from our [releases](https://github.com/stripe/stripe-ios/releases) page and make sure to embed all of the required frameworks. + +For the `Stripe` module, link the following frameworks: +- `Stripe.xcframework` +- `Stripe3DS2.xcframework` +- `StripeApplePay.xcframework` +- `StripePayments.xcframework` +- `StripePaymentsUI.xcframework` +- `StripeCore.xcframework` +- `StripeUICore.xcframework` + +For other modules, follow the instructions below: +- [StripePaymentSheet](StripePaymentSheet/README.md#manual-linking) +- [StripePayments](StripePayments/README.md#manual-linking) +- [StripePaymentsUI](StripePaymentsUI/README.md#manual-linking) +- [StripeApplePay](StripeApplePay/README.md#manual-linking) +- [StripeIdentity](StripeIdentity/README.md#manual-linking) + +If you're reading this on GitHub.com, please make sure you are looking at the [tagged version](https://github.com/stripe/stripe-ios/tags) that corresponds to the release you have installed. Otherwise, the instructions and example code may be mismatched with your copy. + +## Requirements + +The Stripe iOS SDK requires Xcode 15 or later and is compatible with apps targeting iOS 13 or above. We support Catalyst on macOS 11 or later. + +For iOS 12 support, please use [v22.8.4](https://github.com/stripe/stripe-ios/tree/v22.8.4). For iOS 11 support, please use [v21.13.0](https://github.com/stripe/stripe-ios/tree/v21.13.0). For iOS 10, please use [v19.4.0](https://github.com/stripe/stripe-ios/tree/v19.4.0). If you need to support iOS 9, use [v17.0.2](https://github.com/stripe/stripe-ios/tree/v17.0.2). + +## Getting started + +### Integration + +Get started with our [📚 integration guides](https://stripe.com/docs/payments/accept-a-payment?platform=ios) and [example projects](/Example), or [📘 browse the SDK reference](https://stripe.dev/stripe-ios/docs/index.html) for fine-grained documentation of all the classes and methods in the SDK. + +### Examples + +- [Prebuilt UI](Example/PaymentSheet%20Example) (Recommended) + - This example demonstrates how to build a payment flow using [`PaymentSheet`](https://stripe.com/docs/payments/accept-a-payment?platform=ios), an embeddable native UI component that lets you accept [10+ payment methods](https://stripe.com/docs/payments/payment-methods/integration-options#payment-method-product-support) with a single integration. + +- [Non-Card Payment Examples](Example/Non-Card%20Payment%20Examples) + - This example demonstrates how to manually accept various payment methods using the Stripe API. + +## Card scanning + +[PaymentSheet](https://stripe.com/docs/payments/accept-a-payment?platform=ios) offers built-in card scanning. To enable card scanning, you'll need to set `NSCameraUsageDescription` in your application's plist, and provide a reason for accessing the camera (e.g. "To scan cards"). Card scanning is supported on devices with iOS 13 or higher. + +You can demo this feature in our [PaymentSheet example app](Example/PaymentSheet%20Example). When you run the example app on a device, you'll see a "Scan Card" button when adding a new card. + +## Contributing + +We welcome contributions of any kind including new features, bug fixes, and documentation improvements. Please first open an issue describing what you want to build if it is a major change so that we can discuss how to move forward. Otherwise, go ahead and open a pull request for minor changes such as typo fixes and one liners. + +### Running tests + +1. Install Carthage 0.37 or later (if you have homebrew installed, `brew install carthage`) +2. From the root of the repo, run `bundle install && bundle exec fastlane stripeios_tests`. This will install the test dependencies and run the tests. +3. Once you have run this once, you can also run the tests in Xcode from the `StripeiOS` target in `Stripe.xcworkspace`. + +To re-record snapshot tests, use the `bundle exec ruby ci_scripts/snapshots.rb --record`. + +## Migrating from older versions + +See [MIGRATING.md](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md) + +## Code style +We use [swiftlint](https://github.com/realm/SwiftLint) to enforce code style. + +To install it, run `brew install swiftlint` + +To lint your code before pushing you can run `ci_scripts/lint_modified_files.sh` + +You can also add this script as a pre-push hook by running `ln -s "$(pwd)/ci_scripts/lint_modified_files.sh" .git/hooks/pre-push && chmod +x .git/hooks/pre-push` + +To format modified files automatically, you can use `ci_scripts/format_modified_files.sh` and you can add it as a pre-commit hook using `ln -s "$(pwd)/ci_scripts/format_modified_files.sh" .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit` + +## Licenses + +- [Stripe iOS SDK License](LICENSE) diff --git a/Stripe/BuildConfigurations/Stripe Tests-Debug.xcconfig b/Stripe/BuildConfigurations/Stripe Tests-Debug.xcconfig new file mode 100644 index 00000000..6d153e27 --- /dev/null +++ b/Stripe/BuildConfigurations/Stripe Tests-Debug.xcconfig @@ -0,0 +1,8 @@ +// +// Stripe Tests-Debug.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS Tests-Debug.xcconfig" + +SWIFT_OBJC_BRIDGING_HEADER = StripeiOSTests/StripeiOS Tests-Bridging-Header.h +GCC_PREFIX_HEADER = StripeiOSTests/StripeTests-Prefix.pch diff --git a/Stripe/BuildConfigurations/Stripe Tests-Release.xcconfig b/Stripe/BuildConfigurations/Stripe Tests-Release.xcconfig new file mode 100644 index 00000000..b72a89b2 --- /dev/null +++ b/Stripe/BuildConfigurations/Stripe Tests-Release.xcconfig @@ -0,0 +1,8 @@ +// +// Stripe Tests-Release.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS Tests-Release.xcconfig" + +SWIFT_OBJC_BRIDGING_HEADER = StripeiOSTests/StripeiOS Tests-Bridging-Header.h +GCC_PREFIX_HEADER = StripeiOSTests/StripeTests-Prefix.pch diff --git a/Stripe/BuildConfigurations/Stripe-Debug.xcconfig b/Stripe/BuildConfigurations/Stripe-Debug.xcconfig new file mode 100644 index 00000000..c5831ee6 --- /dev/null +++ b/Stripe/BuildConfigurations/Stripe-Debug.xcconfig @@ -0,0 +1,7 @@ +// +// Stripe-Debug.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS-Debug.xcconfig" + +MODULEMAP_FILE = StripeiOS/Stripe.modulemap diff --git a/Stripe/BuildConfigurations/Stripe-Release.xcconfig b/Stripe/BuildConfigurations/Stripe-Release.xcconfig new file mode 100644 index 00000000..662db163 --- /dev/null +++ b/Stripe/BuildConfigurations/Stripe-Release.xcconfig @@ -0,0 +1,7 @@ +// +// Stripe-Release.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS-Release.xcconfig" + +MODULEMAP_FILE = StripeiOS/Stripe.modulemap diff --git a/Stripe/Stripe.xcodeproj/project.pbxproj b/Stripe/Stripe.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1e6707b9 --- /dev/null +++ b/Stripe/Stripe.xcodeproj/project.pbxproj @@ -0,0 +1,2293 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 013F991AB34E38BDBA6E4521 /* STPFormViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44327A2B2C9483F52EE343B /* STPFormViewSnapshotTests.swift */; }; + 0185AC6B123CD73E877D4FCE /* STPPaymentMethodCashAppParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB2CA7E96BE249CE8C0566 /* STPPaymentMethodCashAppParamsTests.swift */; }; + 0305C1689C25B57C43640173 /* STPPaymentMethodBacsDebitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1CF8FB0100664A02468FBC /* STPPaymentMethodBacsDebitTest.swift */; }; + 03E60F9EF24C975AF90E2447 /* StripePaymentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E136A967522048B313E3C62F /* StripePaymentsUI.framework */; }; + 044B7BECFBDB1F6C8CA08514 /* STPSetupIntentConfirmParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC0B1FC92A573AAEA4F4E94 /* STPSetupIntentConfirmParamsTest.swift */; }; + 0684E2ABDA4566356143CC14 /* STPPaymentMethodSofortParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207677E2A0DBC04C88139372 /* STPPaymentMethodSofortParamsTests.swift */; }; + 07A5CDBFDF2340BAD99D6EB3 /* STPCardFormViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D983E089196152DA1C69469 /* STPCardFormViewSnapshotTests.swift */; }; + 07BF3CF1656AF5F5A0678873 /* STPPhoneNumberValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924E878428D15506711CA628 /* STPPhoneNumberValidatorTest.swift */; }; + 08111F4AD3CA0755420E05F7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 147D2DC1FFDFC99269039377 /* LaunchScreen.storyboard */; }; + 08ED7A4EB7E64FDAED2C2D39 /* STPShippingAddressViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789D0B49B0788794739E3DD4 /* STPShippingAddressViewControllerTest.swift */; }; + 093FE3D65978E3DB6B79AE05 /* UIToolbar+Stripe_InputAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2307F2C5E53540D4ACAA1F6 /* UIToolbar+Stripe_InputAccessory.swift */; }; + 0B9C0E9A7A750607413C9E53 /* STPFakeAddPaymentPassViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0669B4CA326CE74D125C789C /* STPFakeAddPaymentPassViewController.swift */; }; + 0C5F4AE769D95AA921F61084 /* STPApplePayPaymentOptionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00985EFC6CB7B912FDBF3813 /* STPApplePayPaymentOptionTest.swift */; }; + 0DFA17378D894C70D72C9F62 /* Error+PaymentSheetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE85C770AEEDBE4AEC93EAA /* Error+PaymentSheetTests.swift */; }; + 0FA3C1494BA57884B5DE3B20 /* Stripe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4259421D2CD26E37B96F97B2 /* Stripe.framework */; }; + 10342D659764A88A695EF38B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DFA7A75BA785EBBE4C05DAA3 /* Images.xcassets */; }; + 124D43C1A633922B1DA3E1E7 /* STPShippingAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71711FC8E2FB66E52A5FDD9A /* STPShippingAddressViewController.swift */; }; + 14656D177E67594B8C75A9FE /* STPConnectAccountParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195DEC752CC82CC4BA1E2351 /* STPConnectAccountParamsTest.swift */; }; + 162C101E57D66F0051164C4A /* Stripe.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4259421D2CD26E37B96F97B2 /* Stripe.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 172D96526023A80534D54CC0 /* STPBankSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81352A0CBE46A59E6B1A712E /* STPBankSelectionViewController.swift */; }; + 17BD7C0391F3182E32A63D6B /* STPAUBECSDebitFormViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBEA9E4823F08C1F5057B5A /* STPAUBECSDebitFormViewSnapshotTests.swift */; }; + 194154708E1A9E013DCE2C72 /* STPPaymentHandlerStubbedMockedFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF48EC440E1ED5D6BAA567FF /* STPPaymentHandlerStubbedMockedFilesTests.swift */; }; + 1948544E75A2E16E46CBA00E /* STPRedirectContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCF9EB77A45F3E9E83F5D8B /* STPRedirectContextTest.swift */; }; + 1A058C42C4703458CA1CA522 /* STPCardNumberInputTextFieldValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C4773C2D193BEDF1CBB530 /* STPCardNumberInputTextFieldValidatorTests.swift */; }; + 1BC4044802EE7D3E2643DC84 /* STPPaymentIntentEnumsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C31CAD0FA74B58BA2B8530 /* STPPaymentIntentEnumsTest.swift */; }; + 1CCFC43F7FCD273E2100D321 /* STPPaymentMethodBancontactTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E5416F6AE8BED88980D6F8 /* STPPaymentMethodBancontactTests.swift */; }; + 1CD3AB315580606AF87A7B1F /* STPPaymentCardTextFieldKVOTest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB08D2AC882B21C8ADD76B92 /* STPPaymentCardTextFieldKVOTest.m */; }; + 1E8D8E2494062262A332879C /* STPCardValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1644AA33E81233EF33022BA /* STPCardValidatorTest.swift */; }; + 1F432D0B37949217E4299A20 /* STPPaymentOptionsInternalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C4A4CC7D2E9B5AB3EC3B79 /* STPPaymentOptionsInternalViewController.swift */; }; + 225140E0BD9C0630116DDE4A /* STPPaymentMethodUSBankAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B112FFF3FCA82094281493F /* STPPaymentMethodUSBankAccountTest.swift */; }; + 22BE2ABB29F77362FF16D945 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C2427C1CDFA85BFC6570F1E9 /* Localizable.strings */; }; + 234C71F480318E9062075924 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BBCE3A905041A709E8F279A /* AppDelegate.swift */; }; + 23CF725CFAB2ABED416BF416 /* STPApplePayContextFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE268457D21A7209862E004 /* STPApplePayContextFunctionalTest.swift */; }; + 23D1246A5DAB5333650F104F /* STPSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E1FED5CE5974C9C1162E93 /* STPSectionHeaderView.swift */; }; + 240993144289CD0DEC2C73C7 /* STPConfirmPaymentMethodOptionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FE1BA89B80336F16924FA2 /* STPConfirmPaymentMethodOptionsTest.swift */; }; + 246920234EE8382FB4E56516 /* STPCardFormViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5E08A1651D9DFE502DA021 /* STPCardFormViewTests.swift */; }; + 26F38A2A57FDDC12926BE044 /* STPAddCardViewControllerLocalizationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E5C1DABAA63F07F0C6AE37 /* STPAddCardViewControllerLocalizationSnapshotTests.swift */; }; + 279D2BA91198E18730626CE6 /* STPUserInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1217AD643A9E8F88B60F645 /* STPUserInformation.swift */; }; + 27F1783CBFEC06BFD6C114F6 /* STPPaymentMethodKlarnaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD94FF270165D699DA89B24 /* STPPaymentMethodKlarnaTests.swift */; }; + 28538CD5885636DC523E8751 /* STPSourceRedirectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0436A8574E7D0730641407A /* STPSourceRedirectTest.swift */; }; + 29428CDB658E6F504402D844 /* STPPaymentMethodBillingDetailsTests+Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0A18C441FCA394BEF6A3D /* STPPaymentMethodBillingDetailsTests+Link.swift */; }; + 2A528B7B2579E5F977797822 /* STPPaymentHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC30E6129279F14506219E98 /* STPPaymentHandlerTests.swift */; }; + 2AC91F23CF3949ADC60D27F7 /* STPThreeDSTextFieldCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A583966A33DCDCF04322A592 /* STPThreeDSTextFieldCustomizationTest.swift */; }; + 2AE9ABA774B430E174279FEA /* stp_test_upload_image.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = FD3398E2352CEA0264F20AEA /* stp_test_upload_image.jpeg */; }; + 2BD45625F6F665B60C6CAD30 /* STPAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A50AFA4603E488FF3D82D0 /* STPAddressViewModel.swift */; }; + 2C6DC246DD12FE0D87156A4D /* STPPaymentMethodPayPalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A63AC868755CB4745E7458E /* STPPaymentMethodPayPalTests.swift */; }; + 2C7991FDF7B374E0E65E253F /* STPPaymentOptionsViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2922A32A754CFC9AB8B48AE /* STPPaymentOptionsViewControllerTest.swift */; }; + 2C9F69E4A384C5743F4EAF69 /* STPPaymentMethodSwishParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9631915F03F157A1CC3FEFFE /* STPPaymentMethodSwishParamsTests.swift */; }; + 2CD7968DA48F7129E16EA0CB /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 911CA85A1610303FA0AF0643 /* OHHTTPStubsSwift */; }; + 2E35B0FB60FCBE7608080642 /* STPPushProvisioningContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6887F19BB9804BF45FD703FF /* STPPushProvisioningContext.swift */; }; + 2EB68A59660A4D1E14799DA4 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 62887B4538E4E41E735685E1 /* OHHTTPStubs */; }; + 2F0FC4E67BE577AD66CD1475 /* StripePaymentSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDC55CC034022DFAC9366E2E /* StripePaymentSheet.framework */; }; + 2F18A1903244E144C7802E09 /* STPCardValidationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9E7B637A8747431B38FD1D /* STPCardValidationState.swift */; }; + 2F9FA9CBCA3C0CE52FAC9B6B /* ConfirmButtonSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806124200E77795DCFC8418E /* ConfirmButtonSnapshotTests.swift */; }; + 2FFA7C2D1C7337FDB4C608A5 /* STPCardScannerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B91C4D5B93FF71C61B140F1 /* STPCardScannerTableViewCell.swift */; }; + 307FD6A103EF7AF3CE451598 /* STPPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D964D9E01627B419B4BD23C /* STPPaymentResult.swift */; }; + 30D48C62B2FA6B28EC23A5BB /* Stripe-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A8598727045C6268B57A5FC7 /* Stripe-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 313F5F792B0BE59100BD98A9 /* Docs.docc in Sources */ = {isa = PBXBuildFile; fileRef = 313F5F782B0BE59000BD98A9 /* Docs.docc */; }; + 315713352C770DA3ED9CBDCD /* Enums+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7E3CE2105E4A39032CD919 /* Enums+CustomStringConvertible.swift */; }; + 3172C789DF2CE133ECA359D7 /* STPPushProvisioningDetailsFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8A6B88797870BC71CCB3AF /* STPPushProvisioningDetailsFunctionalTest.swift */; }; + 319899DEC91B3F88D380DB47 /* STPPaymentMethodGrabPayParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8FF64CA314F909D7EC82FE /* STPPaymentMethodGrabPayParamsTest.swift */; }; + 31CDFC2E2BA3708000B3DD91 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 31CDFC2D2BA3708000B3DD91 /* PrivacyInfo.xcprivacy */; }; + 325694E4284BEAE787A5ECB6 /* NSLocale+STPSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = C641A744CCEA67C07E9BFE05 /* NSLocale+STPSwizzling.swift */; }; + 32874C6147344A9CB2EF4DAD /* Stripe3DS2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33FDC634FD5D79E824240DDC /* Stripe3DS2.framework */; }; + 331924F0801287BAD413FDCB /* STPMandateCustomerAcceptanceParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C713F58BC61A962C720AE0AE /* STPMandateCustomerAcceptanceParamsTest.swift */; }; + 33DF66640B5ABBCB12B46AFE /* STPPaymentMethodCardWalletVisaCheckoutTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E46B18CDDC191934F3D4BE /* STPPaymentMethodCardWalletVisaCheckoutTest.swift */; }; + 35C1CF73701EECC7DB6AB722 /* FormSpecProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C04AFC9CDE50D09D38A3232 /* FormSpecProviderTest.swift */; }; + 35E05040EA813C3B9C8EF054 /* STPViewWithSeparatorSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86983B09A1712944EC012AD4 /* STPViewWithSeparatorSnapshotTests.swift */; }; + 360EEE8B706D2A4A49666F7A /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D1C6EB5826E2D7C80B6CF3 /* StripePayments.framework */; }; + 37E9160706C9EEEFEF133617 /* STPPaymentIntentFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDEAB86BE4711841D426F3B /* STPPaymentIntentFunctionalTest.swift */; }; + 37FBCED5F71F03483EA73F27 /* STPPaymentMethodOXXOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BBCC4D386EE44E809A591C /* STPPaymentMethodOXXOTests.swift */; }; + 385CAC4D2FF119D2E925916B /* STPPostalCodeInputTextFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 917154477796779ECFA1334A /* STPPostalCodeInputTextFieldFormatterTests.swift */; }; + 3930ECBEE003772C1245D25B /* UIViewController+Stripe_ParentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5804C2B9C0704E386B3D25A4 /* UIViewController+Stripe_ParentViewController.swift */; }; + 39B1B88B8506BE4574E6B376 /* STPBankAccountFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989411FA3CD0CCC38BC227F4 /* STPBankAccountFunctionalTest.swift */; }; + 3AAE488F2461A46143B3A687 /* STPPaymentConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 79ABE6A14AF9D14103050876 /* STPPaymentConfigurationTest.m */; }; + 3AD22E0BD44B02D968C6569A /* STPImageLibraryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F546088BA4F763334CFD3D34 /* STPImageLibraryTest.swift */; }; + 3B237145902E3DB07E747E32 /* TextFieldElement+IBANTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BD2CE41E4F0CF648F44E4A /* TextFieldElement+IBANTest.swift */; }; + 3C1A7B9810B038177FF1CF52 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F29C15B47C7CB0941CD4C9E /* ViewController.swift */; }; + 3C1E9069CD03ED9981D7F3E2 /* ConfirmButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFE40AD9D875709F643D2E5 /* ConfirmButtonTests.swift */; }; + 3CE88568CB9648D6F1503B88 /* STPSource+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B75875C55D2C2723DC5090 /* STPSource+BasicUI.swift */; }; + 3EA9D509E59DA65EE4EDF98D /* STPAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39580123A4F1EA96F91768A /* STPAddressTests.swift */; }; + 3EB3745F556EA12AB27A8545 /* APIRequestTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AAB72218409F85FE29E69E /* APIRequestTest.swift */; }; + 3FA556CF8B11E2486F505161 /* UIViewController+Stripe_NavigationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F017A08C7E633FB4297D274 /* UIViewController+Stripe_NavigationItemProxy.swift */; }; + 3FD5ABC45AF3A03F4EFE196F /* STPSourceFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17013F78CE3F9662029FEF5B /* STPSourceFunctionalTest.swift */; }; + 4059301B0365BD4220E591FB /* STPPaymentMethodSwishTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE9F0BAC35EA14579775033 /* STPPaymentMethodSwishTests.swift */; }; + 420F8CAB4FAD6D9AF4AF25C0 /* StripePaymentSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDC55CC034022DFAC9366E2E /* StripePaymentSheet.framework */; }; + 42395EF962DB8AD6A094630B /* StripePaymentSheet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DDC55CC034022DFAC9366E2E /* StripePaymentSheet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 429DBA641E926EBC2D049FE7 /* STPCustomerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB8FCDBC63A79CD1571A2DFB /* STPCustomerContext.swift */; }; + 42F18560F3DC6980408AF051 /* STPPaymentMethodPayPalParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2A766FB355DD9C461939C1 /* STPPaymentMethodPayPalParamsTests.swift */; }; + 43FFF2881D4EFA7B57A60E09 /* STPPaymentMethodCardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD02D8298877F10F2EF2A9D /* STPPaymentMethodCardTest.swift */; }; + 44672917D3AC4B83F9EC3BC3 /* UIView+Stripe_SafeAreaBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86798C95A778362EF815B4C6 /* UIView+Stripe_SafeAreaBounds.swift */; }; + 446A108C8EB6C338A1D774F8 /* STPPaymentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCB69CD3B8C3DAB216A5F0 /* STPPaymentConfiguration.swift */; }; + 447C19BDB2CF5445045F81F7 /* STPPaymentContextAmountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C876DC7F3E31D7189506A8 /* STPPaymentContextAmountModel.swift */; }; + 450FAE41FB4538462D05F2E4 /* LinkSignupViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C7D85A7FAAFDF4F59BA85E /* LinkSignupViewModelTests.swift */; }; + 45FA9B8CC2D18E29BE81CF8F /* STPIntentActionAlipayHandleRedirectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD06AED0AF8A9A7FB4A2E66F /* STPIntentActionAlipayHandleRedirectTest.swift */; }; + 460B31EDB22BD6B912567363 /* STPSourceVerificationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89551702F1F9A3AFF1ED676 /* STPSourceVerificationTest.swift */; }; + 46FF3CC61200F2C27D4F3369 /* STPSourceReceiverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7A947152A728EB2CBC4DB2 /* STPSourceReceiverTest.swift */; }; + 492F7C4DABB4CE8EBE34EEF2 /* STPConnectAccountAddressTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967DDC94B687B14E07842CC8 /* STPConnectAccountAddressTest.swift */; }; + 4935C8B3ECFBAD947E694934 /* STPIntentActionTypeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F205F920E971DEA59E3C31 /* STPIntentActionTypeTest.swift */; }; + 4993037E5386D0AF87B24871 /* STPPaymentMethodAffirmParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87817A1D3D213AA4ADF6A4C /* STPPaymentMethodAffirmParamsTest.swift */; }; + 4A61DC36F10B9C9C24345613 /* STPRadarSessionFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D878F923A1F69B58D6B2812 /* STPRadarSessionFunctionalTest.swift */; }; + 4AAA2CD5AEF1F913395B3B95 /* STPPaymentMethodUSBankAccountParamsStubbedTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF13BAEF86594C9CABD4F42A /* STPPaymentMethodUSBankAccountParamsStubbedTest.swift */; }; + 4B0917FC15BF56D0100E0ED1 /* STPGenericInputTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A53F005EA8FDDAA66126BA /* STPGenericInputTextFieldSnapshotTests.swift */; }; + 4C3B161481D11385352B06D4 /* STPCustomerContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC4B06AB5C02FF54091E5A8 /* STPCustomerContextTest.swift */; }; + 4E09E54E7FEC35C49C59A379 /* STPPushProvisioningDetailsParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B28B8A547CD846277ECD578 /* STPPushProvisioningDetailsParams.swift */; }; + 4E31B1864DA407598FB1BBC6 /* STPPostalCodeInputTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6618739767139C25C05B3631 /* STPPostalCodeInputTextFieldTests.swift */; }; + 4ED44ACF24949F516867235C /* STPPaymentOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6215A9BF343775B1BD0F62AF /* STPPaymentOptionTableViewCell.swift */; }; + 4EFF8B46B12DA4D9AAB22523 /* STPPaymentCardTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B42BE6FB5EC1F708875AB8 /* STPPaymentCardTextFieldCell.swift */; }; + 4FB67F10A0B7106A8142B842 /* STPEphemeralKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588C260880FFC584A00A89F5 /* STPEphemeralKeyManager.swift */; }; + 51044B947A7FDB99451466D8 /* STPGenericInputPickerFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284C67269D2606DA147AE01D /* STPGenericInputPickerFieldSnapshotTests.swift */; }; + 5170651536332C4842E9D009 /* STPPaymentMethodBoletoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E371E9B3B2E343FE954531C /* STPPaymentMethodBoletoTests.swift */; }; + 51D515315F02D4C03BA12366 /* UserDefaults+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0E131538728BC4802627B1 /* UserDefaults+StripeTest.swift */; }; + 5212C7875C07F9BF16AFD98D /* STPAPIClient+PushProvisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807FF966F1DE05F3496B817B /* STPAPIClient+PushProvisioning.swift */; }; + 524AE1978E0A4490D1C390C5 /* CustomerAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C866064B3482878A69892F /* CustomerAdapterTests.swift */; }; + 5302F9246A4A6381CB4FB874 /* StripePaymentsTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77247622AB08FEF48CA0DC26 /* StripePaymentsTestUtils.framework */; }; + 5370700ED1F630E8261507D3 /* STPBankAccountParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C784618A074FF021B3089 /* STPBankAccountParamsTest.swift */; }; + 542610492B38FEB652C6823E /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5398E1156E0BFEBBF56FD2F /* String+Localized.swift */; }; + 54331380F5AC68846DBE94D5 /* UITableViewCell+Stripe_Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5476BD87E0480A93958F0328 /* UITableViewCell+Stripe_Borders.swift */; }; + 58240AA66FAE55131268E4A0 /* STPAddressFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72903593DC432D01720DC9D9 /* STPAddressFieldTableViewCell.swift */; }; + 583DE9869C885BA02E0A071E /* STPAUBECSFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAE5EE11611F9F7762B64C6 /* STPAUBECSFormViewModelTests.swift */; }; + 58A8B3F57FE98C22D8F90C77 /* STPThreeDSButtonCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F3B966470A530E0DC53F8C /* STPThreeDSButtonCustomizationTest.swift */; }; + 590DB84AC15709E3C6F1FC3B /* STPThreeDSNavigationBarCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51408DE266D0345784ADD4FA /* STPThreeDSNavigationBarCustomizationTest.swift */; }; + 5910FCB9822259D5EC7E4051 /* AutoCompleteViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFD2F6A5A046A620BAB75B41 /* AutoCompleteViewControllerSnapshotTests.swift */; }; + 5B6F1BF973FC2D8DD6127B7F /* StripePaymentsUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E136A967522048B313E3C62F /* StripePaymentsUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5C51167CC14F653E7117BA61 /* OCMock in Frameworks */ = {isa = PBXBuildFile; productRef = E804AA8C4156CC85FFD9595F /* OCMock */; }; + 5C5E1CE53D89DE8F0B867115 /* STPPaymentMethodSofortTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8063E8073A32E0B081A1DFA /* STPPaymentMethodSofortTests.swift */; }; + 5D6B52EB4D7258129F134D07 /* STPImageLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93C23F55BEADF9BC74DFBDB /* STPImageLibrary.swift */; }; + 5D7F632025C261B88F0C2016 /* STPFPXBankBrandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C29AE44D809CD677B5E52B /* STPFPXBankBrandTest.swift */; }; + 5D9EB3E2725C38D7098B9965 /* STPPaymentMethodParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4002981AC12687681616D21E /* STPPaymentMethodParamsTest.swift */; }; + 5E498CDA0115CF9F8463C566 /* STPPaymentMethodAUBECSDebitParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF2B9FF57E100301B5C38DB /* STPPaymentMethodAUBECSDebitParamsTests.swift */; }; + 5E5EE69D140F6FEDA5F0A346 /* STPAPIClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DD70E5ED8E9DE8E9752C9E /* STPAPIClientTest.swift */; }; + 5ECED204FD22CFEA3A806767 /* STPPaymentOptionTuple.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD30E5DB8DB3AA3567F5C20 /* STPPaymentOptionTuple.swift */; }; + 605EFBDD21426FD30581563F /* STPAnalyticsClient+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12DBB3F72AEFB52DE27C27ED /* STPAnalyticsClient+BasicUI.swift */; }; + 609C2C8F10AFAA2711639CD0 /* NSArray+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17799DC7FA54E758EED31A6 /* NSArray+StripeTest.swift */; }; + 609E4D384B75F6A111DC0E27 /* STPPaymentActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBAA4B44967B157A4F4E91 /* STPPaymentActivityIndicatorView.swift */; }; + 610DF5DC2B33597500DA6AAA /* HostedSurfaceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610DF5DB2B33597500DA6AAA /* HostedSurfaceTest.swift */; }; + 61152B4F2B866827003B69A0 /* STPPaymentMethodAmazonPayParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61152B4E2B866827003B69A0 /* STPPaymentMethodAmazonPayParamsTests.swift */; }; + 617C1C882BB4992400B10AC5 /* STPPaymentMethodAlmaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617C1C872BB4992400B10AC5 /* STPPaymentMethodAlmaTests.swift */; }; + 617C1C8A2BB4998C00B10AC5 /* STPPaymentMethodAlmaParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617C1C892BB4998C00B10AC5 /* STPPaymentMethodAlmaParamsTests.swift */; }; + 61951FB92B866BA1005F90BE /* STPPaymentMethodAmazonPayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61951FB82B866BA1005F90BE /* STPPaymentMethodAmazonPayTests.swift */; }; + 61E0A0C52BF31D3C00C89786 /* STPPaymentHandlerRefreshTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E0A0C42BF31D3C00C89786 /* STPPaymentHandlerRefreshTests.swift */; }; + 61E1CA1F2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E1CA1E2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift */; }; + 61E1CA212BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E1CA202BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift */; }; + 61E1CA272BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E1CA262BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift */; }; + 62B91808A088C4F9FDB62C53 /* STPEphemeralKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890660C21E3666CE7B82695B /* STPEphemeralKey.swift */; }; + 66065B1D65D7D5502D4E2F2B /* STPCard+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5FB20B2BEFC00D54FDD87D /* STPCard+BasicUI.swift */; }; + 66B7EF2DC1CBF813707C767C /* STPBSBNumberValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C732C25FD961631BD44FDD /* STPBSBNumberValidatorTests.swift */; }; + 68318DB86DFCD19505FC47BA /* NSURLComponents_StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD20E00EAD41091B71ABD5 /* NSURLComponents_StripeTest.swift */; }; + 687517E7FE02FFB96DCE2328 /* STPEphemeralKeyManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C645F78B3EFFAA083B6FD3E9 /* STPEphemeralKeyManagerTest.swift */; }; + 69AC1EDE2A3C03B1D980CA54 /* STPPaymentOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1BB31C7B514984231125B /* STPPaymentOptionsViewController.swift */; }; + 6BA4B91A2BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA4B9192BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift */; }; + 6BA4B91C2BF4343B00D1F21D /* STPPaymentMethodMobilePayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA4B91B2BF4343B00D1F21D /* STPPaymentMethodMobilePayTests.swift */; }; + 6BC5EC2D2B4609FF00CC75E8 /* LinkInlineSignupElementSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5EC2C2B4609FF00CC75E8 /* LinkInlineSignupElementSnapshotTests.swift */; }; + 6BF6ECC4A4E61E2FFC3EA20B /* STPAPIClient+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AFAE24610EC983727F860 /* STPAPIClient+BasicUI.swift */; }; + 6E7AD3CCC966A7F34922B172 /* NSDictionary+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07275F94914B7E7937D24FE /* NSDictionary+StripeTest.swift */; }; + 6EDFC83541EED9E361B71C02 /* STPCustomerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB817ED5B5DB87AE1290894 /* STPCustomerTest.swift */; }; + 6EF3F611E6EA3CB479D62450 /* AfterpayPriceBreakdownViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C742844915B96CFD25BFFF9 /* AfterpayPriceBreakdownViewSnapshotTests.swift */; }; + 6F4FBB4F10B5DB2CF8BB3460 /* STPBinRangeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC0560A312147C37CFE6CF9 /* STPBinRangeTest.swift */; }; + 6F9525063D76A9F86A10CCBF /* STPApplePayContextFunctionalTestExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1272F2E05A0E294DD9ECA26 /* STPApplePayContextFunctionalTestExtras.swift */; }; + 6FCA954C32AB351F902BA876 /* STPPostalCodeValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF7394DDD552EDE996EAD8E /* STPPostalCodeValidatorTest.swift */; }; + 701C464523173C6809544935 /* STPThreeDSUICustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED44491EB0AC72B1B1A773C /* STPThreeDSUICustomizationTest.swift */; }; + 71116C2D5831E271E12DB059 /* ServerErrorMapperTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20879436DCFB1F03BE1608B3 /* ServerErrorMapperTest.swift */; }; + 724429607B2741CF44D9C2E5 /* STPShippingMethodsViewControllerLocalizationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661976D1296DFA48A25E0493 /* STPShippingMethodsViewControllerLocalizationSnapshotTests.swift */; }; + 73AFE2A8839EFAB8330F6CF0 /* STPPaymentIntentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACE8998EAF997A78759E49B5 /* STPPaymentIntentTest.swift */; }; + 7435E6BB6971012A9B0DB52E /* STPErrorBridgeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EAE9A2AE65771403CE57C11 /* STPErrorBridgeTest.m */; }; + 7589E37795D21AB818B0C333 /* STPAnalyticsClient+Payments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD89580A3E41D7167C30B287 /* STPAnalyticsClient+Payments.swift */; }; + 7623057AC6AC5369DCD94E84 /* STPPaymentMethodCardWalletTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC4B62C336EAA05A33FC384 /* STPPaymentMethodCardWalletTest.swift */; }; + 76BC927BC7A591601C1DAB18 /* StripeiOS.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77E846CD56018D8417A3AB95 /* StripeiOS.xcassets */; }; + 77C0FD1BCDA7BBFB88559B44 /* STPPaymentCardTextFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939360978872BBE4334215B1 /* STPPaymentCardTextFieldTest.swift */; }; + 781EC0163AC001C6A66045B6 /* STPMandateDataParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F7AA0B7B86BA5BB2FE92CE /* STPMandateDataParamsTest.swift */; }; + 7844BB705AEB002965EF82B0 /* STPPaymentMethodKlarnaParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E12A0CBFA259A032F7AF0C /* STPPaymentMethodKlarnaParamsTests.swift */; }; + 78641CE4011A1C1EE6E35DC5 /* STPIntentActionPromptPayDisplayQrCodeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9466F23BA8712EA2EDA48BBD /* STPIntentActionPromptPayDisplayQrCodeTest.swift */; }; + 786C30837EAD918EDE52284E /* STPCardBrandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901BE31021AF27DB5D326327 /* STPCardBrandTest.swift */; }; + 78B70C2EE8334F0FA91439CA /* Stripe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4259421D2CD26E37B96F97B2 /* Stripe.framework */; }; + 795F3783D62AB8E2A00DCD05 /* ConsumerSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6312182B5BCAB940D216650 /* ConsumerSessionTests.swift */; }; + 7A9D7D156B5053638F9B21E1 /* STPPaymentMethodThreeDSecureUsageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916DB8789F65D3C1BCB510C0 /* STPPaymentMethodThreeDSecureUsageTest.swift */; }; + 7B9C0D039EA9EF593AEC682D /* STPShippingMethodTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98544B08552407D41D398C68 /* STPShippingMethodTableViewCell.swift */; }; + 7BC98BE168781C5B3EC8A8DB /* STPPaymentMethod+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C1E9B0EE03825DABF6471A /* STPPaymentMethod+BasicUI.swift */; }; + 7D251ABF1EBF65ACA8A4BDD4 /* STPPaymentMethodUSBankAccountParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FDEF9F687C63BADFB96480 /* STPPaymentMethodUSBankAccountParamsTest.swift */; }; + 7D2C0D1BF455625997CBC33B /* STPAPIClientNetworkBridgeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BA8D8467218C7E691C9FAE /* STPAPIClientNetworkBridgeTest.swift */; }; + 7EAA7334372DBC38DF8FA0AA /* STPPinManagementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EBB07171F6FDCE6E20C454A /* STPPinManagementService.swift */; }; + 7F235CD649F6E97E4E7DD180 /* UIView+Stripe_FirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B76DF0FE363F59BF0940A8B /* UIView+Stripe_FirstResponder.swift */; }; + 7F9D08AC5A448C7693162D7D /* STPShippingMethodsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E4B84223DDA131544DBBA7 /* STPShippingMethodsViewController.swift */; }; + 801F417CE53689B95C4A098B /* STPBankAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D103BC590F1E0EC0C31C7B5F /* STPBankAccountTest.swift */; }; + 812682EA323986B8F698FF3C /* STPPaymentMethodParams+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C5AB22D6328E85A6DDF663 /* STPPaymentMethodParams+BasicUI.swift */; }; + 829D43B6705D125FEC9926DA /* STPPaymentContextApplePayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C995125252BED1EEC018B9D /* STPPaymentContextApplePayTest.swift */; }; + 8378F2A4B0796819BB1C6C54 /* STPPaymentMethodCardParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1671EC46C713D51013AD7D8B /* STPPaymentMethodCardParamsTest.swift */; }; + 8520A27C204A068C43592024 /* StripeApplePay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52F8AEC50D4623F80F04A533 /* StripeApplePay.framework */; }; + 8532FEBF4F2E0EB282D466CE /* STPGenericInputPickerFieldValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D63B73C5773432CA134D1FC /* STPGenericInputPickerFieldValidatorTest.swift */; }; + 86BAF121184D71F5F4FFAD7B /* STPPaymentCardTextFieldTestsSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F16C36797D978E72E612100 /* STPPaymentCardTextFieldTestsSwift.swift */; }; + 8AA84A3A52A3D79BCA8C8994 /* STPUIVCStripeParentViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88639E3C3AC622B5EF475538 /* STPUIVCStripeParentViewControllerTests.swift */; }; + 8B80FB6FC88D411A90E9D487 /* WalletHeaderViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB03E83746FE78361831546 /* WalletHeaderViewSnapshotTests.swift */; }; + 8C977F8D224A7360AE8E15A7 /* STPPaymentMethodBoletoParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C67AC5D415615E9F27D3E3 /* STPPaymentMethodBoletoParamsTests.swift */; }; + 8E423294AB602BF25DB11D8E /* OperationDebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61763BA2CCA86F9B8FD4F1F /* OperationDebouncerTests.swift */; }; + 8EC1820299152F8565D30A40 /* STPCardCVCInputTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1859673CAD068B345F5DD7D /* STPCardCVCInputTextFieldSnapshotTests.swift */; }; + 8F0326E98C74EB62E34B9FEA /* STPIntentActionWeChatPayRedirectToAppTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5D11C977A95B8E936E907 /* STPIntentActionWeChatPayRedirectToAppTest.swift */; }; + 8F5AF9D3566B8DBCA5AB5188 /* STPPaymentHandlerFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF450AD17FF7BCE5916DDF1 /* STPPaymentHandlerFunctionalTest.swift */; }; + 903FFB756C6ED520BE38EF6F /* STPInputTextFieldValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE395C4DD0E0112AF3720C /* STPInputTextFieldValidatorTests.swift */; }; + 91558F51B87C72E745244958 /* STPPostalCodeInputTextFieldValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9975553E669AF69F3CE437 /* STPPostalCodeInputTextFieldValidatorTests.swift */; }; + 91A839DEDA7D1EAF6FC66BE0 /* STPFormEncoderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30964128998473CAA9F2DD7E /* STPFormEncoderTest.swift */; }; + 922C0DF37F5AAA29375A5454 /* STPSourceOwnerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095EBF095BA2BC8D299547DB /* STPSourceOwnerTest.swift */; }; + 9291A08CCB34504FCA4B7481 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 005650A59D692F820EF20F5F /* XCTest.framework */; }; + 9363F8F389C04C19B37D0F0A /* StripePaymentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E136A967522048B313E3C62F /* StripePaymentsUI.framework */; }; + 951344464ACF84F0F6D43D10 /* OneTimeCodeTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F9CB667BC68767DFB5FACD /* OneTimeCodeTextFieldTests.swift */; }; + 9535CADFFBC9E1FA291E947E /* STPPIIFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D9793463E266501B74FD /* STPPIIFunctionalTest.swift */; }; + 96098727EFA6A72087A35A52 /* STPFormTextFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E1862744F23286D1FB9D4AE /* STPFormTextFieldTest.swift */; }; + 97756805F41DDB51B3ED0326 /* STPPaymentContextSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B86F3355E44DF4A980B82C /* STPPaymentContextSnapshotTests.swift */; }; + 98E2332DE7F54E970BE5EEF7 /* UIBarButtonItem+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3000668A75E095B514241F /* UIBarButtonItem+Stripe.swift */; }; + 98EE8326C1D133E1C998114F /* STPLocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFF957F38AABE5F748C38C0B /* STPLocalizedString.swift */; }; + 9A24970C5FB6D3F7314AE550 /* STPAPIClientStubbedTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4194E605BB5F31E9CBB8F96 /* STPAPIClientStubbedTest.swift */; }; + 9A57C50938A66604FF16A882 /* STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5062915D961C1BCAFD641FFE /* STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift */; }; + 9B149DA42FB38C3542E0CB4B /* STPApplePayFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C77C7BC4BA57EC296CF2F1C /* STPApplePayFunctionalTest.swift */; }; + 9B1AC278FDCDABF26C5E468C /* STPPostalCodeInputTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090EF7D598B8DE779C275395 /* STPPostalCodeInputTextFieldSnapshotTests.swift */; }; + 9C13E8A017A4E23BCCDE618B /* UINavigationController+Stripe_Completion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C078573F46762353664AC92 /* UINavigationController+Stripe_Completion.swift */; }; + 9D464A252FBD0D4E2A0A7398 /* STPCountryPickerInputFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D49257A97E71A475A9F6E08 /* STPCountryPickerInputFieldSnapshotTests.swift */; }; + 9D8354BDB04CEC5D1EFCF54F /* STPSwiftFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = E315168EF07F52B733EA77F8 /* STPSwiftFixtures.swift */; }; + 9D9692DFC4F06F8C70145000 /* UINavigationBar+Stripe_Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E369CEC9F5B3758F78E88F /* UINavigationBar+Stripe_Theme.swift */; }; + 9FB20E559379F468070C7B50 /* STPLabeledFormTextFieldViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78D1EEABBAE5BD5615486B0F /* STPLabeledFormTextFieldViewSnapshotTests.swift */; }; + 9FD92B3ADEBEC96660B70409 /* STPMocks.m in Sources */ = {isa = PBXBuildFile; fileRef = 88AEABC15CCBB9EA393C175F /* STPMocks.m */; }; + A01BB7F09134F7081679F9C4 /* STPPaymentMethodUPIParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E5E1173A37AABB07FB68AB /* STPPaymentMethodUPIParamsTest.swift */; }; + A08C2F0E7F642515B1D263ED /* STPPaymentMethodTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148C1D7D1BBBC6B74894A869 /* STPPaymentMethodTest.swift */; }; + A0AA0B8AEF5B429858D71F6B /* STPBlocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B42EBAC0DC7ED0D9200DB7 /* STPBlocks.swift */; }; + A12CFA90DAE8BBB39A8C7AA1 /* STPCardFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38184A7CD27B978DFA30E69 /* STPCardFunctionalTest.swift */; }; + A22D548084E7DE1FE5ABE8E7 /* STPLabeledMultiFormTextFieldViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F68637E75142DCD46710796 /* STPLabeledMultiFormTextFieldViewSnapshotTests.swift */; }; + A66C279957B6AC8F72DE05C7 /* STPCardExpiryInputTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75157665428685C7A4FD20 /* STPCardExpiryInputTextFieldSnapshotTests.swift */; }; + A77C5769B20D7884FC8FC4FB /* STPNumericDigitInputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6223E57D3A198F956A37ED89 /* STPNumericDigitInputTextFormatterTests.swift */; }; + A781FB0F586B26655FAEC3C0 /* STPCertTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D51B04D83D4FEF7F90DF16A /* STPCertTest.swift */; }; + A8B0DB753CAA2223C8BED099 /* StripeErrorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AD586DDED620B9E68F461 /* StripeErrorTest.swift */; }; + A930DF2880EAD0CB9096E49E /* STPShippingAddressViewControllerLocalizationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED6BAC91E8A827DCDB38B15 /* STPShippingAddressViewControllerLocalizationSnapshotTests.swift */; }; + AC35943F1EAD50E9D5D509B3 /* STPCardExpiryInputTextFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40BB87E28719FE0C6B946BB5 /* STPCardExpiryInputTextFieldFormatterTests.swift */; }; + AC7C127B11A60222465F4696 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 005650A59D692F820EF20F5F /* XCTest.framework */; }; + ACC1B91FC687AFD0DFD27CD4 /* STPIntentActionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33B9D01D037909D1C9C0B617 /* STPIntentActionTest.swift */; }; + ACF6CFE0F8B88FDBBB16968C /* FraudDetectionDataTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FF7A07CFC3B9B7AD6B49EE /* FraudDetectionDataTest.swift */; }; + AD09F2ACD0CDCBD414AC30AD /* STPAPISettingsObjCBridgeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B669C53A601CD3CB0203A4B9 /* STPAPISettingsObjCBridgeTest.m */; }; + AD9B9F3FF697D4A3892E86F2 /* PaymentAnalyticTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F8FCC84601E4ADC6B7F3CE /* PaymentAnalyticTest.swift */; }; + AE747ADA2841AA06F32558D8 /* STPSourceCardDetailsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63114D0EAAE2606732DF5AA0 /* STPSourceCardDetailsTest.swift */; }; + AF18D569B296BFC1EB5A7338 /* ImageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF368BCD5990EE5DC17D299 /* ImageTest.swift */; }; + AF23CB4EF17E87007CFC3E96 /* STPFPXBankStatusResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA08DCDD421CE92ECB61EF5C /* STPFPXBankStatusResponse.swift */; }; + AF44725558E654548FED2A2B /* STPPaymentMethodUPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940209E5D30E86E856016906 /* STPPaymentMethodUPITests.swift */; }; + B00F7FC372E376C6B2170D37 /* STPPaymentContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C750C2C4AB33BC232D1592BA /* STPPaymentContext.swift */; }; + B1BF689B91D538BDCA4C8578 /* STPCoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FC9ED423D40C88D5A24441 /* STPCoreViewController.swift */; }; + B359F6DCB31EAD0814AD9AFD /* STPPaymentMethodSEPADebitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41F721AEBB942BB81408A59 /* STPPaymentMethodSEPADebitTest.swift */; }; + B44E4CF6C65522F80C946775 /* STPPaymentMethodFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8192839B0F1AE9D9F2A94504 /* STPPaymentMethodFunctionalTest.swift */; }; + B4719234E4BBDAD260E31373 /* STPPaymentCardTextFieldViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DEE912364C9F4B51B374D0 /* STPPaymentCardTextFieldViewModelTest.swift */; }; + B6656829DEC006DBEED2AA0E /* STPEphemeralKeyTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BEAA15B53AC5662A33D0E1 /* STPEphemeralKeyTest.swift */; }; + B6784B7F4B9B04617C0EE510 /* PKAddPaymentPassRequest+Stripe_Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273EE407039913F0B644172B /* PKAddPaymentPassRequest+Stripe_Error.swift */; }; + B6EC2F572B618A3D00FF72A2 /* STPBasicUIAnalyticsSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC2F562B618A3D00FF72A2 /* STPBasicUIAnalyticsSerializer.swift */; }; + B71F04D02538FA1723558C48 /* STPPaymentMethodiDEALTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B83930B631CF8EADFB606D6 /* STPPaymentMethodiDEALTest.swift */; }; + B795A5EB8FDECA1060A9655C /* iOSSnapshotTestCase in Frameworks */ = {isa = PBXBuildFile; productRef = C55551F29B99CF6D6DD9EE2F /* iOSSnapshotTestCase */; }; + B82859A4444B9F735720F232 /* STPMandateOnlineParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58AC5E2E0A68221260FD44 /* STPMandateOnlineParamsTest.swift */; }; + B8385576DC25BDEEB92D812F /* STPEphemeralKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485E747DA1F72F091986787B /* STPEphemeralKeyProvider.swift */; }; + B86EE8C85E6AB6B0A34C1887 /* STPTokenTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F090B8D315E7FD12A5F9C09 /* STPTokenTest.swift */; }; + B8ED1F697519A6FCD3D79431 /* STPPaymentMethodGiropayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DB466066B47C0F716E474 /* STPPaymentMethodGiropayTests.swift */; }; + B917BF282C84507292112B9D /* STPCardBINMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1C5E08678292561255B1C5 /* STPCardBINMetadataTests.swift */; }; + B98D71ED9ACC2E1B47372F53 /* NSDecimalNumber+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20552E792B8E7BA15821AB5D /* NSDecimalNumber+StripeTest.swift */; }; + BAFD06E994739E1C38DFFBBC /* STPCardScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69BD038947E8E2376A0D240B /* STPCardScanner.swift */; }; + BB46077C256C26418420F240 /* STPAddCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA793904C7B2D3AA0A4D5EFB /* STPAddCardViewController.swift */; }; + BBB734F006FAD749678B87D1 /* STPPaymentMethodRevolutPayParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE7BEADD3824A06C2994854 /* STPPaymentMethodRevolutPayParamsTests.swift */; }; + BC6912C0DE15008C8D8C303C /* STPFloatingPlaceholderTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7385193226663A5B79E69ED /* STPFloatingPlaceholderTextFieldSnapshotTests.swift */; }; + BC694A1642DC30D530B60635 /* RotatingCardBrandsViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA32D0C9E8A7A69F4899EDC /* RotatingCardBrandsViewSnapshotTests.swift */; }; + BEC0435570B9199B918ED4DA /* STPAPIClient+LinkAccountSessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E956CFA6317CAA8B41E217CA /* STPAPIClient+LinkAccountSessionTest.swift */; }; + BEC5B2ACC54FB72DEBFB70AB /* STPPaymentMethodBancontactParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655209238C85F466F9F14F14 /* STPPaymentMethodBancontactParamsTests.swift */; }; + BF4A92064926FD7B0E3E92F7 /* StripePayments.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22D1C6EB5826E2D7C80B6CF3 /* StripePayments.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BF4ED4828114E2E89A3D4AB7 /* StripeBundleLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D31B0D7BD9F97AF3BB61E6 /* StripeBundleLocator.swift */; }; + C035D82D7096E3005858848C /* StripeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43B4E4B85C598D7A9AFCB4D4 /* StripeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C0688E067AE4FFDFFDDC03BB /* PKPaymentAuthorizationViewController+Stripe_Blocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D9F97ABC88302478220267 /* PKPaymentAuthorizationViewController+Stripe_Blocks.swift */; }; + C0B59D0A7025A55ECD948D47 /* STPPaymentMethodNetBankingParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41081066DC4465734F7FCD7 /* STPPaymentMethodNetBankingParamsTest.swift */; }; + C1E70FD29BBE36D76A7E6929 /* CardExpiryDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153071C69A0BEE033E035DCF /* CardExpiryDateTests.swift */; }; + C314A5C55064C51C2B999E6B /* STPBackendAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B782E1D974A4C131E60E2BD /* STPBackendAPIAdapter.swift */; }; + C32D7ACEBC852CBC295BBEF2 /* STPSetupIntentFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AA313E068FB99CEAA5F7D3 /* STPSetupIntentFunctionalTest.swift */; }; + C34D0BBDF6553ACF85204ACD /* STPPaymentMethodAfterpayClearpayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDD1E823223F450193E8746 /* STPPaymentMethodAfterpayClearpayTest.swift */; }; + C35CF837D67AE8DB7CBDAD98 /* STPThreeDSLabelCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDD4956E0A04D33F0856F31 /* STPThreeDSLabelCustomizationTest.swift */; }; + C3AAA4AFEE274B27D3483876 /* STPPaymentMethodFPXTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEE347A6372AAE2735FAD6F /* STPPaymentMethodFPXTest.swift */; }; + C4C1295E7DA618DFB944A534 /* NSString+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F5F35DB97D8A176FB6ED24 /* NSString+StripeTest.swift */; }; + C4DC3F4FA93A3BAF6EE782A0 /* PKPayment+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D12508C2F1056A7EAFEC86 /* PKPayment+StripeTest.swift */; }; + C57BDA835AED735321906977 /* STPCardExpiryInputTextFieldValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4418164D75002AE6A0273176 /* STPCardExpiryInputTextFieldValidatorTests.swift */; }; + C5D295FE9988CA80ABA57801 /* RotatingCardBrandsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12632E6710DE8861CAF1BAA4 /* RotatingCardBrandsViewTests.swift */; }; + C7EB8FB325BF491FDE25FE66 /* STPPaymentMethodEPSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44B4366D6C4FD4B11662C8 /* STPPaymentMethodEPSTests.swift */; }; + C8226E24CA51133091131391 /* STPConfirmCardOptionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD77D4B1C5B64E45F9DA09B5 /* STPConfirmCardOptionsTest.swift */; }; + C8490E55B1F2EB836144F91C /* STPConnectAccountFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97018A201D01A8FA59999C2 /* STPConnectAccountFunctionalTest.swift */; }; + C861BB9EAAD04949E338D7FF /* PaymentTypeCellSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9A4A2B0FB9F8C743BBED48 /* PaymentTypeCellSnapshotTests.swift */; }; + C9E66A22494C02050AE34A9B /* FBSnapshotTestCase+STPViewControllerLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180CF848E3ABF0236C494D8B /* FBSnapshotTestCase+STPViewControllerLoading.swift */; }; + CA189278AD606BEAC62D545F /* STPPaymentIntentParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF902DC49DD90860BD0E5E80 /* STPPaymentIntentParamsTest.swift */; }; + CA4F392070740C56FE2BB461 /* STPStringUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6AE83989B0596F0C111E13 /* STPStringUtilsTest.swift */; }; + CAC3A0342C2F176A007BC888 /* STPPaymentMethodSunbitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC3A0332C2F176A007BC888 /* STPPaymentMethodSunbitTests.swift */; }; + CAC3A03E2C2F2CD8007BC888 /* STPPaymentMethodBillieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC3A03D2C2F2CD8007BC888 /* STPPaymentMethodBillieTests.swift */; }; + CAC80EBF2C33339D001E3D0D /* STPPaymentMethodSatispayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC80EBE2C33339D001E3D0D /* STPPaymentMethodSatispayTests.swift */; }; + CB5AADE45B7B7A40514C054B /* StripeApplePay.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 52F8AEC50D4623F80F04A533 /* StripeApplePay.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CBAF9C6F87F746F17495ADC2 /* STPPaymentMethodCashAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDE50CBC86AD77084C877B6 /* STPPaymentMethodCashAppTests.swift */; }; + CBCA59D39B30D869B4FDC04B /* STPE2ETest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C1548BA518F7AC2A9ECF9D5 /* STPE2ETest.swift */; }; + CC072EBAD035AA54A2AD3ABC /* UIViewController+Stripe_KeyboardAvoiding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F08757CA6F6B2DA65C14E0A /* UIViewController+Stripe_KeyboardAvoiding.swift */; }; + CEE483EB7B06B3C607BC755C /* UINavigationBar+StripeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E68F6B90F3BC61A49570FAF4 /* UINavigationBar+StripeTest.m */; }; + CEF318C74D2E44C78EF85306 /* STPBankSelectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458F8576215E0F8ECE1D74CE /* STPBankSelectionTableViewCell.swift */; }; + CF2E17AC77EB08393B8A3F98 /* STPCardNumberInputTextFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3803D0DED98501AA26B2EAC /* STPCardNumberInputTextFieldFormatterTests.swift */; }; + CFC1F2B8D48FFF7B0F81B5A0 /* STPPaymentMethodAddressTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB3F9F0228008BE213706DF /* STPPaymentMethodAddressTest.swift */; }; + CFDD431A9A8A82BAA11AE5BF /* Stripe3DS2.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 33FDC634FD5D79E824240DDC /* Stripe3DS2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0342D50F9AC319919D93D59 /* STPBECSDebitAccountNumberValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23997D61DF41CA84BFC33080 /* STPBECSDebitAccountNumberValidatorTests.swift */; }; + D0C81317E0AA8EB0370B1BA1 /* LinkLegalTermsViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835CB781FBC19773ACC20676 /* LinkLegalTermsViewSnapshotTests.swift */; }; + D15160C0F0763078DBB434E4 /* STPCardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D575C31524E596E9C1A8E9B /* STPCardTest.swift */; }; + D151C8724925DCBA4BA4F46A /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 512A0E7C246D5F044245E069 /* StripeCoreTestUtils.framework */; }; + D2869246B446B8B31F1CD368 /* STPIntentActionLinkAuthenticateAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB24EC81CE2C8D1C863B044 /* STPIntentActionLinkAuthenticateAccount.swift */; }; + D2C062CE4E54094B1AC33E78 /* STPCardCVCInputTextFieldValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42F83F785EAF24F5DC7ED1A /* STPCardCVCInputTextFieldValidatorTests.swift */; }; + D375ADBD1F4B48380D5347D1 /* CircularButtonSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46AC0B5EC7433E081825D31B /* CircularButtonSnapshotTests.swift */; }; + D3D654D8376AAA634466D31D /* STPSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05AA6A1B2A462F1CE2F537C5 /* STPSourceTest.swift */; }; + D4602454AC17D3584BA88217 /* STPPaymentMethodEPSParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF93C4B7A5F8FA4E7919794F /* STPPaymentMethodEPSParamsTests.swift */; }; + D53C04A27B6B8EFB70E236A7 /* STPCardParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3F2B714DB3D1DED561A7EF /* STPCardParamsTest.swift */; }; + D54508ED433792AD8AA6610F /* STPPaymentMethodRevolutPayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7963CC618A1A1346EC20C7 /* STPPaymentMethodRevolutPayTests.swift */; }; + D567569568C0D8F2D7B179B3 /* STPPaymentMethodAUBECSDebitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54426CBF6F77ABEFBDFDA8C4 /* STPPaymentMethodAUBECSDebitTests.swift */; }; + D73B7A0C24EDCA415FFBBB18 /* StripeUICore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D794C5E6396B4A19DC4F6921 /* StripeUICore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D76D24F6A94108853BB08712 /* STPFileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E0CA28591EB0748C64D1FA /* STPFileTest.swift */; }; + D776B91F0E8E6CCB6C09AC4F /* STPFileFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1264C4DCB32B1FA5CE19201 /* STPFileFunctionalTest.swift */; }; + D7956073A8FD3785193E0577 /* STPStackViewWithSeparatorSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E62BB62EA9B782778CA880 /* STPStackViewWithSeparatorSnapshotTests.swift */; }; + D7C555B36C282B99E22B8D45 /* STPInputTextFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A22E5B87755C1F05C3DB438C /* STPInputTextFieldFormatterTests.swift */; }; + D7D24DCC9402153965AF7F1B /* STPPaymentMethodPrzelewy24Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6558C62376C2397030BD4A6 /* STPPaymentMethodPrzelewy24Tests.swift */; }; + D83F76F584BC345CFBA71CF8 /* OneTimeCodeTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D85FA7B714BDD8D1FD83B75 /* OneTimeCodeTextFieldSnapshotTests.swift */; }; + D85C432B241FDE23875037F9 /* UserDefaults+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26302721419496F37DE91DF8 /* UserDefaults+Stripe.swift */; }; + D8BECFB70834CC42BA6706D8 /* STPSourceParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6CB4B8FAD14B4D70A63595 /* STPSourceParamsTest.swift */; }; + DC57D2DC40C6BA0C9CF7EC92 /* PayWithLinkButtonSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF342CBC167F9CAB5B49CC32 /* PayWithLinkButtonSnapshotTests.swift */; }; + DCF615643A22D0A7B739547C /* Stripe+Exports.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70DF0B659009041F485EE0F /* Stripe+Exports.swift */; }; + DD16FC7ABCA7817794ECC407 /* STPThreeDSSelectionCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23D612FD5AD7772E1B30DCC /* STPThreeDSSelectionCustomizationTest.swift */; }; + DD8E2B99BAE917F83258DC35 /* STPPaymentMethodOptionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2638F7AA0906914117C2D5 /* STPPaymentMethodOptionsTest.swift */; }; + DDBF5AAE607C698618DDE865 /* STPCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8308B7250B642D19827D8 /* STPCameraView.swift */; }; + DE23FEF74E860620A334FDF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 884C01B087B4D820395BD374 /* Main.storyboard */; }; + DF73457BF349BC962A6AC502 /* STPCoreScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1525AF65BDEF691F8BCBE8 /* STPCoreScrollViewController.swift */; }; + DF85F5EC6E16CAD21491891A /* AnalyticsHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AF6E95FE0DD913204CAB32 /* AnalyticsHelperTests.swift */; }; + E0F011E9C6CA368EF87F8E28 /* STPPaymentMethodBillingDetailsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0E24B5689732EB9106DA232 /* STPPaymentMethodBillingDetailsTest.swift */; }; + E2790AB17C8C65CDE1E81532 /* STPPaymentMethodPrzelewy24ParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6269E77F81C32A5EC8BE412 /* STPPaymentMethodPrzelewy24ParamsTests.swift */; }; + E3E916EB10E19727D6B33081 /* STPPaymentMethodOXXOParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC9D4B0158266B01840AD9A /* STPPaymentMethodOXXOParamsTests.swift */; }; + E3F1BAD22CC6E90B761B0502 /* STPTextFieldDelegateProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8A2CD759D465290066EF65 /* STPTextFieldDelegateProxyTests.swift */; }; + E63B5BAF6B5645C979BFBA71 /* STPAddress+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498C3FB07CFD532779C755D3 /* STPAddress+BasicUI.swift */; }; + E699508F4DB4D9D4666BAA08 /* STPSetupIntentLastSetupErrorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B057D99A14E5BA6019C349 /* STPSetupIntentLastSetupErrorTest.swift */; }; + E6F428CFAD64979A8874B00B /* STPAnalyticsClientPaymentsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ACB4FAFAD33296DE34D036 /* STPAnalyticsClientPaymentsTest.swift */; }; + E97168F37D769524B58461B6 /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43B4E4B85C598D7A9AFCB4D4 /* StripeCore.framework */; }; + E9A2C6E153CB480891846705 /* STPPaymentMethodGiropayParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F372EDF9C2C45E1CA2C76866 /* STPPaymentMethodGiropayParamsTests.swift */; }; + E9C690F3629C0AC3CD0260AF /* StripePaymentsObjcTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D237177A7F99EB0F5F4F5E4 /* StripePaymentsObjcTestUtils.framework */; }; + EA34719659CB9F1A269FECC7 /* StripeUICore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D794C5E6396B4A19DC4F6921 /* StripeUICore.framework */; }; + EA571ECEFDDF10AF87CE2B74 /* STPThreeDSFooterCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483B243268646AE65B06E98C /* STPThreeDSFooterCustomizationTest.swift */; }; + EA7FEC518AA07BA59405A5E3 /* STPPaymentIntentParams+BasicUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F01150CD0255164FE2CF3A4 /* STPPaymentIntentParams+BasicUI.swift */; }; + EA80A8DB806DEF4F519059CB /* STPSourceSEPADebitDetailsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21780C410D22264B7C299520 /* STPSourceSEPADebitDetailsTest.swift */; }; + EBD436689635CC28A24DECD4 /* STPPinManagementServiceFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336CC555B845DED30208D39D /* STPPinManagementServiceFunctionalTest.swift */; }; + EC4DC8E386544959E1AA9355 /* STPSetupIntentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8B8F540CD05B3DC2C5EEA6 /* STPSetupIntentTest.swift */; }; + EEA502DF8809B8FD0D00785E /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D1C6EB5826E2D7C80B6CF3 /* StripePayments.framework */; }; + EEB5E5E9C4E06B148A91C7BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6955B3A3353F8442E4FBBBF6 /* Assets.xcassets */; }; + EEBA9A95E8057A06E5E7C103 /* STPCardNumberInputTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD289E1EA9F0CE1C848AC0BB /* STPCardNumberInputTextFieldSnapshotTests.swift */; }; + EEFFE199D9769FF449BFD7FF /* STPCoreTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B78C72B0DB434EC7F700FDE0 /* STPCoreTableViewController.swift */; }; + F06EAD0F48302B061ED29E61 /* STPPaymentMethodCardWalletMasterpassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 533538E3EB92E326CCB95506 /* STPPaymentMethodCardWalletMasterpassTest.swift */; }; + F10FC337254A34ED8F13E341 /* STPPaymentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B694A39D54886392AA5DE3 /* STPPaymentOption.swift */; }; + F2655328479314A9C8718DE4 /* STPApplePayContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F7AB40A5A10C2D267323ABE /* STPApplePayContextTest.swift */; }; + F35E090A607EB5F86FFC3D31 /* STPCardCVCInputTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A7104C1C470515616E4D2B /* STPCardCVCInputTextFieldTests.swift */; }; + F49D9C4030829D13A6EB45BE /* MockFiles in Resources */ = {isa = PBXBuildFile; fileRef = FE2DED6ABA7407C17C1391B6 /* MockFiles */; }; + F53E04785DB804EA5C2AAC18 /* STPAddressViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D2DB08C335695B705F544C /* STPAddressViewModelTest.swift */; }; + F550D4EB3DCFE03D6FC8F023 /* STPIntentActionPayNowDisplayQrCodeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A517686D4D12691351311CA /* STPIntentActionPayNowDisplayQrCodeTest.swift */; }; + F5CC4F320D09A06F0B21ABE6 /* STPPaymentMethodAffirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DF2ED9232A7CC51F5FCB1 /* STPPaymentMethodAffirmTests.swift */; }; + F729E784CFFC1F79EF5F2ABE /* STPPaymentIntentLastPaymentErrorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7B8DACB0A7294BC235E3BC /* STPPaymentIntentLastPaymentErrorTest.swift */; }; + F835CEC935464FF32726A0A0 /* STPTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E8CA4361964E1BA400EFC89 /* STPTheme.swift */; }; + F86F2DF6E46EFABE23AD5D27 /* STPApplePayContextDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E452877E5D11120B1E28A6E7 /* STPApplePayContextDelegate.swift */; }; + F975CE029DF30419B8DB0D8F /* STPNumericStringValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA36705DED9164663A98B6A /* STPNumericStringValidatorTests.swift */; }; + FB34906C9215D0E03850064B /* STPAddCardViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8704ABFA91A5226847F4A69A /* STPAddCardViewControllerTest.swift */; }; + FBBA3B39598BBECB664C5E7F /* STPApplePayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC4BC1AB047ED88C4D13C89 /* STPApplePayTest.swift */; }; + FC166455478EAF51F7C34E68 /* STPApplePayPaymentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACDC7EEC28D1FE50008F65 /* STPApplePayPaymentOption.swift */; }; + FDADE3E36804A8AD82301BF3 /* STPPaymentMethodAfterpayClearpayParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBA5B09A3FD875C93218573 /* STPPaymentMethodAfterpayClearpayParamsTest.swift */; }; + FDD1858CAEFCEBB22BEC9BBC /* MKPlacemark+PaymentSheetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCCA0E8A02B4F4B23837FB4 /* MKPlacemark+PaymentSheetTests.swift */; }; + FE6647242714D9BEA1EBC055 /* STPCardCVCInputTextFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1F6514E7530C2A3478B2F5 /* STPCardCVCInputTextFieldFormatterTests.swift */; }; + FE7C38B95B3B7E028AB21238 /* STPPaymentMethodCardChecksTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15BBFFA401852A8719E3DDD /* STPPaymentMethodCardChecksTest.swift */; }; + FEE74744B657F86873EA2F3D /* STPPushProvisioningDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E5DA3029F141B5111A5B2C /* STPPushProvisioningDetails.swift */; }; + FEF2E0DAC862FF42B814AFCA /* STPPaymentHandlerFunctionalTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 683F7735569D22CBEC9CA2E6 /* STPPaymentHandlerFunctionalTest.m */; }; + FF0F9BA6FE4B88297A434EA7 /* STPPaymentMethodNetBankingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739934737B9A09775CD278C9 /* STPPaymentMethodNetBankingTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 32221E5BA07FB5AA4EBFE81C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E63832AA5BB4225708B7C838 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1628E8B14F1F7C63CF8C9962; + remoteInfo = StripeiOSTestHostApp; + }; + 8F25D3DFD6D65DAE4B581911 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E63832AA5BB4225708B7C838 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADF894AA8F6022D9BED17346; + remoteInfo = StripeiOS; + }; + D90CE98566BBDA0E7340E1D7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E63832AA5BB4225708B7C838 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ADF894AA8F6022D9BED17346; + remoteInfo = StripeiOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 16FB342935F756BD2EA7CE4C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CFDD431A9A8A82BAA11AE5BF /* Stripe3DS2.framework in Embed Frameworks */, + CB5AADE45B7B7A40514C054B /* StripeApplePay.framework in Embed Frameworks */, + C035D82D7096E3005858848C /* StripeCore.framework in Embed Frameworks */, + 42395EF962DB8AD6A094630B /* StripePaymentSheet.framework in Embed Frameworks */, + BF4A92064926FD7B0E3E92F7 /* StripePayments.framework in Embed Frameworks */, + 5B6F1BF973FC2D8DD6127B7F /* StripePaymentsUI.framework in Embed Frameworks */, + D73B7A0C24EDCA415FFBBB18 /* StripeUICore.framework in Embed Frameworks */, + 162C101E57D66F0051164C4A /* Stripe.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5789BE2CD297329ED86678C0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 9F30C6D40956861F588C2CD0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + CAAA5D4C8E87896995960E8C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 002634603200AABECC9686B1 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + 005650A59D692F820EF20F5F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 00985EFC6CB7B912FDBF3813 /* STPApplePayPaymentOptionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayPaymentOptionTest.swift; sourceTree = ""; }; + 01B057D99A14E5BA6019C349 /* STPSetupIntentLastSetupErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSetupIntentLastSetupErrorTest.swift; sourceTree = ""; }; + 01B42BE6FB5EC1F708875AB8 /* STPPaymentCardTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentCardTextFieldCell.swift; sourceTree = ""; }; + 02FC9ED423D40C88D5A24441 /* STPCoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCoreViewController.swift; sourceTree = ""; }; + 03ACDC7EEC28D1FE50008F65 /* STPApplePayPaymentOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayPaymentOption.swift; sourceTree = ""; }; + 04838ACE779F5CC949C276CB /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 04FE0C74090AE8A871CCE5EC /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mt; path = mt.lproj/Localizable.strings; sourceTree = ""; }; + 05AA6A1B2A462F1CE2F537C5 /* STPSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceTest.swift; sourceTree = ""; }; + 05FE1BA89B80336F16924FA2 /* STPConfirmPaymentMethodOptionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPConfirmPaymentMethodOptionsTest.swift; sourceTree = ""; }; + 064CFCA8FCEA9E4BAB3547D0 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + 0669B4CA326CE74D125C789C /* STPFakeAddPaymentPassViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFakeAddPaymentPassViewController.swift; sourceTree = ""; }; + 084B1B9FCCF4AB727B4ECFB2 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; + 090EF7D598B8DE779C275395 /* STPPostalCodeInputTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeInputTextFieldSnapshotTests.swift; sourceTree = ""; }; + 095EBF095BA2BC8D299547DB /* STPSourceOwnerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceOwnerTest.swift; sourceTree = ""; }; + 0A16326394D71637A2CF68C3 /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Localizable.strings; sourceTree = ""; }; + 0A8FF64CA314F909D7EC82FE /* STPPaymentMethodGrabPayParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodGrabPayParamsTest.swift; sourceTree = ""; }; + 0ABB2CA7E96BE249CE8C0566 /* STPPaymentMethodCashAppParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCashAppParamsTests.swift; sourceTree = ""; }; + 0B91C4D5B93FF71C61B140F1 /* STPCardScannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardScannerTableViewCell.swift; sourceTree = ""; }; + 0C44B4366D6C4FD4B11662C8 /* STPPaymentMethodEPSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodEPSTests.swift; sourceTree = ""; }; + 0C75157665428685C7A4FD20 /* STPCardExpiryInputTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardExpiryInputTextFieldSnapshotTests.swift; sourceTree = ""; }; + 0C9D6F99E303A17A91101723 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = ""; }; + 0DB03E83746FE78361831546 /* WalletHeaderViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHeaderViewSnapshotTests.swift; sourceTree = ""; }; + 0E10C4D64477638398251FFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 0F735744F27D46F005BB5D67 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; + 1007571188950D7FBF745A4E /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + 11C866064B3482878A69892F /* CustomerAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerAdapterTests.swift; sourceTree = ""; }; + 121B7EDCCD0957C9A444A8E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 12632E6710DE8861CAF1BAA4 /* RotatingCardBrandsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotatingCardBrandsViewTests.swift; sourceTree = ""; }; + 12D757B36030685C401A6990 /* et-EE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "et-EE"; path = "et-EE.lproj/Localizable.strings"; sourceTree = ""; }; + 12DBB3F72AEFB52DE27C27ED /* STPAnalyticsClient+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAnalyticsClient+BasicUI.swift"; sourceTree = ""; }; + 13DDBEA7D444A8AC14E0F1C8 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = ""; }; + 148C1D7D1BBBC6B74894A869 /* STPPaymentMethodTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodTest.swift; sourceTree = ""; }; + 153071C69A0BEE033E035DCF /* CardExpiryDateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardExpiryDateTests.swift; sourceTree = ""; }; + 1645D9793463E266501B74FD /* STPPIIFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPIIFunctionalTest.swift; sourceTree = ""; }; + 1671EC46C713D51013AD7D8B /* STPPaymentMethodCardParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardParamsTest.swift; sourceTree = ""; }; + 17013F78CE3F9662029FEF5B /* STPSourceFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceFunctionalTest.swift; sourceTree = ""; }; + 180CF848E3ABF0236C494D8B /* FBSnapshotTestCase+STPViewControllerLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FBSnapshotTestCase+STPViewControllerLoading.swift"; sourceTree = ""; }; + 18FCB69CD3B8C3DAB216A5F0 /* STPPaymentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentConfiguration.swift; sourceTree = ""; }; + 195DEC752CC82CC4BA1E2351 /* STPConnectAccountParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPConnectAccountParamsTest.swift; sourceTree = ""; }; + 1A8A6B88797870BC71CCB3AF /* STPPushProvisioningDetailsFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPushProvisioningDetailsFunctionalTest.swift; sourceTree = ""; }; + 1B76DF0FE363F59BF0940A8B /* UIView+Stripe_FirstResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Stripe_FirstResponder.swift"; sourceTree = ""; }; + 1C1548BA518F7AC2A9ECF9D5 /* STPE2ETest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPE2ETest.swift; sourceTree = ""; }; + 1CE268457D21A7209862E004 /* STPApplePayContextFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayContextFunctionalTest.swift; sourceTree = ""; }; + 1D23EB567F573612E0794B3A /* Stripe Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe Tests-Debug.xcconfig"; sourceTree = ""; }; + 1D51B04D83D4FEF7F90DF16A /* STPCertTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCertTest.swift; sourceTree = ""; }; + 1D575C31524E596E9C1A8E9B /* STPCardTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardTest.swift; sourceTree = ""; }; + 1D983E089196152DA1C69469 /* STPCardFormViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardFormViewSnapshotTests.swift; sourceTree = ""; }; + 1DD6897858F46976A946394E /* StripeiOSAppHostedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeiOSAppHostedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E2638F7AA0906914117C2D5 /* STPPaymentMethodOptionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodOptionsTest.swift; sourceTree = ""; }; + 1E8AFAE24610EC983727F860 /* STPAPIClient+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+BasicUI.swift"; sourceTree = ""; }; + 1ED6BAC91E8A827DCDB38B15 /* STPShippingAddressViewControllerLocalizationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingAddressViewControllerLocalizationSnapshotTests.swift; sourceTree = ""; }; + 1F0DF2ED9232A7CC51F5FCB1 /* STPPaymentMethodAffirmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAffirmTests.swift; sourceTree = ""; }; + 1F16C36797D978E72E612100 /* STPPaymentCardTextFieldTestsSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentCardTextFieldTestsSwift.swift; sourceTree = ""; }; + 1F29C15B47C7CB0941CD4C9E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 1F6CB4B8FAD14B4D70A63595 /* STPSourceParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceParamsTest.swift; sourceTree = ""; }; + 1FFBAA4B44967B157A4F4E91 /* STPPaymentActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentActivityIndicatorView.swift; sourceTree = ""; }; + 20552E792B8E7BA15821AB5D /* NSDecimalNumber+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDecimalNumber+StripeTest.swift"; sourceTree = ""; }; + 207677E2A0DBC04C88139372 /* STPPaymentMethodSofortParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSofortParamsTests.swift; sourceTree = ""; }; + 20879436DCFB1F03BE1608B3 /* ServerErrorMapperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerErrorMapperTest.swift; sourceTree = ""; }; + 21780C410D22264B7C299520 /* STPSourceSEPADebitDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceSEPADebitDetailsTest.swift; sourceTree = ""; }; + 21E4B84223DDA131544DBBA7 /* STPShippingMethodsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingMethodsViewController.swift; sourceTree = ""; }; + 22D1C6EB5826E2D7C80B6CF3 /* StripePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 23997D61DF41CA84BFC33080 /* STPBECSDebitAccountNumberValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBECSDebitAccountNumberValidatorTests.swift; sourceTree = ""; }; + 2560B4EEE60D40FB31B8552F /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 26302721419496F37DE91DF8 /* UserDefaults+Stripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Stripe.swift"; sourceTree = ""; }; + 26E5C1DABAA63F07F0C6AE37 /* STPAddCardViewControllerLocalizationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddCardViewControllerLocalizationSnapshotTests.swift; sourceTree = ""; }; + 26E70469F4032553B4BB62DA /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/Localizable.strings"; sourceTree = ""; }; + 273EE407039913F0B644172B /* PKAddPaymentPassRequest+Stripe_Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKAddPaymentPassRequest+Stripe_Error.swift"; sourceTree = ""; }; + 27A4D83C0E7D4AE0CCD38B89 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + 284C67269D2606DA147AE01D /* STPGenericInputPickerFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPGenericInputPickerFieldSnapshotTests.swift; sourceTree = ""; }; + 28A50AFA4603E488FF3D82D0 /* STPAddressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddressViewModel.swift; sourceTree = ""; }; + 294CD46E24BB2743042872D7 /* StripeiOSTestHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StripeiOSTestHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A63AC868755CB4745E7458E /* STPPaymentMethodPayPalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodPayPalTests.swift; sourceTree = ""; }; + 2A8A2CD759D465290066EF65 /* STPTextFieldDelegateProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPTextFieldDelegateProxyTests.swift; sourceTree = ""; }; + 2B28B8A547CD846277ECD578 /* STPPushProvisioningDetailsParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPushProvisioningDetailsParams.swift; sourceTree = ""; }; + 2C078573F46762353664AC92 /* UINavigationController+Stripe_Completion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Stripe_Completion.swift"; sourceTree = ""; }; + 2CC4B06AB5C02FF54091E5A8 /* STPCustomerContextTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCustomerContextTest.swift; sourceTree = ""; }; + 2D1525AF65BDEF691F8BCBE8 /* STPCoreScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCoreScrollViewController.swift; sourceTree = ""; }; + 2D63B73C5773432CA134D1FC /* STPGenericInputPickerFieldValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPGenericInputPickerFieldValidatorTest.swift; sourceTree = ""; }; + 2D878F923A1F69B58D6B2812 /* STPRadarSessionFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPRadarSessionFunctionalTest.swift; sourceTree = ""; }; + 2DC4B62C336EAA05A33FC384 /* STPPaymentMethodCardWalletTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardWalletTest.swift; sourceTree = ""; }; + 2E1862744F23286D1FB9D4AE /* STPFormTextFieldTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFormTextFieldTest.swift; sourceTree = ""; }; + 2F08757CA6F6B2DA65C14E0A /* UIViewController+Stripe_KeyboardAvoiding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Stripe_KeyboardAvoiding.swift"; sourceTree = ""; }; + 30964128998473CAA9F2DD7E /* STPFormEncoderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFormEncoderTest.swift; sourceTree = ""; }; + 30B694A39D54886392AA5DE3 /* STPPaymentOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOption.swift; sourceTree = ""; }; + 313263D9DC0629C5EE279FEB /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 313F5F782B0BE59000BD98A9 /* Docs.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Docs.docc; sourceTree = ""; }; + 31CDFC2D2BA3708000B3DD91 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 336CC555B845DED30208D39D /* STPPinManagementServiceFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPinManagementServiceFunctionalTest.swift; sourceTree = ""; }; + 33B9D01D037909D1C9C0B617 /* STPIntentActionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionTest.swift; sourceTree = ""; }; + 33FDC634FD5D79E824240DDC /* Stripe3DS2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stripe3DS2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 35ABE696542469B79A9D52E6 /* tk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tk; path = tk.lproj/Localizable.strings; sourceTree = ""; }; + 35C1E9B0EE03825DABF6471A /* STPPaymentMethod+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentMethod+BasicUI.swift"; sourceTree = ""; }; + 3AE7BEADD3824A06C2994854 /* STPPaymentMethodRevolutPayParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodRevolutPayParamsTests.swift; sourceTree = ""; }; + 3B0E131538728BC4802627B1 /* UserDefaults+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+StripeTest.swift"; sourceTree = ""; }; + 3B112FFF3FCA82094281493F /* STPPaymentMethodUSBankAccountTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodUSBankAccountTest.swift; sourceTree = ""; }; + 3B3000668A75E095B514241F /* UIBarButtonItem+Stripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Stripe.swift"; sourceTree = ""; }; + 3C742844915B96CFD25BFFF9 /* AfterpayPriceBreakdownViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AfterpayPriceBreakdownViewSnapshotTests.swift; sourceTree = ""; }; + 3C77C7BC4BA57EC296CF2F1C /* STPApplePayFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayFunctionalTest.swift; sourceTree = ""; }; + 3C995125252BED1EEC018B9D /* STPPaymentContextApplePayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentContextApplePayTest.swift; sourceTree = ""; }; + 3CDD1E823223F450193E8746 /* STPPaymentMethodAfterpayClearpayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAfterpayClearpayTest.swift; sourceTree = ""; }; + 3E1C5E08678292561255B1C5 /* STPCardBINMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardBINMetadataTests.swift; sourceTree = ""; }; + 3E5FB20B2BEFC00D54FDD87D /* STPCard+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPCard+BasicUI.swift"; sourceTree = ""; }; + 3EBB07171F6FDCE6E20C454A /* STPPinManagementService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPinManagementService.swift; sourceTree = ""; }; + 3ED44491EB0AC72B1B1A773C /* STPThreeDSUICustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSUICustomizationTest.swift; sourceTree = ""; }; + 3FC0560A312147C37CFE6CF9 /* STPBinRangeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBinRangeTest.swift; sourceTree = ""; }; + 4002981AC12687681616D21E /* STPPaymentMethodParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodParamsTest.swift; sourceTree = ""; }; + 4077600B9B3C1ABFF38383BE /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + 40BB87E28719FE0C6B946BB5 /* STPCardExpiryInputTextFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardExpiryInputTextFieldFormatterTests.swift; sourceTree = ""; }; + 4259421D2CD26E37B96F97B2 /* Stripe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stripe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 43ADFC4EF612D7C4A46E81B9 /* lt-LT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lt-LT"; path = "lt-LT.lproj/Localizable.strings"; sourceTree = ""; }; + 43B4E4B85C598D7A9AFCB4D4 /* StripeCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4418164D75002AE6A0273176 /* STPCardExpiryInputTextFieldValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardExpiryInputTextFieldValidatorTests.swift; sourceTree = ""; }; + 458F8576215E0F8ECE1D74CE /* STPBankSelectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBankSelectionTableViewCell.swift; sourceTree = ""; }; + 45FF7A07CFC3B9B7AD6B49EE /* FraudDetectionDataTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FraudDetectionDataTest.swift; sourceTree = ""; }; + 46AC0B5EC7433E081825D31B /* CircularButtonSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularButtonSnapshotTests.swift; sourceTree = ""; }; + 46C31CAD0FA74B58BA2B8530 /* STPPaymentIntentEnumsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentIntentEnumsTest.swift; sourceTree = ""; }; + 47E12A0CBFA259A032F7AF0C /* STPPaymentMethodKlarnaParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodKlarnaParamsTests.swift; sourceTree = ""; }; + 47E5E1173A37AABB07FB68AB /* STPPaymentMethodUPIParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodUPIParamsTest.swift; sourceTree = ""; }; + 482693A3D9A527B0E671F757 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 483B243268646AE65B06E98C /* STPThreeDSFooterCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSFooterCustomizationTest.swift; sourceTree = ""; }; + 485E747DA1F72F091986787B /* STPEphemeralKeyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPEphemeralKeyProvider.swift; sourceTree = ""; }; + 498C3FB07CFD532779C755D3 /* STPAddress+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAddress+BasicUI.swift"; sourceTree = ""; }; + 49AA313E068FB99CEAA5F7D3 /* STPSetupIntentFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSetupIntentFunctionalTest.swift; sourceTree = ""; }; + 4AA36705DED9164663A98B6A /* STPNumericStringValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPNumericStringValidatorTests.swift; sourceTree = ""; }; + 4AAE5EE11611F9F7762B64C6 /* STPAUBECSFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAUBECSFormViewModelTests.swift; sourceTree = ""; }; + 4CA5D11C977A95B8E936E907 /* STPIntentActionWeChatPayRedirectToAppTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionWeChatPayRedirectToAppTest.swift; sourceTree = ""; }; + 4DFDFC019C67AFF7C0F7630B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + 4E29B46F2C940E0A21734E09 /* StripeiOSTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = StripeiOSTests.xctestplan; sourceTree = ""; }; + 4E371E9B3B2E343FE954531C /* STPPaymentMethodBoletoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBoletoTests.swift; sourceTree = ""; }; + 4FD94FF270165D699DA89B24 /* STPPaymentMethodKlarnaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodKlarnaTests.swift; sourceTree = ""; }; + 4FFA8B446217CDE678D7287F /* STPBlocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STPBlocks.h; sourceTree = ""; }; + 5062915D961C1BCAFD641FFE /* STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift; sourceTree = ""; }; + 512A0E7C246D5F044245E069 /* StripeCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 51408DE266D0345784ADD4FA /* STPThreeDSNavigationBarCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSNavigationBarCustomizationTest.swift; sourceTree = ""; }; + 51BD2CE41E4F0CF648F44E4A /* TextFieldElement+IBANTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextFieldElement+IBANTest.swift"; sourceTree = ""; }; + 51E62BB62EA9B782778CA880 /* STPStackViewWithSeparatorSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStackViewWithSeparatorSnapshotTests.swift; sourceTree = ""; }; + 52F8AEC50D4623F80F04A533 /* StripeApplePay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeApplePay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 533538E3EB92E326CCB95506 /* STPPaymentMethodCardWalletMasterpassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardWalletMasterpassTest.swift; sourceTree = ""; }; + 53C5AB22D6328E85A6DDF663 /* STPPaymentMethodParams+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentMethodParams+BasicUI.swift"; sourceTree = ""; }; + 54426CBF6F77ABEFBDFDA8C4 /* STPPaymentMethodAUBECSDebitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAUBECSDebitTests.swift; sourceTree = ""; }; + 5476BD87E0480A93958F0328 /* UITableViewCell+Stripe_Borders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Stripe_Borders.swift"; sourceTree = ""; }; + 5804C2B9C0704E386B3D25A4 /* UIViewController+Stripe_ParentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Stripe_ParentViewController.swift"; sourceTree = ""; }; + 58158E8A117299834246100F /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + 583DB466066B47C0F716E474 /* STPPaymentMethodGiropayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodGiropayTests.swift; sourceTree = ""; }; + 588C260880FFC584A00A89F5 /* STPEphemeralKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPEphemeralKeyManager.swift; sourceTree = ""; }; + 58A53F005EA8FDDAA66126BA /* STPGenericInputTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPGenericInputTextFieldSnapshotTests.swift; sourceTree = ""; }; + 5D237177A7F99EB0F5F4F5E4 /* StripePaymentsObjcTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripePaymentsObjcTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5DE9F0BAC35EA14579775033 /* STPPaymentMethodSwishTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSwishTests.swift; sourceTree = ""; }; + 5E4EA6394497D1BD57ED0032 /* Stripe Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe Tests-Release.xcconfig"; sourceTree = ""; }; + 5F7AB40A5A10C2D267323ABE /* STPApplePayContextTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayContextTest.swift; sourceTree = ""; }; + 5FDD4956E0A04D33F0856F31 /* STPThreeDSLabelCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSLabelCustomizationTest.swift; sourceTree = ""; }; + 610DF5DB2B33597500DA6AAA /* HostedSurfaceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostedSurfaceTest.swift; sourceTree = ""; }; + 61152B4E2B866827003B69A0 /* STPPaymentMethodAmazonPayParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAmazonPayParamsTests.swift; sourceTree = ""; }; + 617C1C872BB4992400B10AC5 /* STPPaymentMethodAlmaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAlmaTests.swift; sourceTree = ""; }; + 617C1C892BB4998C00B10AC5 /* STPPaymentMethodAlmaParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAlmaParamsTests.swift; sourceTree = ""; }; + 618DE183886175AF23C4E668 /* sl-SI */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sl-SI"; path = "sl-SI.lproj/Localizable.strings"; sourceTree = ""; }; + 61951FB82B866BA1005F90BE /* STPPaymentMethodAmazonPayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAmazonPayTests.swift; sourceTree = ""; }; + 61AF6E95FE0DD913204CAB32 /* AnalyticsHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHelperTests.swift; sourceTree = ""; }; + 61E0A0C42BF31D3C00C89786 /* STPPaymentHandlerRefreshTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentHandlerRefreshTests.swift; sourceTree = ""; }; + 61E1CA1E2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMultibancoTests.swift; sourceTree = ""; }; + 61E1CA202BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMultibancoParamsTests.swift; sourceTree = ""; }; + 61E1CA262BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionMultibancoDisplayDetailsTest.swift; sourceTree = ""; }; + 61F8308B7250B642D19827D8 /* STPCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCameraView.swift; sourceTree = ""; }; + 6215A9BF343775B1BD0F62AF /* STPPaymentOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionTableViewCell.swift; sourceTree = ""; }; + 6223E57D3A198F956A37ED89 /* STPNumericDigitInputTextFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPNumericDigitInputTextFormatterTests.swift; sourceTree = ""; }; + 63114D0EAAE2606732DF5AA0 /* STPSourceCardDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceCardDetailsTest.swift; sourceTree = ""; }; + 63F5F35DB97D8A176FB6ED24 /* NSString+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSString+StripeTest.swift"; sourceTree = ""; }; + 655209238C85F466F9F14F14 /* STPPaymentMethodBancontactParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBancontactParamsTests.swift; sourceTree = ""; }; + 65DCC9BDA647E58D3882C698 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 6618739767139C25C05B3631 /* STPPostalCodeInputTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeInputTextFieldTests.swift; sourceTree = ""; }; + 661976D1296DFA48A25E0493 /* STPShippingMethodsViewControllerLocalizationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingMethodsViewControllerLocalizationSnapshotTests.swift; sourceTree = ""; }; + 683F7735569D22CBEC9CA2E6 /* STPPaymentHandlerFunctionalTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentHandlerFunctionalTest.m; sourceTree = ""; }; + 6887F19BB9804BF45FD703FF /* STPPushProvisioningContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPushProvisioningContext.swift; sourceTree = ""; }; + 6955B3A3353F8442E4FBBBF6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 69BD038947E8E2376A0D240B /* STPCardScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardScanner.swift; sourceTree = ""; }; + 6A9E7B637A8747431B38FD1D /* STPCardValidationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardValidationState.swift; sourceTree = ""; }; + 6B7A947152A728EB2CBC4DB2 /* STPSourceReceiverTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceReceiverTest.swift; sourceTree = ""; }; + 6BA4B9192BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMobilePayParamsTests.swift; sourceTree = ""; }; + 6BA4B91B2BF4343B00D1F21D /* STPPaymentMethodMobilePayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMobilePayTests.swift; sourceTree = ""; }; + 6BC5EC2C2B4609FF00CC75E8 /* LinkInlineSignupElementSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInlineSignupElementSnapshotTests.swift; sourceTree = ""; }; + 6C7B8DACB0A7294BC235E3BC /* STPPaymentIntentLastPaymentErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentIntentLastPaymentErrorTest.swift; sourceTree = ""; }; + 6CC0B1FC92A573AAEA4F4E94 /* STPSetupIntentConfirmParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSetupIntentConfirmParamsTest.swift; sourceTree = ""; }; + 6E1F6514E7530C2A3478B2F5 /* STPCardCVCInputTextFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldFormatterTests.swift; sourceTree = ""; }; + 6F01150CD0255164FE2CF3A4 /* STPPaymentIntentParams+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentIntentParams+BasicUI.swift"; sourceTree = ""; }; + 6F017A08C7E633FB4297D274 /* UIViewController+Stripe_NavigationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Stripe_NavigationItemProxy.swift"; sourceTree = ""; }; + 71711FC8E2FB66E52A5FDD9A /* STPShippingAddressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingAddressViewController.swift; sourceTree = ""; }; + 72903593DC432D01720DC9D9 /* STPAddressFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddressFieldTableViewCell.swift; sourceTree = ""; }; + 739934737B9A09775CD278C9 /* STPPaymentMethodNetBankingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodNetBankingTests.swift; sourceTree = ""; }; + 74FDEF9F687C63BADFB96480 /* STPPaymentMethodUSBankAccountParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodUSBankAccountParamsTest.swift; sourceTree = ""; }; + 77247622AB08FEF48CA0DC26 /* StripePaymentsTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripePaymentsTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 77E846CD56018D8417A3AB95 /* StripeiOS.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = StripeiOS.xcassets; sourceTree = ""; }; + 789D0B49B0788794739E3DD4 /* STPShippingAddressViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingAddressViewControllerTest.swift; sourceTree = ""; }; + 78D1EEABBAE5BD5615486B0F /* STPLabeledFormTextFieldViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPLabeledFormTextFieldViewSnapshotTests.swift; sourceTree = ""; }; + 79ABE6A14AF9D14103050876 /* STPPaymentConfigurationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentConfigurationTest.m; sourceTree = ""; }; + 7A517686D4D12691351311CA /* STPIntentActionPayNowDisplayQrCodeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionPayNowDisplayQrCodeTest.swift; sourceTree = ""; }; + 7A7963CC618A1A1346EC20C7 /* STPPaymentMethodRevolutPayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodRevolutPayTests.swift; sourceTree = ""; }; + 7AC0A18C441FCA394BEF6A3D /* STPPaymentMethodBillingDetailsTests+Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentMethodBillingDetailsTests+Link.swift"; sourceTree = ""; }; + 7B10B1A15063FEDBA4A59953 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 7B8ADF2EA2D5C4C90BCDDDD5 /* bg-BG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bg-BG"; path = "bg-BG.lproj/Localizable.strings"; sourceTree = ""; }; + 7B9A4A2B0FB9F8C743BBED48 /* PaymentTypeCellSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTypeCellSnapshotTests.swift; sourceTree = ""; }; + 7C04AFC9CDE50D09D38A3232 /* FormSpecProviderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormSpecProviderTest.swift; sourceTree = ""; }; + 7CAE7444CEFE1D1EFB888996 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; + 7D964D9E01627B419B4BD23C /* STPPaymentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentResult.swift; sourceTree = ""; }; + 7DDE50CBC86AD77084C877B6 /* STPPaymentMethodCashAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCashAppTests.swift; sourceTree = ""; }; + 7F090B8D315E7FD12A5F9C09 /* STPTokenTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPTokenTest.swift; sourceTree = ""; }; + 7F68637E75142DCD46710796 /* STPLabeledMultiFormTextFieldViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPLabeledMultiFormTextFieldViewSnapshotTests.swift; sourceTree = ""; }; + 7FF2B9FF57E100301B5C38DB /* STPPaymentMethodAUBECSDebitParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAUBECSDebitParamsTests.swift; sourceTree = ""; }; + 806124200E77795DCFC8418E /* ConfirmButtonSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonSnapshotTests.swift; sourceTree = ""; }; + 807FF966F1DE05F3496B817B /* STPAPIClient+PushProvisioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+PushProvisioning.swift"; sourceTree = ""; }; + 80BBCC4D386EE44E809A591C /* STPPaymentMethodOXXOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodOXXOTests.swift; sourceTree = ""; }; + 81352A0CBE46A59E6B1A712E /* STPBankSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBankSelectionViewController.swift; sourceTree = ""; }; + 8192839B0F1AE9D9F2A94504 /* STPPaymentMethodFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodFunctionalTest.swift; sourceTree = ""; }; + 82E46B18CDDC191934F3D4BE /* STPPaymentMethodCardWalletVisaCheckoutTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardWalletVisaCheckoutTest.swift; sourceTree = ""; }; + 835CB781FBC19773ACC20676 /* LinkLegalTermsViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkLegalTermsViewSnapshotTests.swift; sourceTree = ""; }; + 85AAB72218409F85FE29E69E /* APIRequestTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestTest.swift; sourceTree = ""; }; + 85C29AE44D809CD677B5E52B /* STPFPXBankBrandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFPXBankBrandTest.swift; sourceTree = ""; }; + 86798C95A778362EF815B4C6 /* UIView+Stripe_SafeAreaBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Stripe_SafeAreaBounds.swift"; sourceTree = ""; }; + 86983B09A1712944EC012AD4 /* STPViewWithSeparatorSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPViewWithSeparatorSnapshotTests.swift; sourceTree = ""; }; + 8704ABFA91A5226847F4A69A /* STPAddCardViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddCardViewControllerTest.swift; sourceTree = ""; }; + 88639E3C3AC622B5EF475538 /* STPUIVCStripeParentViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPUIVCStripeParentViewControllerTests.swift; sourceTree = ""; }; + 88AEABC15CCBB9EA393C175F /* STPMocks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPMocks.m; sourceTree = ""; }; + 890660C21E3666CE7B82695B /* STPEphemeralKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPEphemeralKey.swift; sourceTree = ""; }; + 89E5DA3029F141B5111A5B2C /* STPPushProvisioningDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPushProvisioningDetails.swift; sourceTree = ""; }; + 8A3F2B714DB3D1DED561A7EF /* STPCardParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardParamsTest.swift; sourceTree = ""; }; + 8BD02D8298877F10F2EF2A9D /* STPPaymentMethodCardTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardTest.swift; sourceTree = ""; }; + 8CE85C770AEEDBE4AEC93EAA /* Error+PaymentSheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+PaymentSheetTests.swift"; sourceTree = ""; }; + 8D49257A97E71A475A9F6E08 /* STPCountryPickerInputFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCountryPickerInputFieldSnapshotTests.swift; sourceTree = ""; }; + 8E8CA4361964E1BA400EFC89 /* STPTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPTheme.swift; sourceTree = ""; }; + 8ED737CB1253C3C5704B6C05 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = ""; }; + 8F7E3CE2105E4A39032CD919 /* Enums+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enums+CustomStringConvertible.swift"; sourceTree = ""; }; + 901BE31021AF27DB5D326327 /* STPCardBrandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardBrandTest.swift; sourceTree = ""; }; + 90D0900C5FDBB7952BCF2C3A /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; + 916DB8789F65D3C1BCB510C0 /* STPPaymentMethodThreeDSecureUsageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodThreeDSecureUsageTest.swift; sourceTree = ""; }; + 917154477796779ECFA1334A /* STPPostalCodeInputTextFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeInputTextFieldFormatterTests.swift; sourceTree = ""; }; + 924E878428D15506711CA628 /* STPPhoneNumberValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPhoneNumberValidatorTest.swift; sourceTree = ""; }; + 939360978872BBE4334215B1 /* STPPaymentCardTextFieldTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentCardTextFieldTest.swift; sourceTree = ""; }; + 940209E5D30E86E856016906 /* STPPaymentMethodUPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodUPITests.swift; sourceTree = ""; }; + 9466F23BA8712EA2EDA48BBD /* STPIntentActionPromptPayDisplayQrCodeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionPromptPayDisplayQrCodeTest.swift; sourceTree = ""; }; + 94A7104C1C470515616E4D2B /* STPCardCVCInputTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldTests.swift; sourceTree = ""; }; + 9631915F03F157A1CC3FEFFE /* STPPaymentMethodSwishParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSwishParamsTests.swift; sourceTree = ""; }; + 967C784618A074FF021B3089 /* STPBankAccountParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBankAccountParamsTest.swift; sourceTree = ""; }; + 967DDC94B687B14E07842CC8 /* STPConnectAccountAddressTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPConnectAccountAddressTest.swift; sourceTree = ""; }; + 969E196AB597EEF68C38103E /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + 96E1FED5CE5974C9C1162E93 /* STPSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSectionHeaderView.swift; sourceTree = ""; }; + 98544B08552407D41D398C68 /* STPShippingMethodTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPShippingMethodTableViewCell.swift; sourceTree = ""; }; + 989411FA3CD0CCC38BC227F4 /* STPBankAccountFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBankAccountFunctionalTest.swift; sourceTree = ""; }; + 98F9CB667BC68767DFB5FACD /* OneTimeCodeTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneTimeCodeTextFieldTests.swift; sourceTree = ""; }; + 9B782E1D974A4C131E60E2BD /* STPBackendAPIAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBackendAPIAdapter.swift; sourceTree = ""; }; + 9B83930B631CF8EADFB606D6 /* STPPaymentMethodiDEALTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodiDEALTest.swift; sourceTree = ""; }; + 9BBCE3A905041A709E8F279A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9D3BBCE8C46A38D0E20DBF4E /* ms-MY */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ms-MY"; path = "ms-MY.lproj/Localizable.strings"; sourceTree = ""; }; + 9D85FA7B714BDD8D1FD83B75 /* OneTimeCodeTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneTimeCodeTextFieldSnapshotTests.swift; sourceTree = ""; }; + 9DCCA0E8A02B4F4B23837FB4 /* MKPlacemark+PaymentSheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MKPlacemark+PaymentSheetTests.swift"; sourceTree = ""; }; + 9EAE9A2AE65771403CE57C11 /* STPErrorBridgeTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPErrorBridgeTest.m; sourceTree = ""; }; + 9EB24EC81CE2C8D1C863B044 /* STPIntentActionLinkAuthenticateAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionLinkAuthenticateAccount.swift; sourceTree = ""; }; + 9FEE395C4DD0E0112AF3720C /* STPInputTextFieldValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPInputTextFieldValidatorTests.swift; sourceTree = ""; }; + A0E24B5689732EB9106DA232 /* STPPaymentMethodBillingDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBillingDetailsTest.swift; sourceTree = ""; }; + A1272F2E05A0E294DD9ECA26 /* STPApplePayContextFunctionalTestExtras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayContextFunctionalTestExtras.swift; sourceTree = ""; }; + A15BBFFA401852A8719E3DDD /* STPPaymentMethodCardChecksTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardChecksTest.swift; sourceTree = ""; }; + A1C67AC5D415615E9F27D3E3 /* STPPaymentMethodBoletoParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBoletoParamsTests.swift; sourceTree = ""; }; + A1C876DC7F3E31D7189506A8 /* STPPaymentContextAmountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentContextAmountModel.swift; sourceTree = ""; }; + A22E5B87755C1F05C3DB438C /* STPInputTextFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPInputTextFieldFormatterTests.swift; sourceTree = ""; }; + A2922A32A754CFC9AB8B48AE /* STPPaymentOptionsViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionsViewControllerTest.swift; sourceTree = ""; }; + A39580123A4F1EA96F91768A /* STPAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddressTests.swift; sourceTree = ""; }; + A41F721AEBB942BB81408A59 /* STPPaymentMethodSEPADebitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSEPADebitTest.swift; sourceTree = ""; }; + A5398E1156E0BFEBBF56FD2F /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; + A583966A33DCDCF04322A592 /* STPThreeDSTextFieldCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSTextFieldCustomizationTest.swift; sourceTree = ""; }; + A61763BA2CCA86F9B8FD4F1F /* OperationDebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDebouncerTests.swift; sourceTree = ""; }; + A6269E77F81C32A5EC8BE412 /* STPPaymentMethodPrzelewy24ParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodPrzelewy24ParamsTests.swift; sourceTree = ""; }; + A6F6634AD12771A9BB100DD3 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + A7E369CEC9F5B3758F78E88F /* UINavigationBar+Stripe_Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Stripe_Theme.swift"; sourceTree = ""; }; + A8598727045C6268B57A5FC7 /* Stripe-umbrella.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Stripe-umbrella.h"; sourceTree = ""; }; + A9A1BB31C7B514984231125B /* STPPaymentOptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionsViewController.swift; sourceTree = ""; }; + AA3CA5B4B91838CC7ED4D5EB /* ro-RO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ro-RO"; path = "ro-RO.lproj/Localizable.strings"; sourceTree = ""; }; + AAF368BCD5990EE5DC17D299 /* ImageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTest.swift; sourceTree = ""; }; + AB1A92C77AC61335184BBDBC /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + ABAABB969BFB7102D5AA5598 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + ACE8998EAF997A78759E49B5 /* STPPaymentIntentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentIntentTest.swift; sourceTree = ""; }; + ACF450AD17FF7BCE5916DDF1 /* STPPaymentHandlerFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentHandlerFunctionalTest.swift; sourceTree = ""; }; + AD06AED0AF8A9A7FB4A2E66F /* STPIntentActionAlipayHandleRedirectTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionAlipayHandleRedirectTest.swift; sourceTree = ""; }; + AD8BED2A6066514B51693172 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AF342CBC167F9CAB5B49CC32 /* PayWithLinkButtonSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayWithLinkButtonSnapshotTests.swift; sourceTree = ""; }; + AFF957F38AABE5F748C38C0B /* STPLocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPLocalizedString.swift; sourceTree = ""; }; + B1217AD643A9E8F88B60F645 /* STPUserInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPUserInformation.swift; sourceTree = ""; }; + B334078D1C3E629BEB498BAB /* nn-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nn-NO"; path = "nn-NO.lproj/Localizable.strings"; sourceTree = ""; }; + B3E0745D13EB19BAA24F3BA3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B407FE2D39775902A95B1118 /* StripeiOS Tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StripeiOS Tests-Bridging-Header.h"; sourceTree = ""; }; + B4D12508C2F1056A7EAFEC86 /* PKPayment+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPayment+StripeTest.swift"; sourceTree = ""; }; + B4D31B0D7BD9F97AF3BB61E6 /* StripeBundleLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeBundleLocator.swift; sourceTree = ""; }; + B5B86F3355E44DF4A980B82C /* STPPaymentContextSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentContextSnapshotTests.swift; sourceTree = ""; }; + B669C53A601CD3CB0203A4B9 /* STPAPISettingsObjCBridgeTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPAPISettingsObjCBridgeTest.m; sourceTree = ""; }; + B6EC2F562B618A3D00FF72A2 /* STPBasicUIAnalyticsSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBasicUIAnalyticsSerializer.swift; sourceTree = ""; }; + B6F3B966470A530E0DC53F8C /* STPThreeDSButtonCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSButtonCustomizationTest.swift; sourceTree = ""; }; + B70DF0B659009041F485EE0F /* Stripe+Exports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stripe+Exports.swift"; sourceTree = ""; }; + B78C72B0DB434EC7F700FDE0 /* STPCoreTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCoreTableViewController.swift; sourceTree = ""; }; + B7F7AA0B7B86BA5BB2FE92CE /* STPMandateDataParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPMandateDataParamsTest.swift; sourceTree = ""; }; + B8DD70E5ED8E9DE8E9752C9E /* STPAPIClientTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAPIClientTest.swift; sourceTree = ""; }; + B9BA8D8467218C7E691C9FAE /* STPAPIClientNetworkBridgeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAPIClientNetworkBridgeTest.swift; sourceTree = ""; }; + BA08DCDD421CE92ECB61EF5C /* STPFPXBankStatusResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFPXBankStatusResponse.swift; sourceTree = ""; }; + BAFD938F3A149A56CC99FE96 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + BB08D2AC882B21C8ADD76B92 /* STPPaymentCardTextFieldKVOTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentCardTextFieldKVOTest.m; sourceTree = ""; }; + BB8FCDBC63A79CD1571A2DFB /* STPCustomerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCustomerContext.swift; sourceTree = ""; }; + BCBEA9E4823F08C1F5057B5A /* STPAUBECSDebitFormViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAUBECSDebitFormViewSnapshotTests.swift; sourceTree = ""; }; + BD77D4B1C5B64E45F9DA09B5 /* STPConfirmCardOptionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPConfirmCardOptionsTest.swift; sourceTree = ""; }; + BD89580A3E41D7167C30B287 /* STPAnalyticsClient+Payments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAnalyticsClient+Payments.swift"; sourceTree = ""; }; + BEB3F9F0228008BE213706DF /* STPPaymentMethodAddressTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAddressTest.swift; sourceTree = ""; }; + BF1CF8FB0100664A02468FBC /* STPPaymentMethodBacsDebitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBacsDebitTest.swift; sourceTree = ""; }; + BFB4A210A30D1D4F3D3100E5 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + C0436A8574E7D0730641407A /* STPSourceRedirectTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceRedirectTest.swift; sourceTree = ""; }; + C17799DC7FA54E758EED31A6 /* NSArray+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSArray+StripeTest.swift"; sourceTree = ""; }; + C23D612FD5AD7772E1B30DCC /* STPThreeDSSelectionCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSSelectionCustomizationTest.swift; sourceTree = ""; }; + C3B75875C55D2C2723DC5090 /* STPSource+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPSource+BasicUI.swift"; sourceTree = ""; }; + C45852D37E323E65C47348B0 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; + C5923A4DD3CD39CB64B8A8C9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + C5BEAA15B53AC5662A33D0E1 /* STPEphemeralKeyTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPEphemeralKeyTest.swift; sourceTree = ""; }; + C641A744CCEA67C07E9BFE05 /* NSLocale+STPSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLocale+STPSwizzling.swift"; sourceTree = ""; }; + C645F78B3EFFAA083B6FD3E9 /* STPEphemeralKeyManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPEphemeralKeyManagerTest.swift; sourceTree = ""; }; + C713F58BC61A962C720AE0AE /* STPMandateCustomerAcceptanceParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPMandateCustomerAcceptanceParamsTest.swift; sourceTree = ""; }; + C750C2C4AB33BC232D1592BA /* STPPaymentContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentContext.swift; sourceTree = ""; }; + C8F8FCC84601E4ADC6B7F3CE /* PaymentAnalyticTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAnalyticTest.swift; sourceTree = ""; }; + C980D24DDC884FECCE39139F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + CA8B8F540CD05B3DC2C5EEA6 /* STPSetupIntentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSetupIntentTest.swift; sourceTree = ""; }; + CAC3A0332C2F176A007BC888 /* STPPaymentMethodSunbitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSunbitTests.swift; sourceTree = ""; }; + CAC3A03D2C2F2CD8007BC888 /* STPPaymentMethodBillieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBillieTests.swift; sourceTree = ""; }; + CAC80EBE2C33339D001E3D0D /* STPPaymentMethodSatispayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSatispayTests.swift; sourceTree = ""; }; + CBC9D4B0158266B01840AD9A /* STPPaymentMethodOXXOParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodOXXOParamsTests.swift; sourceTree = ""; }; + CD5AC2BFBC8141F98C00CF9F /* StripeiOS_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeiOS_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CDDEAB86BE4711841D426F3B /* STPPaymentIntentFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentIntentFunctionalTest.swift; sourceTree = ""; }; + CF13BAEF86594C9CABD4F42A /* STPPaymentMethodUSBankAccountParamsStubbedTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodUSBankAccountParamsStubbedTest.swift; sourceTree = ""; }; + CF902DC49DD90860BD0E5E80 /* STPPaymentIntentParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentIntentParamsTest.swift; sourceTree = ""; }; + CFC4BC1AB047ED88C4D13C89 /* STPApplePayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayTest.swift; sourceTree = ""; }; + CFFE40AD9D875709F643D2E5 /* ConfirmButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonTests.swift; sourceTree = ""; }; + D07275F94914B7E7937D24FE /* NSDictionary+StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDictionary+StripeTest.swift"; sourceTree = ""; }; + D103BC590F1E0EC0C31C7B5F /* STPBankAccountTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBankAccountTest.swift; sourceTree = ""; }; + D1859673CAD068B345F5DD7D /* STPCardCVCInputTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldSnapshotTests.swift; sourceTree = ""; }; + D2F205F920E971DEA59E3C31 /* STPIntentActionTypeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionTypeTest.swift; sourceTree = ""; }; + D3667F97B7665F699D6704BE /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + D3803D0DED98501AA26B2EAC /* STPCardNumberInputTextFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardNumberInputTextFieldFormatterTests.swift; sourceTree = ""; }; + D38184A7CD27B978DFA30E69 /* STPCardFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardFunctionalTest.swift; sourceTree = ""; }; + D3D2DB08C335695B705F544C /* STPAddressViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddressViewModelTest.swift; sourceTree = ""; }; + D3E0CA28591EB0748C64D1FA /* STPFileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFileTest.swift; sourceTree = ""; }; + D41081066DC4465734F7FCD7 /* STPPaymentMethodNetBankingParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodNetBankingParamsTest.swift; sourceTree = ""; }; + D42F83F785EAF24F5DC7ED1A /* STPCardCVCInputTextFieldValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldValidatorTests.swift; sourceTree = ""; }; + D5C4A4CC7D2E9B5AB3EC3B79 /* STPPaymentOptionsInternalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionsInternalViewController.swift; sourceTree = ""; }; + D609FFD051FC01BF566665A0 /* StripeiOS Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Debug.xcconfig"; sourceTree = ""; }; + D794C5E6396B4A19DC4F6921 /* StripeUICore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeUICore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D7C4773C2D193BEDF1CBB530 /* STPCardNumberInputTextFieldValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardNumberInputTextFieldValidatorTests.swift; sourceTree = ""; }; + D87817A1D3D213AA4ADF6A4C /* STPPaymentMethodAffirmParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAffirmParamsTest.swift; sourceTree = ""; }; + D93C23F55BEADF9BC74DFBDB /* STPImageLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPImageLibrary.swift; sourceTree = ""; }; + DA82BB67D434E76B9ABA4CEC /* Stripe-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe-Release.xcconfig"; sourceTree = ""; }; + DAB817ED5B5DB87AE1290894 /* STPCustomerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCustomerTest.swift; sourceTree = ""; }; + DB58AC5E2E0A68221260FD44 /* STPMandateOnlineParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPMandateOnlineParamsTest.swift; sourceTree = ""; }; + DC3AD586DDED620B9E68F461 /* StripeErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeErrorTest.swift; sourceTree = ""; }; + DDC55CC034022DFAC9366E2E /* StripePaymentSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripePaymentSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DE24B3BAD7E890661CCA817D /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = ""; }; + DE2A766FB355DD9C461939C1 /* STPPaymentMethodPayPalParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodPayPalParamsTests.swift; sourceTree = ""; }; + DFA4E34E459EA612E065DE64 /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; + DFA7A75BA785EBBE4C05DAA3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + E1264C4DCB32B1FA5CE19201 /* STPFileFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFileFunctionalTest.swift; sourceTree = ""; }; + E136A967522048B313E3C62F /* StripePaymentsUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripePaymentsUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E1644AA33E81233EF33022BA /* STPCardValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardValidatorTest.swift; sourceTree = ""; }; + E1A2173E0891B7138687D544 /* Stripe-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe-Debug.xcconfig"; sourceTree = ""; }; + E1CD20E00EAD41091B71ABD5 /* NSURLComponents_StripeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSURLComponents_StripeTest.swift; sourceTree = ""; }; + E2307F2C5E53540D4ACAA1F6 /* UIToolbar+Stripe_InputAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIToolbar+Stripe_InputAccessory.swift"; sourceTree = ""; }; + E315168EF07F52B733EA77F8 /* STPSwiftFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSwiftFixtures.swift; sourceTree = ""; }; + E3B42EBAC0DC7ED0D9200DB7 /* STPBlocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBlocks.swift; sourceTree = ""; }; + E3C8833E7EBA7A1EBF349D19 /* StripeiOS Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Release.xcconfig"; sourceTree = ""; }; + E4194E605BB5F31E9CBB8F96 /* STPAPIClientStubbedTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAPIClientStubbedTest.swift; sourceTree = ""; }; + E452877E5D11120B1E28A6E7 /* STPApplePayContextDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayContextDelegate.swift; sourceTree = ""; }; + E5D9F97ABC88302478220267 /* PKPaymentAuthorizationViewController+Stripe_Blocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPaymentAuthorizationViewController+Stripe_Blocks.swift"; sourceTree = ""; }; + E6312182B5BCAB940D216650 /* ConsumerSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsumerSessionTests.swift; sourceTree = ""; }; + E68F6B90F3BC61A49570FAF4 /* UINavigationBar+StripeTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+StripeTest.m"; sourceTree = ""; }; + E6DEE912364C9F4B51B374D0 /* STPPaymentCardTextFieldViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentCardTextFieldViewModelTest.swift; sourceTree = ""; }; + E7ACB4FAFAD33296DE34D036 /* STPAnalyticsClientPaymentsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAnalyticsClientPaymentsTest.swift; sourceTree = ""; }; + E7C7D85A7FAAFDF4F59BA85E /* LinkSignupViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkSignupViewModelTests.swift; sourceTree = ""; }; + E8063E8073A32E0B081A1DFA /* STPPaymentMethodSofortTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodSofortTests.swift; sourceTree = ""; }; + E89551702F1F9A3AFF1ED676 /* STPSourceVerificationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceVerificationTest.swift; sourceTree = ""; }; + E956CFA6317CAA8B41E217CA /* STPAPIClient+LinkAccountSessionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+LinkAccountSessionTest.swift"; sourceTree = ""; }; + E97018A201D01A8FA59999C2 /* STPConnectAccountFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPConnectAccountFunctionalTest.swift; sourceTree = ""; }; + EA20E0E29EDC1F61ADA226A2 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; + EA9975553E669AF69F3CE437 /* STPPostalCodeInputTextFieldValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeInputTextFieldValidatorTests.swift; sourceTree = ""; }; + EAEE347A6372AAE2735FAD6F /* STPPaymentMethodFPXTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodFPXTest.swift; sourceTree = ""; }; + EB6AE83989B0596F0C111E13 /* STPStringUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStringUtilsTest.swift; sourceTree = ""; }; + EB71A4A2762CF864DB198BCF /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + ECBA5B09A3FD875C93218573 /* STPPaymentMethodAfterpayClearpayParamsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodAfterpayClearpayParamsTest.swift; sourceTree = ""; }; + EDD30E5DB8DB3AA3567F5C20 /* STPPaymentOptionTuple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionTuple.swift; sourceTree = ""; }; + EF48EC440E1ED5D6BAA567FF /* STPPaymentHandlerStubbedMockedFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentHandlerStubbedMockedFilesTests.swift; sourceTree = ""; }; + EFD2F6A5A046A620BAB75B41 /* AutoCompleteViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewControllerSnapshotTests.swift; sourceTree = ""; }; + F13E3DE09A463B4501733B87 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + F153420EA142B5CA76E89A04 /* STPMocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STPMocks.h; sourceTree = ""; }; + F372EDF9C2C45E1CA2C76866 /* STPPaymentMethodGiropayParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodGiropayParamsTests.swift; sourceTree = ""; }; + F3C732C25FD961631BD44FDD /* STPBSBNumberValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPBSBNumberValidatorTests.swift; sourceTree = ""; }; + F44327A2B2C9483F52EE343B /* STPFormViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFormViewSnapshotTests.swift; sourceTree = ""; }; + F4E5416F6AE8BED88980D6F8 /* STPPaymentMethodBancontactTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodBancontactTests.swift; sourceTree = ""; }; + F546088BA4F763334CFD3D34 /* STPImageLibraryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPImageLibraryTest.swift; sourceTree = ""; }; + F6558C62376C2397030BD4A6 /* STPPaymentMethodPrzelewy24Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodPrzelewy24Tests.swift; sourceTree = ""; }; + F7385193226663A5B79E69ED /* STPFloatingPlaceholderTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPFloatingPlaceholderTextFieldSnapshotTests.swift; sourceTree = ""; }; + FA793904C7B2D3AA0A4D5EFB /* STPAddCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPAddCardViewController.swift; sourceTree = ""; }; + FC30E6129279F14506219E98 /* STPPaymentHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentHandlerTests.swift; sourceTree = ""; }; + FD289E1EA9F0CE1C848AC0BB /* STPCardNumberInputTextFieldSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardNumberInputTextFieldSnapshotTests.swift; sourceTree = ""; }; + FD3398E2352CEA0264F20AEA /* stp_test_upload_image.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = stp_test_upload_image.jpeg; sourceTree = ""; }; + FDA32D0C9E8A7A69F4899EDC /* RotatingCardBrandsViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotatingCardBrandsViewSnapshotTests.swift; sourceTree = ""; }; + FDF7394DDD552EDE996EAD8E /* STPPostalCodeValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeValidatorTest.swift; sourceTree = ""; }; + FE2DED6ABA7407C17C1391B6 /* MockFiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MockFiles; sourceTree = ""; }; + FF5E08A1651D9DFE502DA021 /* STPCardFormViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardFormViewTests.swift; sourceTree = ""; }; + FF93C4B7A5F8FA4E7919794F /* STPPaymentMethodEPSParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodEPSParamsTests.swift; sourceTree = ""; }; + FFCF9EB77A45F3E9E83F5D8B /* STPRedirectContextTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPRedirectContextTest.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1587AD7E52759FBC80315765 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 32874C6147344A9CB2EF4DAD /* Stripe3DS2.framework in Frameworks */, + 8520A27C204A068C43592024 /* StripeApplePay.framework in Frameworks */, + E97168F37D769524B58461B6 /* StripeCore.framework in Frameworks */, + 360EEE8B706D2A4A49666F7A /* StripePayments.framework in Frameworks */, + 9363F8F389C04C19B37D0F0A /* StripePaymentsUI.framework in Frameworks */, + EA34719659CB9F1A269FECC7 /* StripeUICore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6ACBDD66E08F7CEE34A8FFFA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AC7C127B11A60222465F4696 /* XCTest.framework in Frameworks */, + D151C8724925DCBA4BA4F46A /* StripeCoreTestUtils.framework in Frameworks */, + 420F8CAB4FAD6D9AF4AF25C0 /* StripePaymentSheet.framework in Frameworks */, + EEA502DF8809B8FD0D00785E /* StripePayments.framework in Frameworks */, + E9C690F3629C0AC3CD0260AF /* StripePaymentsObjcTestUtils.framework in Frameworks */, + 5302F9246A4A6381CB4FB874 /* StripePaymentsTestUtils.framework in Frameworks */, + 03E60F9EF24C975AF90E2447 /* StripePaymentsUI.framework in Frameworks */, + 0FA3C1494BA57884B5DE3B20 /* Stripe.framework in Frameworks */, + 2EB68A59660A4D1E14799DA4 /* OHHTTPStubs in Frameworks */, + 2CD7968DA48F7129E16EA0CB /* OHHTTPStubsSwift in Frameworks */, + 5C51167CC14F653E7117BA61 /* OCMock in Frameworks */, + B795A5EB8FDECA1060A9655C /* iOSSnapshotTestCase in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7129AA5AD64208D3B1A76AAE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9291A08CCB34504FCA4B7481 /* XCTest.framework in Frameworks */, + 2F0FC4E67BE577AD66CD1475 /* StripePaymentSheet.framework in Frameworks */, + 78B70C2EE8334F0FA91439CA /* Stripe.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C04854A8658964DDD0A15E1C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00AA0CCB7109D2B82EF8EEA0 /* Project */ = { + isa = PBXGroup; + children = ( + D0F67D3A4FA2B67D2AB0A49F /* BuildConfigurations */, + 97BE66955C28DB78DA12F5F6 /* BuildConfigurations */, + B035E857851EAF160C88DC2B /* StripeiOS */, + 9C700DBA02FFEC23B92EF107 /* StripeiOSAppHostedTests */, + CFD249F84390F2310542962A /* StripeiOSTestHostApp */, + EB228B1D404A48918221D9D6 /* StripeiOSTests */, + 4E29B46F2C940E0A21734E09 /* StripeiOSTests.xctestplan */, + ); + name = Project; + sourceTree = ""; + }; + 437EB711F100257495D02BD7 /* Resources */ = { + isa = PBXGroup; + children = ( + 147D2DC1FFDFC99269039377 /* LaunchScreen.storyboard */, + 884C01B087B4D820395BD374 /* Main.storyboard */, + 6955B3A3353F8442E4FBBBF6 /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + 7EA3633D452F7A9C017F4D52 /* Source */ = { + isa = PBXGroup; + children = ( + 8F7E3CE2105E4A39032CD919 /* Enums+CustomStringConvertible.swift */, + 273EE407039913F0B644172B /* PKAddPaymentPassRequest+Stripe_Error.swift */, + E5D9F97ABC88302478220267 /* PKPaymentAuthorizationViewController+Stripe_Blocks.swift */, + FA793904C7B2D3AA0A4D5EFB /* STPAddCardViewController.swift */, + 498C3FB07CFD532779C755D3 /* STPAddress+BasicUI.swift */, + 72903593DC432D01720DC9D9 /* STPAddressFieldTableViewCell.swift */, + 28A50AFA4603E488FF3D82D0 /* STPAddressViewModel.swift */, + 12DBB3F72AEFB52DE27C27ED /* STPAnalyticsClient+BasicUI.swift */, + B6EC2F562B618A3D00FF72A2 /* STPBasicUIAnalyticsSerializer.swift */, + BD89580A3E41D7167C30B287 /* STPAnalyticsClient+Payments.swift */, + 1E8AFAE24610EC983727F860 /* STPAPIClient+BasicUI.swift */, + 807FF966F1DE05F3496B817B /* STPAPIClient+PushProvisioning.swift */, + E452877E5D11120B1E28A6E7 /* STPApplePayContextDelegate.swift */, + 03ACDC7EEC28D1FE50008F65 /* STPApplePayPaymentOption.swift */, + 9B782E1D974A4C131E60E2BD /* STPBackendAPIAdapter.swift */, + 458F8576215E0F8ECE1D74CE /* STPBankSelectionTableViewCell.swift */, + 81352A0CBE46A59E6B1A712E /* STPBankSelectionViewController.swift */, + E3B42EBAC0DC7ED0D9200DB7 /* STPBlocks.swift */, + 61F8308B7250B642D19827D8 /* STPCameraView.swift */, + 3E5FB20B2BEFC00D54FDD87D /* STPCard+BasicUI.swift */, + 69BD038947E8E2376A0D240B /* STPCardScanner.swift */, + 0B91C4D5B93FF71C61B140F1 /* STPCardScannerTableViewCell.swift */, + 6A9E7B637A8747431B38FD1D /* STPCardValidationState.swift */, + 2D1525AF65BDEF691F8BCBE8 /* STPCoreScrollViewController.swift */, + B78C72B0DB434EC7F700FDE0 /* STPCoreTableViewController.swift */, + 02FC9ED423D40C88D5A24441 /* STPCoreViewController.swift */, + BB8FCDBC63A79CD1571A2DFB /* STPCustomerContext.swift */, + 890660C21E3666CE7B82695B /* STPEphemeralKey.swift */, + 588C260880FFC584A00A89F5 /* STPEphemeralKeyManager.swift */, + 485E747DA1F72F091986787B /* STPEphemeralKeyProvider.swift */, + 0669B4CA326CE74D125C789C /* STPFakeAddPaymentPassViewController.swift */, + BA08DCDD421CE92ECB61EF5C /* STPFPXBankStatusResponse.swift */, + D93C23F55BEADF9BC74DFBDB /* STPImageLibrary.swift */, + 9EB24EC81CE2C8D1C863B044 /* STPIntentActionLinkAuthenticateAccount.swift */, + AFF957F38AABE5F748C38C0B /* STPLocalizedString.swift */, + 1FFBAA4B44967B157A4F4E91 /* STPPaymentActivityIndicatorView.swift */, + 01B42BE6FB5EC1F708875AB8 /* STPPaymentCardTextFieldCell.swift */, + 18FCB69CD3B8C3DAB216A5F0 /* STPPaymentConfiguration.swift */, + C750C2C4AB33BC232D1592BA /* STPPaymentContext.swift */, + A1C876DC7F3E31D7189506A8 /* STPPaymentContextAmountModel.swift */, + 6F01150CD0255164FE2CF3A4 /* STPPaymentIntentParams+BasicUI.swift */, + 35C1E9B0EE03825DABF6471A /* STPPaymentMethod+BasicUI.swift */, + 53C5AB22D6328E85A6DDF663 /* STPPaymentMethodParams+BasicUI.swift */, + 30B694A39D54886392AA5DE3 /* STPPaymentOption.swift */, + D5C4A4CC7D2E9B5AB3EC3B79 /* STPPaymentOptionsInternalViewController.swift */, + A9A1BB31C7B514984231125B /* STPPaymentOptionsViewController.swift */, + 6215A9BF343775B1BD0F62AF /* STPPaymentOptionTableViewCell.swift */, + EDD30E5DB8DB3AA3567F5C20 /* STPPaymentOptionTuple.swift */, + 7D964D9E01627B419B4BD23C /* STPPaymentResult.swift */, + 3EBB07171F6FDCE6E20C454A /* STPPinManagementService.swift */, + 6887F19BB9804BF45FD703FF /* STPPushProvisioningContext.swift */, + 89E5DA3029F141B5111A5B2C /* STPPushProvisioningDetails.swift */, + 2B28B8A547CD846277ECD578 /* STPPushProvisioningDetailsParams.swift */, + 96E1FED5CE5974C9C1162E93 /* STPSectionHeaderView.swift */, + 71711FC8E2FB66E52A5FDD9A /* STPShippingAddressViewController.swift */, + 21E4B84223DDA131544DBBA7 /* STPShippingMethodsViewController.swift */, + 98544B08552407D41D398C68 /* STPShippingMethodTableViewCell.swift */, + C3B75875C55D2C2723DC5090 /* STPSource+BasicUI.swift */, + 8E8CA4361964E1BA400EFC89 /* STPTheme.swift */, + B1217AD643A9E8F88B60F645 /* STPUserInformation.swift */, + A5398E1156E0BFEBBF56FD2F /* String+Localized.swift */, + B70DF0B659009041F485EE0F /* Stripe+Exports.swift */, + B4D31B0D7BD9F97AF3BB61E6 /* StripeBundleLocator.swift */, + 3B3000668A75E095B514241F /* UIBarButtonItem+Stripe.swift */, + A7E369CEC9F5B3758F78E88F /* UINavigationBar+Stripe_Theme.swift */, + 2C078573F46762353664AC92 /* UINavigationController+Stripe_Completion.swift */, + 5476BD87E0480A93958F0328 /* UITableViewCell+Stripe_Borders.swift */, + E2307F2C5E53540D4ACAA1F6 /* UIToolbar+Stripe_InputAccessory.swift */, + 1B76DF0FE363F59BF0940A8B /* UIView+Stripe_FirstResponder.swift */, + 86798C95A778362EF815B4C6 /* UIView+Stripe_SafeAreaBounds.swift */, + 2F08757CA6F6B2DA65C14E0A /* UIViewController+Stripe_KeyboardAvoiding.swift */, + 6F017A08C7E633FB4297D274 /* UIViewController+Stripe_NavigationItemProxy.swift */, + 5804C2B9C0704E386B3D25A4 /* UIViewController+Stripe_ParentViewController.swift */, + 26302721419496F37DE91DF8 /* UserDefaults+Stripe.swift */, + ); + path = Source; + sourceTree = ""; + }; + 85B4F3432AB3A8B94C9D2591 /* Products */ = { + isa = PBXGroup; + children = ( + 4259421D2CD26E37B96F97B2 /* Stripe.framework */, + 33FDC634FD5D79E824240DDC /* Stripe3DS2.framework */, + 52F8AEC50D4623F80F04A533 /* StripeApplePay.framework */, + 43B4E4B85C598D7A9AFCB4D4 /* StripeCore.framework */, + 512A0E7C246D5F044245E069 /* StripeCoreTestUtils.framework */, + CD5AC2BFBC8141F98C00CF9F /* StripeiOS_Tests.xctest */, + 1DD6897858F46976A946394E /* StripeiOSAppHostedTests.xctest */, + 294CD46E24BB2743042872D7 /* StripeiOSTestHostApp.app */, + 22D1C6EB5826E2D7C80B6CF3 /* StripePayments.framework */, + DDC55CC034022DFAC9366E2E /* StripePaymentSheet.framework */, + 5D237177A7F99EB0F5F4F5E4 /* StripePaymentsObjcTestUtils.framework */, + 77247622AB08FEF48CA0DC26 /* StripePaymentsTestUtils.framework */, + E136A967522048B313E3C62F /* StripePaymentsUI.framework */, + D794C5E6396B4A19DC4F6921 /* StripeUICore.framework */, + ); + name = Products; + sourceTree = ""; + }; + 97BE66955C28DB78DA12F5F6 /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + EB71A4A2762CF864DB198BCF /* Project-Debug.xcconfig */, + 969E196AB597EEF68C38103E /* Project-Release.xcconfig */, + D609FFD051FC01BF566665A0 /* StripeiOS Tests-Debug.xcconfig */, + E3C8833E7EBA7A1EBF349D19 /* StripeiOS Tests-Release.xcconfig */, + ); + name = BuildConfigurations; + path = ../BuildConfigurations; + sourceTree = ""; + }; + 9C700DBA02FFEC23B92EF107 /* StripeiOSAppHostedTests */ = { + isa = PBXGroup; + children = ( + 65DCC9BDA647E58D3882C698 /* Info.plist */, + ); + path = StripeiOSAppHostedTests; + sourceTree = ""; + }; + AF41AE099441C2A09DECC1AC /* Localizations */ = { + isa = PBXGroup; + children = ( + C2427C1CDFA85BFC6570F1E9 /* Localizable.strings */, + ); + path = Localizations; + sourceTree = ""; + }; + B035E857851EAF160C88DC2B /* StripeiOS */ = { + isa = PBXGroup; + children = ( + 31CDFC2D2BA3708000B3DD91 /* PrivacyInfo.xcprivacy */, + 313F5F782B0BE59000BD98A9 /* Docs.docc */, + FBC7A77342A98B1DE8E416B7 /* Resources */, + 7EA3633D452F7A9C017F4D52 /* Source */, + C5923A4DD3CD39CB64B8A8C9 /* Info.plist */, + A8598727045C6268B57A5FC7 /* Stripe-umbrella.h */, + ); + path = StripeiOS; + sourceTree = ""; + }; + B7A38600F42999DA8F83AD27 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 005650A59D692F820EF20F5F /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + CFD249F84390F2310542962A /* StripeiOSTestHostApp */ = { + isa = PBXGroup; + children = ( + 437EB711F100257495D02BD7 /* Resources */, + 9BBCE3A905041A709E8F279A /* AppDelegate.swift */, + B3E0745D13EB19BAA24F3BA3 /* Info.plist */, + 1F29C15B47C7CB0941CD4C9E /* ViewController.swift */, + ); + path = StripeiOSTestHostApp; + sourceTree = ""; + }; + D0F67D3A4FA2B67D2AB0A49F /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + 1D23EB567F573612E0794B3A /* Stripe Tests-Debug.xcconfig */, + 5E4EA6394497D1BD57ED0032 /* Stripe Tests-Release.xcconfig */, + E1A2173E0891B7138687D544 /* Stripe-Debug.xcconfig */, + DA82BB67D434E76B9ABA4CEC /* Stripe-Release.xcconfig */, + ); + path = BuildConfigurations; + sourceTree = ""; + }; + E4802964A9471C082CE01BA9 = { + isa = PBXGroup; + children = ( + 00AA0CCB7109D2B82EF8EEA0 /* Project */, + B7A38600F42999DA8F83AD27 /* Frameworks */, + 85B4F3432AB3A8B94C9D2591 /* Products */, + ); + sourceTree = ""; + }; + EB228B1D404A48918221D9D6 /* StripeiOSTests */ = { + isa = PBXGroup; + children = ( + EB90CE5EA5682921B4C247A0 /* Resources */, + 3C742844915B96CFD25BFFF9 /* AfterpayPriceBreakdownViewSnapshotTests.swift */, + 61AF6E95FE0DD913204CAB32 /* AnalyticsHelperTests.swift */, + 85AAB72218409F85FE29E69E /* APIRequestTest.swift */, + EFD2F6A5A046A620BAB75B41 /* AutoCompleteViewControllerSnapshotTests.swift */, + 153071C69A0BEE033E035DCF /* CardExpiryDateTests.swift */, + 46AC0B5EC7433E081825D31B /* CircularButtonSnapshotTests.swift */, + 806124200E77795DCFC8418E /* ConfirmButtonSnapshotTests.swift */, + CFFE40AD9D875709F643D2E5 /* ConfirmButtonTests.swift */, + E6312182B5BCAB940D216650 /* ConsumerSessionTests.swift */, + 11C866064B3482878A69892F /* CustomerAdapterTests.swift */, + 8CE85C770AEEDBE4AEC93EAA /* Error+PaymentSheetTests.swift */, + 180CF848E3ABF0236C494D8B /* FBSnapshotTestCase+STPViewControllerLoading.swift */, + 7C04AFC9CDE50D09D38A3232 /* FormSpecProviderTest.swift */, + 45FF7A07CFC3B9B7AD6B49EE /* FraudDetectionDataTest.swift */, + AAF368BCD5990EE5DC17D299 /* ImageTest.swift */, + 0E10C4D64477638398251FFB /* Info.plist */, + 610DF5DB2B33597500DA6AAA /* HostedSurfaceTest.swift */, + 6BC5EC2C2B4609FF00CC75E8 /* LinkInlineSignupElementSnapshotTests.swift */, + 835CB781FBC19773ACC20676 /* LinkLegalTermsViewSnapshotTests.swift */, + E7C7D85A7FAAFDF4F59BA85E /* LinkSignupViewModelTests.swift */, + 9DCCA0E8A02B4F4B23837FB4 /* MKPlacemark+PaymentSheetTests.swift */, + C17799DC7FA54E758EED31A6 /* NSArray+StripeTest.swift */, + 20552E792B8E7BA15821AB5D /* NSDecimalNumber+StripeTest.swift */, + D07275F94914B7E7937D24FE /* NSDictionary+StripeTest.swift */, + C641A744CCEA67C07E9BFE05 /* NSLocale+STPSwizzling.swift */, + 63F5F35DB97D8A176FB6ED24 /* NSString+StripeTest.swift */, + E1CD20E00EAD41091B71ABD5 /* NSURLComponents_StripeTest.swift */, + 9D85FA7B714BDD8D1FD83B75 /* OneTimeCodeTextFieldSnapshotTests.swift */, + 98F9CB667BC68767DFB5FACD /* OneTimeCodeTextFieldTests.swift */, + A61763BA2CCA86F9B8FD4F1F /* OperationDebouncerTests.swift */, + C8F8FCC84601E4ADC6B7F3CE /* PaymentAnalyticTest.swift */, + 7B9A4A2B0FB9F8C743BBED48 /* PaymentTypeCellSnapshotTests.swift */, + AF342CBC167F9CAB5B49CC32 /* PayWithLinkButtonSnapshotTests.swift */, + B4D12508C2F1056A7EAFEC86 /* PKPayment+StripeTest.swift */, + FDA32D0C9E8A7A69F4899EDC /* RotatingCardBrandsViewSnapshotTests.swift */, + 12632E6710DE8861CAF1BAA4 /* RotatingCardBrandsViewTests.swift */, + 20879436DCFB1F03BE1608B3 /* ServerErrorMapperTest.swift */, + 26E5C1DABAA63F07F0C6AE37 /* STPAddCardViewControllerLocalizationSnapshotTests.swift */, + 8704ABFA91A5226847F4A69A /* STPAddCardViewControllerTest.swift */, + A39580123A4F1EA96F91768A /* STPAddressTests.swift */, + D3D2DB08C335695B705F544C /* STPAddressViewModelTest.swift */, + E7ACB4FAFAD33296DE34D036 /* STPAnalyticsClientPaymentsTest.swift */, + E956CFA6317CAA8B41E217CA /* STPAPIClient+LinkAccountSessionTest.swift */, + B9BA8D8467218C7E691C9FAE /* STPAPIClientNetworkBridgeTest.swift */, + E4194E605BB5F31E9CBB8F96 /* STPAPIClientStubbedTest.swift */, + B8DD70E5ED8E9DE8E9752C9E /* STPAPIClientTest.swift */, + B669C53A601CD3CB0203A4B9 /* STPAPISettingsObjCBridgeTest.m */, + 1CE268457D21A7209862E004 /* STPApplePayContextFunctionalTest.swift */, + A1272F2E05A0E294DD9ECA26 /* STPApplePayContextFunctionalTestExtras.swift */, + 5F7AB40A5A10C2D267323ABE /* STPApplePayContextTest.swift */, + 3C77C7BC4BA57EC296CF2F1C /* STPApplePayFunctionalTest.swift */, + 00985EFC6CB7B912FDBF3813 /* STPApplePayPaymentOptionTest.swift */, + CFC4BC1AB047ED88C4D13C89 /* STPApplePayTest.swift */, + BCBEA9E4823F08C1F5057B5A /* STPAUBECSDebitFormViewSnapshotTests.swift */, + 4AAE5EE11611F9F7762B64C6 /* STPAUBECSFormViewModelTests.swift */, + 989411FA3CD0CCC38BC227F4 /* STPBankAccountFunctionalTest.swift */, + 967C784618A074FF021B3089 /* STPBankAccountParamsTest.swift */, + D103BC590F1E0EC0C31C7B5F /* STPBankAccountTest.swift */, + 23997D61DF41CA84BFC33080 /* STPBECSDebitAccountNumberValidatorTests.swift */, + 3FC0560A312147C37CFE6CF9 /* STPBinRangeTest.swift */, + 4FFA8B446217CDE678D7287F /* STPBlocks.h */, + F3C732C25FD961631BD44FDD /* STPBSBNumberValidatorTests.swift */, + 3E1C5E08678292561255B1C5 /* STPCardBINMetadataTests.swift */, + 901BE31021AF27DB5D326327 /* STPCardBrandTest.swift */, + 6E1F6514E7530C2A3478B2F5 /* STPCardCVCInputTextFieldFormatterTests.swift */, + D1859673CAD068B345F5DD7D /* STPCardCVCInputTextFieldSnapshotTests.swift */, + 94A7104C1C470515616E4D2B /* STPCardCVCInputTextFieldTests.swift */, + D42F83F785EAF24F5DC7ED1A /* STPCardCVCInputTextFieldValidatorTests.swift */, + 40BB87E28719FE0C6B946BB5 /* STPCardExpiryInputTextFieldFormatterTests.swift */, + 0C75157665428685C7A4FD20 /* STPCardExpiryInputTextFieldSnapshotTests.swift */, + 4418164D75002AE6A0273176 /* STPCardExpiryInputTextFieldValidatorTests.swift */, + 1D983E089196152DA1C69469 /* STPCardFormViewSnapshotTests.swift */, + FF5E08A1651D9DFE502DA021 /* STPCardFormViewTests.swift */, + D38184A7CD27B978DFA30E69 /* STPCardFunctionalTest.swift */, + D3803D0DED98501AA26B2EAC /* STPCardNumberInputTextFieldFormatterTests.swift */, + FD289E1EA9F0CE1C848AC0BB /* STPCardNumberInputTextFieldSnapshotTests.swift */, + D7C4773C2D193BEDF1CBB530 /* STPCardNumberInputTextFieldValidatorTests.swift */, + 8A3F2B714DB3D1DED561A7EF /* STPCardParamsTest.swift */, + 1D575C31524E596E9C1A8E9B /* STPCardTest.swift */, + E1644AA33E81233EF33022BA /* STPCardValidatorTest.swift */, + 1D51B04D83D4FEF7F90DF16A /* STPCertTest.swift */, + BD77D4B1C5B64E45F9DA09B5 /* STPConfirmCardOptionsTest.swift */, + 05FE1BA89B80336F16924FA2 /* STPConfirmPaymentMethodOptionsTest.swift */, + 967DDC94B687B14E07842CC8 /* STPConnectAccountAddressTest.swift */, + E97018A201D01A8FA59999C2 /* STPConnectAccountFunctionalTest.swift */, + 195DEC752CC82CC4BA1E2351 /* STPConnectAccountParamsTest.swift */, + 8D49257A97E71A475A9F6E08 /* STPCountryPickerInputFieldSnapshotTests.swift */, + 2CC4B06AB5C02FF54091E5A8 /* STPCustomerContextTest.swift */, + DAB817ED5B5DB87AE1290894 /* STPCustomerTest.swift */, + 1C1548BA518F7AC2A9ECF9D5 /* STPE2ETest.swift */, + C645F78B3EFFAA083B6FD3E9 /* STPEphemeralKeyManagerTest.swift */, + C5BEAA15B53AC5662A33D0E1 /* STPEphemeralKeyTest.swift */, + 9EAE9A2AE65771403CE57C11 /* STPErrorBridgeTest.m */, + E1264C4DCB32B1FA5CE19201 /* STPFileFunctionalTest.swift */, + D3E0CA28591EB0748C64D1FA /* STPFileTest.swift */, + F7385193226663A5B79E69ED /* STPFloatingPlaceholderTextFieldSnapshotTests.swift */, + 30964128998473CAA9F2DD7E /* STPFormEncoderTest.swift */, + 2E1862744F23286D1FB9D4AE /* STPFormTextFieldTest.swift */, + F44327A2B2C9483F52EE343B /* STPFormViewSnapshotTests.swift */, + 85C29AE44D809CD677B5E52B /* STPFPXBankBrandTest.swift */, + 284C67269D2606DA147AE01D /* STPGenericInputPickerFieldSnapshotTests.swift */, + 2D63B73C5773432CA134D1FC /* STPGenericInputPickerFieldValidatorTest.swift */, + 58A53F005EA8FDDAA66126BA /* STPGenericInputTextFieldSnapshotTests.swift */, + F546088BA4F763334CFD3D34 /* STPImageLibraryTest.swift */, + A22E5B87755C1F05C3DB438C /* STPInputTextFieldFormatterTests.swift */, + 9FEE395C4DD0E0112AF3720C /* STPInputTextFieldValidatorTests.swift */, + AD06AED0AF8A9A7FB4A2E66F /* STPIntentActionAlipayHandleRedirectTest.swift */, + 7A517686D4D12691351311CA /* STPIntentActionPayNowDisplayQrCodeTest.swift */, + 61E1CA262BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift */, + 9466F23BA8712EA2EDA48BBD /* STPIntentActionPromptPayDisplayQrCodeTest.swift */, + 33B9D01D037909D1C9C0B617 /* STPIntentActionTest.swift */, + D2F205F920E971DEA59E3C31 /* STPIntentActionTypeTest.swift */, + 4CA5D11C977A95B8E936E907 /* STPIntentActionWeChatPayRedirectToAppTest.swift */, + 78D1EEABBAE5BD5615486B0F /* STPLabeledFormTextFieldViewSnapshotTests.swift */, + 7F68637E75142DCD46710796 /* STPLabeledMultiFormTextFieldViewSnapshotTests.swift */, + C713F58BC61A962C720AE0AE /* STPMandateCustomerAcceptanceParamsTest.swift */, + B7F7AA0B7B86BA5BB2FE92CE /* STPMandateDataParamsTest.swift */, + DB58AC5E2E0A68221260FD44 /* STPMandateOnlineParamsTest.swift */, + F153420EA142B5CA76E89A04 /* STPMocks.h */, + 88AEABC15CCBB9EA393C175F /* STPMocks.m */, + 6223E57D3A198F956A37ED89 /* STPNumericDigitInputTextFormatterTests.swift */, + 4AA36705DED9164663A98B6A /* STPNumericStringValidatorTests.swift */, + BB08D2AC882B21C8ADD76B92 /* STPPaymentCardTextFieldKVOTest.m */, + 939360978872BBE4334215B1 /* STPPaymentCardTextFieldTest.swift */, + 1F16C36797D978E72E612100 /* STPPaymentCardTextFieldTestsSwift.swift */, + E6DEE912364C9F4B51B374D0 /* STPPaymentCardTextFieldViewModelTest.swift */, + 79ABE6A14AF9D14103050876 /* STPPaymentConfigurationTest.m */, + 3C995125252BED1EEC018B9D /* STPPaymentContextApplePayTest.swift */, + B5B86F3355E44DF4A980B82C /* STPPaymentContextSnapshotTests.swift */, + 683F7735569D22CBEC9CA2E6 /* STPPaymentHandlerFunctionalTest.m */, + ACF450AD17FF7BCE5916DDF1 /* STPPaymentHandlerFunctionalTest.swift */, + EF48EC440E1ED5D6BAA567FF /* STPPaymentHandlerStubbedMockedFilesTests.swift */, + FC30E6129279F14506219E98 /* STPPaymentHandlerTests.swift */, + 61E0A0C42BF31D3C00C89786 /* STPPaymentHandlerRefreshTests.swift */, + 46C31CAD0FA74B58BA2B8530 /* STPPaymentIntentEnumsTest.swift */, + CDDEAB86BE4711841D426F3B /* STPPaymentIntentFunctionalTest.swift */, + 6C7B8DACB0A7294BC235E3BC /* STPPaymentIntentLastPaymentErrorTest.swift */, + CF902DC49DD90860BD0E5E80 /* STPPaymentIntentParamsTest.swift */, + ACE8998EAF997A78759E49B5 /* STPPaymentIntentTest.swift */, + BEB3F9F0228008BE213706DF /* STPPaymentMethodAddressTest.swift */, + D87817A1D3D213AA4ADF6A4C /* STPPaymentMethodAffirmParamsTest.swift */, + 1F0DF2ED9232A7CC51F5FCB1 /* STPPaymentMethodAffirmTests.swift */, + ECBA5B09A3FD875C93218573 /* STPPaymentMethodAfterpayClearpayParamsTest.swift */, + 3CDD1E823223F450193E8746 /* STPPaymentMethodAfterpayClearpayTest.swift */, + 7FF2B9FF57E100301B5C38DB /* STPPaymentMethodAUBECSDebitParamsTests.swift */, + 54426CBF6F77ABEFBDFDA8C4 /* STPPaymentMethodAUBECSDebitTests.swift */, + BF1CF8FB0100664A02468FBC /* STPPaymentMethodBacsDebitTest.swift */, + 655209238C85F466F9F14F14 /* STPPaymentMethodBancontactParamsTests.swift */, + F4E5416F6AE8BED88980D6F8 /* STPPaymentMethodBancontactTests.swift */, + A0E24B5689732EB9106DA232 /* STPPaymentMethodBillingDetailsTest.swift */, + 7AC0A18C441FCA394BEF6A3D /* STPPaymentMethodBillingDetailsTests+Link.swift */, + A1C67AC5D415615E9F27D3E3 /* STPPaymentMethodBoletoParamsTests.swift */, + 4E371E9B3B2E343FE954531C /* STPPaymentMethodBoletoTests.swift */, + A15BBFFA401852A8719E3DDD /* STPPaymentMethodCardChecksTest.swift */, + 1671EC46C713D51013AD7D8B /* STPPaymentMethodCardParamsTest.swift */, + 8BD02D8298877F10F2EF2A9D /* STPPaymentMethodCardTest.swift */, + 533538E3EB92E326CCB95506 /* STPPaymentMethodCardWalletMasterpassTest.swift */, + 2DC4B62C336EAA05A33FC384 /* STPPaymentMethodCardWalletTest.swift */, + 82E46B18CDDC191934F3D4BE /* STPPaymentMethodCardWalletVisaCheckoutTest.swift */, + 0ABB2CA7E96BE249CE8C0566 /* STPPaymentMethodCashAppParamsTests.swift */, + 61152B4E2B866827003B69A0 /* STPPaymentMethodAmazonPayParamsTests.swift */, + 617C1C892BB4998C00B10AC5 /* STPPaymentMethodAlmaParamsTests.swift */, + 61E1CA202BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift */, + 7DDE50CBC86AD77084C877B6 /* STPPaymentMethodCashAppTests.swift */, + 61951FB82B866BA1005F90BE /* STPPaymentMethodAmazonPayTests.swift */, + 617C1C872BB4992400B10AC5 /* STPPaymentMethodAlmaTests.swift */, + 61E1CA1E2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift */, + FF93C4B7A5F8FA4E7919794F /* STPPaymentMethodEPSParamsTests.swift */, + 0C44B4366D6C4FD4B11662C8 /* STPPaymentMethodEPSTests.swift */, + EAEE347A6372AAE2735FAD6F /* STPPaymentMethodFPXTest.swift */, + 8192839B0F1AE9D9F2A94504 /* STPPaymentMethodFunctionalTest.swift */, + F372EDF9C2C45E1CA2C76866 /* STPPaymentMethodGiropayParamsTests.swift */, + 583DB466066B47C0F716E474 /* STPPaymentMethodGiropayTests.swift */, + 0A8FF64CA314F909D7EC82FE /* STPPaymentMethodGrabPayParamsTest.swift */, + 9B83930B631CF8EADFB606D6 /* STPPaymentMethodiDEALTest.swift */, + 47E12A0CBFA259A032F7AF0C /* STPPaymentMethodKlarnaParamsTests.swift */, + 4FD94FF270165D699DA89B24 /* STPPaymentMethodKlarnaTests.swift */, + 6BA4B9192BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift */, + 6BA4B91B2BF4343B00D1F21D /* STPPaymentMethodMobilePayTests.swift */, + D41081066DC4465734F7FCD7 /* STPPaymentMethodNetBankingParamsTest.swift */, + 739934737B9A09775CD278C9 /* STPPaymentMethodNetBankingTests.swift */, + 1E2638F7AA0906914117C2D5 /* STPPaymentMethodOptionsTest.swift */, + CBC9D4B0158266B01840AD9A /* STPPaymentMethodOXXOParamsTests.swift */, + 80BBCC4D386EE44E809A591C /* STPPaymentMethodOXXOTests.swift */, + 4002981AC12687681616D21E /* STPPaymentMethodParamsTest.swift */, + DE2A766FB355DD9C461939C1 /* STPPaymentMethodPayPalParamsTests.swift */, + 2A63AC868755CB4745E7458E /* STPPaymentMethodPayPalTests.swift */, + A6269E77F81C32A5EC8BE412 /* STPPaymentMethodPrzelewy24ParamsTests.swift */, + F6558C62376C2397030BD4A6 /* STPPaymentMethodPrzelewy24Tests.swift */, + 3AE7BEADD3824A06C2994854 /* STPPaymentMethodRevolutPayParamsTests.swift */, + 7A7963CC618A1A1346EC20C7 /* STPPaymentMethodRevolutPayTests.swift */, + A41F721AEBB942BB81408A59 /* STPPaymentMethodSEPADebitTest.swift */, + 207677E2A0DBC04C88139372 /* STPPaymentMethodSofortParamsTests.swift */, + E8063E8073A32E0B081A1DFA /* STPPaymentMethodSofortTests.swift */, + 9631915F03F157A1CC3FEFFE /* STPPaymentMethodSwishParamsTests.swift */, + 5DE9F0BAC35EA14579775033 /* STPPaymentMethodSwishTests.swift */, + 148C1D7D1BBBC6B74894A869 /* STPPaymentMethodTest.swift */, + 916DB8789F65D3C1BCB510C0 /* STPPaymentMethodThreeDSecureUsageTest.swift */, + 47E5E1173A37AABB07FB68AB /* STPPaymentMethodUPIParamsTest.swift */, + 940209E5D30E86E856016906 /* STPPaymentMethodUPITests.swift */, + CF13BAEF86594C9CABD4F42A /* STPPaymentMethodUSBankAccountParamsStubbedTest.swift */, + 74FDEF9F687C63BADFB96480 /* STPPaymentMethodUSBankAccountParamsTest.swift */, + 3B112FFF3FCA82094281493F /* STPPaymentMethodUSBankAccountTest.swift */, + 5062915D961C1BCAFD641FFE /* STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift */, + A2922A32A754CFC9AB8B48AE /* STPPaymentOptionsViewControllerTest.swift */, + 924E878428D15506711CA628 /* STPPhoneNumberValidatorTest.swift */, + 1645D9793463E266501B74FD /* STPPIIFunctionalTest.swift */, + 336CC555B845DED30208D39D /* STPPinManagementServiceFunctionalTest.swift */, + 917154477796779ECFA1334A /* STPPostalCodeInputTextFieldFormatterTests.swift */, + 090EF7D598B8DE779C275395 /* STPPostalCodeInputTextFieldSnapshotTests.swift */, + 6618739767139C25C05B3631 /* STPPostalCodeInputTextFieldTests.swift */, + EA9975553E669AF69F3CE437 /* STPPostalCodeInputTextFieldValidatorTests.swift */, + FDF7394DDD552EDE996EAD8E /* STPPostalCodeValidatorTest.swift */, + 1A8A6B88797870BC71CCB3AF /* STPPushProvisioningDetailsFunctionalTest.swift */, + 2D878F923A1F69B58D6B2812 /* STPRadarSessionFunctionalTest.swift */, + FFCF9EB77A45F3E9E83F5D8B /* STPRedirectContextTest.swift */, + 6CC0B1FC92A573AAEA4F4E94 /* STPSetupIntentConfirmParamsTest.swift */, + 49AA313E068FB99CEAA5F7D3 /* STPSetupIntentFunctionalTest.swift */, + 01B057D99A14E5BA6019C349 /* STPSetupIntentLastSetupErrorTest.swift */, + CA8B8F540CD05B3DC2C5EEA6 /* STPSetupIntentTest.swift */, + 1ED6BAC91E8A827DCDB38B15 /* STPShippingAddressViewControllerLocalizationSnapshotTests.swift */, + 789D0B49B0788794739E3DD4 /* STPShippingAddressViewControllerTest.swift */, + 661976D1296DFA48A25E0493 /* STPShippingMethodsViewControllerLocalizationSnapshotTests.swift */, + 63114D0EAAE2606732DF5AA0 /* STPSourceCardDetailsTest.swift */, + 17013F78CE3F9662029FEF5B /* STPSourceFunctionalTest.swift */, + 095EBF095BA2BC8D299547DB /* STPSourceOwnerTest.swift */, + 1F6CB4B8FAD14B4D70A63595 /* STPSourceParamsTest.swift */, + 6B7A947152A728EB2CBC4DB2 /* STPSourceReceiverTest.swift */, + C0436A8574E7D0730641407A /* STPSourceRedirectTest.swift */, + 21780C410D22264B7C299520 /* STPSourceSEPADebitDetailsTest.swift */, + 05AA6A1B2A462F1CE2F537C5 /* STPSourceTest.swift */, + E89551702F1F9A3AFF1ED676 /* STPSourceVerificationTest.swift */, + 51E62BB62EA9B782778CA880 /* STPStackViewWithSeparatorSnapshotTests.swift */, + EB6AE83989B0596F0C111E13 /* STPStringUtilsTest.swift */, + E315168EF07F52B733EA77F8 /* STPSwiftFixtures.swift */, + 2A8A2CD759D465290066EF65 /* STPTextFieldDelegateProxyTests.swift */, + B6F3B966470A530E0DC53F8C /* STPThreeDSButtonCustomizationTest.swift */, + 483B243268646AE65B06E98C /* STPThreeDSFooterCustomizationTest.swift */, + 5FDD4956E0A04D33F0856F31 /* STPThreeDSLabelCustomizationTest.swift */, + 51408DE266D0345784ADD4FA /* STPThreeDSNavigationBarCustomizationTest.swift */, + C23D612FD5AD7772E1B30DCC /* STPThreeDSSelectionCustomizationTest.swift */, + A583966A33DCDCF04322A592 /* STPThreeDSTextFieldCustomizationTest.swift */, + 3ED44491EB0AC72B1B1A773C /* STPThreeDSUICustomizationTest.swift */, + 7F090B8D315E7FD12A5F9C09 /* STPTokenTest.swift */, + 88639E3C3AC622B5EF475538 /* STPUIVCStripeParentViewControllerTests.swift */, + 86983B09A1712944EC012AD4 /* STPViewWithSeparatorSnapshotTests.swift */, + DC3AD586DDED620B9E68F461 /* StripeErrorTest.swift */, + B407FE2D39775902A95B1118 /* StripeiOS Tests-Bridging-Header.h */, + 51BD2CE41E4F0CF648F44E4A /* TextFieldElement+IBANTest.swift */, + E68F6B90F3BC61A49570FAF4 /* UINavigationBar+StripeTest.m */, + 3B0E131538728BC4802627B1 /* UserDefaults+StripeTest.swift */, + 0DB03E83746FE78361831546 /* WalletHeaderViewSnapshotTests.swift */, + CAC3A0332C2F176A007BC888 /* STPPaymentMethodSunbitTests.swift */, + CAC3A03D2C2F2CD8007BC888 /* STPPaymentMethodBillieTests.swift */, + CAC80EBE2C33339D001E3D0D /* STPPaymentMethodSatispayTests.swift */, + ); + path = StripeiOSTests; + sourceTree = ""; + }; + EB90CE5EA5682921B4C247A0 /* Resources */ = { + isa = PBXGroup; + children = ( + DFA7A75BA785EBBE4C05DAA3 /* Images.xcassets */, + FE2DED6ABA7407C17C1391B6 /* MockFiles */, + FD3398E2352CEA0264F20AEA /* stp_test_upload_image.jpeg */, + ); + path = Resources; + sourceTree = ""; + }; + FBC7A77342A98B1DE8E416B7 /* Resources */ = { + isa = PBXGroup; + children = ( + AF41AE099441C2A09DECC1AC /* Localizations */, + 77E846CD56018D8417A3AB95 /* StripeiOS.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8D1F9C232F0DF9D129FE872E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 30D48C62B2FA6B28EC23A5BB /* Stripe-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 1628E8B14F1F7C63CF8C9962 /* StripeiOSTestHostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6D8846CE184BBD9139D6D15 /* Build configuration list for PBXNativeTarget "StripeiOSTestHostApp" */; + buildPhases = ( + C4ED1B5A417CECDB38B21698 /* Sources */, + 9BE857B21623552BA4F3FAA1 /* Resources */, + 9F30C6D40956861F588C2CD0 /* Embed Frameworks */, + C04854A8658964DDD0A15E1C /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StripeiOSTestHostApp; + productName = StripeiOSTestHostApp; + productReference = 294CD46E24BB2743042872D7 /* StripeiOSTestHostApp.app */; + productType = "com.apple.product-type.application"; + }; + 49A14BBD10B1E97F2B43C448 /* StripeiOSAppHostedTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FCBB942DEBE57423D79373B4 /* Build configuration list for PBXNativeTarget "StripeiOSAppHostedTests" */; + buildPhases = ( + E160E75F2870000E2FA9C5DC /* Sources */, + C501B175E87CBBB0075EB9C5 /* Resources */, + 16FB342935F756BD2EA7CE4C /* Embed Frameworks */, + 7129AA5AD64208D3B1A76AAE /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 4E57D9EF1E91091C57E49111 /* PBXTargetDependency */, + 11BFD72CD1F291E0E86EC784 /* PBXTargetDependency */, + ); + name = StripeiOSAppHostedTests; + productName = StripeiOSAppHostedTests; + productReference = 1DD6897858F46976A946394E /* StripeiOSAppHostedTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 8BE23AD5D9A3D939AF46F31E /* StripeiOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 54F4DB1E9C991950FD9EB4B4 /* Build configuration list for PBXNativeTarget "StripeiOSTests" */; + buildPhases = ( + FB03810F22F4E0919BB2EF68 /* Sources */, + BADDFD2C3E190FF24FEB93C7 /* Resources */, + 5789BE2CD297329ED86678C0 /* Embed Frameworks */, + 6ACBDD66E08F7CEE34A8FFFA /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C4F1FB6EFF8D25D54580374A /* PBXTargetDependency */, + ); + name = StripeiOSTests; + packageProductDependencies = ( + 62887B4538E4E41E735685E1 /* OHHTTPStubs */, + 911CA85A1610303FA0AF0643 /* OHHTTPStubsSwift */, + E804AA8C4156CC85FFD9595F /* OCMock */, + C55551F29B99CF6D6DD9EE2F /* iOSSnapshotTestCase */, + ); + productName = StripeiOS_Tests; + productReference = CD5AC2BFBC8141F98C00CF9F /* StripeiOS_Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + ADF894AA8F6022D9BED17346 /* StripeiOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0FCB32AE130ABA66178DD9B /* Build configuration list for PBXNativeTarget "StripeiOS" */; + buildPhases = ( + 8D1F9C232F0DF9D129FE872E /* Headers */, + 24CDC502E3D468F309116FE1 /* Sources */, + 0F32E5D5E633BAB349B6EB50 /* Resources */, + CAAA5D4C8E87896995960E8C /* Embed Frameworks */, + 1587AD7E52759FBC80315765 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StripeiOS; + productName = Stripe; + productReference = 4259421D2CD26E37B96F97B2 /* Stripe.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E63832AA5BB4225708B7C838 /* Project object */ = { + isa = PBXProject; + attributes = { + TargetAttributes = { + 49A14BBD10B1E97F2B43C448 = { + TestTargetID = 1628E8B14F1F7C63CF8C9962; + }; + }; + }; + buildConfigurationList = 2804CCD7A91C99DF32596147 /* Build configuration list for PBXProject "Stripe" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + "bg-BG", + "ca-ES", + "cs-CZ", + da, + de, + "el-GR", + en, + "en-GB", + es, + "es-419", + "et-EE", + fi, + fil, + fr, + "fr-CA", + hr, + hu, + id, + it, + ja, + ko, + "lt-LT", + "lv-LV", + "ms-MY", + mt, + nb, + nl, + "nn-NO", + "pl-PL", + "pt-BR", + "pt-PT", + "ro-RO", + ru, + "sk-SK", + "sl-SI", + sv, + tk, + tr, + vi, + "zh-HK", + "zh-Hans", + "zh-Hant", + ); + mainGroup = E4802964A9471C082CE01BA9; + packageReferences = ( + D244E8B5402CB2CF89CE7872 /* XCRemoteSwiftPackageReference "ocmock" */, + 44C6210EB47ACF468D255723 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, + 71086E1440B890A9DC01C9DF /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */, + ); + productRefGroup = 85B4F3432AB3A8B94C9D2591 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + ADF894AA8F6022D9BED17346 /* StripeiOS */, + 8BE23AD5D9A3D939AF46F31E /* StripeiOSTests */, + 1628E8B14F1F7C63CF8C9962 /* StripeiOSTestHostApp */, + 49A14BBD10B1E97F2B43C448 /* StripeiOSAppHostedTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0F32E5D5E633BAB349B6EB50 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 31CDFC2E2BA3708000B3DD91 /* PrivacyInfo.xcprivacy in Resources */, + 22BE2ABB29F77362FF16D945 /* Localizable.strings in Resources */, + 76BC927BC7A591601C1DAB18 /* StripeiOS.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9BE857B21623552BA4F3FAA1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EEB5E5E9C4E06B148A91C7BD /* Assets.xcassets in Resources */, + 08111F4AD3CA0755420E05F7 /* LaunchScreen.storyboard in Resources */, + DE23FEF74E860620A334FDF5 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BADDFD2C3E190FF24FEB93C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 10342D659764A88A695EF38B /* Images.xcassets in Resources */, + F49D9C4030829D13A6EB45BE /* MockFiles in Resources */, + 2AE9ABA774B430E174279FEA /* stp_test_upload_image.jpeg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C501B175E87CBBB0075EB9C5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 24CDC502E3D468F309116FE1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 315713352C770DA3ED9CBDCD /* Enums+CustomStringConvertible.swift in Sources */, + B6784B7F4B9B04617C0EE510 /* PKAddPaymentPassRequest+Stripe_Error.swift in Sources */, + C0688E067AE4FFDFFDDC03BB /* PKPaymentAuthorizationViewController+Stripe_Blocks.swift in Sources */, + 6BF6ECC4A4E61E2FFC3EA20B /* STPAPIClient+BasicUI.swift in Sources */, + 5212C7875C07F9BF16AFD98D /* STPAPIClient+PushProvisioning.swift in Sources */, + BB46077C256C26418420F240 /* STPAddCardViewController.swift in Sources */, + E63B5BAF6B5645C979BFBA71 /* STPAddress+BasicUI.swift in Sources */, + 58240AA66FAE55131268E4A0 /* STPAddressFieldTableViewCell.swift in Sources */, + 313F5F792B0BE59100BD98A9 /* Docs.docc in Sources */, + 2BD45625F6F665B60C6CAD30 /* STPAddressViewModel.swift in Sources */, + 605EFBDD21426FD30581563F /* STPAnalyticsClient+BasicUI.swift in Sources */, + 7589E37795D21AB818B0C333 /* STPAnalyticsClient+Payments.swift in Sources */, + F86F2DF6E46EFABE23AD5D27 /* STPApplePayContextDelegate.swift in Sources */, + FC166455478EAF51F7C34E68 /* STPApplePayPaymentOption.swift in Sources */, + C314A5C55064C51C2B999E6B /* STPBackendAPIAdapter.swift in Sources */, + CEF318C74D2E44C78EF85306 /* STPBankSelectionTableViewCell.swift in Sources */, + 172D96526023A80534D54CC0 /* STPBankSelectionViewController.swift in Sources */, + A0AA0B8AEF5B429858D71F6B /* STPBlocks.swift in Sources */, + DDBF5AAE607C698618DDE865 /* STPCameraView.swift in Sources */, + 66065B1D65D7D5502D4E2F2B /* STPCard+BasicUI.swift in Sources */, + BAFD06E994739E1C38DFFBBC /* STPCardScanner.swift in Sources */, + 2FFA7C2D1C7337FDB4C608A5 /* STPCardScannerTableViewCell.swift in Sources */, + 2F18A1903244E144C7802E09 /* STPCardValidationState.swift in Sources */, + DF73457BF349BC962A6AC502 /* STPCoreScrollViewController.swift in Sources */, + EEFFE199D9769FF449BFD7FF /* STPCoreTableViewController.swift in Sources */, + B1BF689B91D538BDCA4C8578 /* STPCoreViewController.swift in Sources */, + 429DBA641E926EBC2D049FE7 /* STPCustomerContext.swift in Sources */, + 62B91808A088C4F9FDB62C53 /* STPEphemeralKey.swift in Sources */, + 4FB67F10A0B7106A8142B842 /* STPEphemeralKeyManager.swift in Sources */, + B8385576DC25BDEEB92D812F /* STPEphemeralKeyProvider.swift in Sources */, + AF23CB4EF17E87007CFC3E96 /* STPFPXBankStatusResponse.swift in Sources */, + B6EC2F572B618A3D00FF72A2 /* STPBasicUIAnalyticsSerializer.swift in Sources */, + 0B9C0E9A7A750607413C9E53 /* STPFakeAddPaymentPassViewController.swift in Sources */, + 5D6B52EB4D7258129F134D07 /* STPImageLibrary.swift in Sources */, + D2869246B446B8B31F1CD368 /* STPIntentActionLinkAuthenticateAccount.swift in Sources */, + 98EE8326C1D133E1C998114F /* STPLocalizedString.swift in Sources */, + 609E4D384B75F6A111DC0E27 /* STPPaymentActivityIndicatorView.swift in Sources */, + 4EFF8B46B12DA4D9AAB22523 /* STPPaymentCardTextFieldCell.swift in Sources */, + 446A108C8EB6C338A1D774F8 /* STPPaymentConfiguration.swift in Sources */, + B00F7FC372E376C6B2170D37 /* STPPaymentContext.swift in Sources */, + 447C19BDB2CF5445045F81F7 /* STPPaymentContextAmountModel.swift in Sources */, + EA7FEC518AA07BA59405A5E3 /* STPPaymentIntentParams+BasicUI.swift in Sources */, + 7BC98BE168781C5B3EC8A8DB /* STPPaymentMethod+BasicUI.swift in Sources */, + 812682EA323986B8F698FF3C /* STPPaymentMethodParams+BasicUI.swift in Sources */, + F10FC337254A34ED8F13E341 /* STPPaymentOption.swift in Sources */, + 4ED44ACF24949F516867235C /* STPPaymentOptionTableViewCell.swift in Sources */, + 5ECED204FD22CFEA3A806767 /* STPPaymentOptionTuple.swift in Sources */, + 1F432D0B37949217E4299A20 /* STPPaymentOptionsInternalViewController.swift in Sources */, + 69AC1EDE2A3C03B1D980CA54 /* STPPaymentOptionsViewController.swift in Sources */, + 307FD6A103EF7AF3CE451598 /* STPPaymentResult.swift in Sources */, + 7EAA7334372DBC38DF8FA0AA /* STPPinManagementService.swift in Sources */, + 2E35B0FB60FCBE7608080642 /* STPPushProvisioningContext.swift in Sources */, + FEE74744B657F86873EA2F3D /* STPPushProvisioningDetails.swift in Sources */, + 4E09E54E7FEC35C49C59A379 /* STPPushProvisioningDetailsParams.swift in Sources */, + 23D1246A5DAB5333650F104F /* STPSectionHeaderView.swift in Sources */, + 124D43C1A633922B1DA3E1E7 /* STPShippingAddressViewController.swift in Sources */, + 7B9C0D039EA9EF593AEC682D /* STPShippingMethodTableViewCell.swift in Sources */, + 7F9D08AC5A448C7693162D7D /* STPShippingMethodsViewController.swift in Sources */, + 3CE88568CB9648D6F1503B88 /* STPSource+BasicUI.swift in Sources */, + F835CEC935464FF32726A0A0 /* STPTheme.swift in Sources */, + 279D2BA91198E18730626CE6 /* STPUserInformation.swift in Sources */, + 542610492B38FEB652C6823E /* String+Localized.swift in Sources */, + DCF615643A22D0A7B739547C /* Stripe+Exports.swift in Sources */, + BF4ED4828114E2E89A3D4AB7 /* StripeBundleLocator.swift in Sources */, + 98E2332DE7F54E970BE5EEF7 /* UIBarButtonItem+Stripe.swift in Sources */, + 9D9692DFC4F06F8C70145000 /* UINavigationBar+Stripe_Theme.swift in Sources */, + 9C13E8A017A4E23BCCDE618B /* UINavigationController+Stripe_Completion.swift in Sources */, + 54331380F5AC68846DBE94D5 /* UITableViewCell+Stripe_Borders.swift in Sources */, + 093FE3D65978E3DB6B79AE05 /* UIToolbar+Stripe_InputAccessory.swift in Sources */, + 7F235CD649F6E97E4E7DD180 /* UIView+Stripe_FirstResponder.swift in Sources */, + 44672917D3AC4B83F9EC3BC3 /* UIView+Stripe_SafeAreaBounds.swift in Sources */, + CC072EBAD035AA54A2AD3ABC /* UIViewController+Stripe_KeyboardAvoiding.swift in Sources */, + 3FA556CF8B11E2486F505161 /* UIViewController+Stripe_NavigationItemProxy.swift in Sources */, + 3930ECBEE003772C1245D25B /* UIViewController+Stripe_ParentViewController.swift in Sources */, + D85C432B241FDE23875037F9 /* UserDefaults+Stripe.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C4ED1B5A417CECDB38B21698 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 234C71F480318E9062075924 /* AppDelegate.swift in Sources */, + 3C1A7B9810B038177FF1CF52 /* ViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E160E75F2870000E2FA9C5DC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FB03810F22F4E0919BB2EF68 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 617C1C882BB4992400B10AC5 /* STPPaymentMethodAlmaTests.swift in Sources */, + 3EB3745F556EA12AB27A8545 /* APIRequestTest.swift in Sources */, + 6EF3F611E6EA3CB479D62450 /* AfterpayPriceBreakdownViewSnapshotTests.swift in Sources */, + DF85F5EC6E16CAD21491891A /* AnalyticsHelperTests.swift in Sources */, + 5910FCB9822259D5EC7E4051 /* AutoCompleteViewControllerSnapshotTests.swift in Sources */, + C1E70FD29BBE36D76A7E6929 /* CardExpiryDateTests.swift in Sources */, + D375ADBD1F4B48380D5347D1 /* CircularButtonSnapshotTests.swift in Sources */, + 2F9FA9CBCA3C0CE52FAC9B6B /* ConfirmButtonSnapshotTests.swift in Sources */, + 3C1E9069CD03ED9981D7F3E2 /* ConfirmButtonTests.swift in Sources */, + 795F3783D62AB8E2A00DCD05 /* ConsumerSessionTests.swift in Sources */, + 524AE1978E0A4490D1C390C5 /* CustomerAdapterTests.swift in Sources */, + 61E1CA1F2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift in Sources */, + 0DFA17378D894C70D72C9F62 /* Error+PaymentSheetTests.swift in Sources */, + C9E66A22494C02050AE34A9B /* FBSnapshotTestCase+STPViewControllerLoading.swift in Sources */, + 35C1CF73701EECC7DB6AB722 /* FormSpecProviderTest.swift in Sources */, + ACF6CFE0F8B88FDBBB16968C /* FraudDetectionDataTest.swift in Sources */, + AF18D569B296BFC1EB5A7338 /* ImageTest.swift in Sources */, + D0C81317E0AA8EB0370B1BA1 /* LinkLegalTermsViewSnapshotTests.swift in Sources */, + 450FAE41FB4538462D05F2E4 /* LinkSignupViewModelTests.swift in Sources */, + FDD1858CAEFCEBB22BEC9BBC /* MKPlacemark+PaymentSheetTests.swift in Sources */, + 609C2C8F10AFAA2711639CD0 /* NSArray+StripeTest.swift in Sources */, + B98D71ED9ACC2E1B47372F53 /* NSDecimalNumber+StripeTest.swift in Sources */, + 6E7AD3CCC966A7F34922B172 /* NSDictionary+StripeTest.swift in Sources */, + 325694E4284BEAE787A5ECB6 /* NSLocale+STPSwizzling.swift in Sources */, + C4C1295E7DA618DFB944A534 /* NSString+StripeTest.swift in Sources */, + 68318DB86DFCD19505FC47BA /* NSURLComponents_StripeTest.swift in Sources */, + D83F76F584BC345CFBA71CF8 /* OneTimeCodeTextFieldSnapshotTests.swift in Sources */, + 951344464ACF84F0F6D43D10 /* OneTimeCodeTextFieldTests.swift in Sources */, + 8E423294AB602BF25DB11D8E /* OperationDebouncerTests.swift in Sources */, + C4DC3F4FA93A3BAF6EE782A0 /* PKPayment+StripeTest.swift in Sources */, + DC57D2DC40C6BA0C9CF7EC92 /* PayWithLinkButtonSnapshotTests.swift in Sources */, + AD9B9F3FF697D4A3892E86F2 /* PaymentAnalyticTest.swift in Sources */, + C861BB9EAAD04949E338D7FF /* PaymentTypeCellSnapshotTests.swift in Sources */, + BC694A1642DC30D530B60635 /* RotatingCardBrandsViewSnapshotTests.swift in Sources */, + 6BA4B91C2BF4343B00D1F21D /* STPPaymentMethodMobilePayTests.swift in Sources */, + C5D295FE9988CA80ABA57801 /* RotatingCardBrandsViewTests.swift in Sources */, + BEC0435570B9199B918ED4DA /* STPAPIClient+LinkAccountSessionTest.swift in Sources */, + 7D2C0D1BF455625997CBC33B /* STPAPIClientNetworkBridgeTest.swift in Sources */, + 9A24970C5FB6D3F7314AE550 /* STPAPIClientStubbedTest.swift in Sources */, + 5E5EE69D140F6FEDA5F0A346 /* STPAPIClientTest.swift in Sources */, + AD09F2ACD0CDCBD414AC30AD /* STPAPISettingsObjCBridgeTest.m in Sources */, + 17BD7C0391F3182E32A63D6B /* STPAUBECSDebitFormViewSnapshotTests.swift in Sources */, + 583DE9869C885BA02E0A071E /* STPAUBECSFormViewModelTests.swift in Sources */, + 26F38A2A57FDDC12926BE044 /* STPAddCardViewControllerLocalizationSnapshotTests.swift in Sources */, + FB34906C9215D0E03850064B /* STPAddCardViewControllerTest.swift in Sources */, + 3EA9D509E59DA65EE4EDF98D /* STPAddressTests.swift in Sources */, + F53E04785DB804EA5C2AAC18 /* STPAddressViewModelTest.swift in Sources */, + CAC3A0342C2F176A007BC888 /* STPPaymentMethodSunbitTests.swift in Sources */, + E6F428CFAD64979A8874B00B /* STPAnalyticsClientPaymentsTest.swift in Sources */, + 23CF725CFAB2ABED416BF416 /* STPApplePayContextFunctionalTest.swift in Sources */, + 6F9525063D76A9F86A10CCBF /* STPApplePayContextFunctionalTestExtras.swift in Sources */, + F2655328479314A9C8718DE4 /* STPApplePayContextTest.swift in Sources */, + 9B149DA42FB38C3542E0CB4B /* STPApplePayFunctionalTest.swift in Sources */, + 0C5F4AE769D95AA921F61084 /* STPApplePayPaymentOptionTest.swift in Sources */, + FBBA3B39598BBECB664C5E7F /* STPApplePayTest.swift in Sources */, + D0342D50F9AC319919D93D59 /* STPBECSDebitAccountNumberValidatorTests.swift in Sources */, + 66B7EF2DC1CBF813707C767C /* STPBSBNumberValidatorTests.swift in Sources */, + 39B1B88B8506BE4574E6B376 /* STPBankAccountFunctionalTest.swift in Sources */, + 5370700ED1F630E8261507D3 /* STPBankAccountParamsTest.swift in Sources */, + 801F417CE53689B95C4A098B /* STPBankAccountTest.swift in Sources */, + 6F4FBB4F10B5DB2CF8BB3460 /* STPBinRangeTest.swift in Sources */, + B917BF282C84507292112B9D /* STPCardBINMetadataTests.swift in Sources */, + 786C30837EAD918EDE52284E /* STPCardBrandTest.swift in Sources */, + FE6647242714D9BEA1EBC055 /* STPCardCVCInputTextFieldFormatterTests.swift in Sources */, + 8EC1820299152F8565D30A40 /* STPCardCVCInputTextFieldSnapshotTests.swift in Sources */, + F35E090A607EB5F86FFC3D31 /* STPCardCVCInputTextFieldTests.swift in Sources */, + D2C062CE4E54094B1AC33E78 /* STPCardCVCInputTextFieldValidatorTests.swift in Sources */, + AC35943F1EAD50E9D5D509B3 /* STPCardExpiryInputTextFieldFormatterTests.swift in Sources */, + A66C279957B6AC8F72DE05C7 /* STPCardExpiryInputTextFieldSnapshotTests.swift in Sources */, + C57BDA835AED735321906977 /* STPCardExpiryInputTextFieldValidatorTests.swift in Sources */, + 07A5CDBFDF2340BAD99D6EB3 /* STPCardFormViewSnapshotTests.swift in Sources */, + 246920234EE8382FB4E56516 /* STPCardFormViewTests.swift in Sources */, + A12CFA90DAE8BBB39A8C7AA1 /* STPCardFunctionalTest.swift in Sources */, + CF2E17AC77EB08393B8A3F98 /* STPCardNumberInputTextFieldFormatterTests.swift in Sources */, + EEBA9A95E8057A06E5E7C103 /* STPCardNumberInputTextFieldSnapshotTests.swift in Sources */, + 1A058C42C4703458CA1CA522 /* STPCardNumberInputTextFieldValidatorTests.swift in Sources */, + D53C04A27B6B8EFB70E236A7 /* STPCardParamsTest.swift in Sources */, + D15160C0F0763078DBB434E4 /* STPCardTest.swift in Sources */, + 1E8D8E2494062262A332879C /* STPCardValidatorTest.swift in Sources */, + A781FB0F586B26655FAEC3C0 /* STPCertTest.swift in Sources */, + C8226E24CA51133091131391 /* STPConfirmCardOptionsTest.swift in Sources */, + 6BC5EC2D2B4609FF00CC75E8 /* LinkInlineSignupElementSnapshotTests.swift in Sources */, + 240993144289CD0DEC2C73C7 /* STPConfirmPaymentMethodOptionsTest.swift in Sources */, + 492F7C4DABB4CE8EBE34EEF2 /* STPConnectAccountAddressTest.swift in Sources */, + C8490E55B1F2EB836144F91C /* STPConnectAccountFunctionalTest.swift in Sources */, + 14656D177E67594B8C75A9FE /* STPConnectAccountParamsTest.swift in Sources */, + 610DF5DC2B33597500DA6AAA /* HostedSurfaceTest.swift in Sources */, + 9D464A252FBD0D4E2A0A7398 /* STPCountryPickerInputFieldSnapshotTests.swift in Sources */, + 4C3B161481D11385352B06D4 /* STPCustomerContextTest.swift in Sources */, + 6EDFC83541EED9E361B71C02 /* STPCustomerTest.swift in Sources */, + CBCA59D39B30D869B4FDC04B /* STPE2ETest.swift in Sources */, + 687517E7FE02FFB96DCE2328 /* STPEphemeralKeyManagerTest.swift in Sources */, + B6656829DEC006DBEED2AA0E /* STPEphemeralKeyTest.swift in Sources */, + 7435E6BB6971012A9B0DB52E /* STPErrorBridgeTest.m in Sources */, + 5D7F632025C261B88F0C2016 /* STPFPXBankBrandTest.swift in Sources */, + D776B91F0E8E6CCB6C09AC4F /* STPFileFunctionalTest.swift in Sources */, + D76D24F6A94108853BB08712 /* STPFileTest.swift in Sources */, + BC6912C0DE15008C8D8C303C /* STPFloatingPlaceholderTextFieldSnapshotTests.swift in Sources */, + 91A839DEDA7D1EAF6FC66BE0 /* STPFormEncoderTest.swift in Sources */, + 96098727EFA6A72087A35A52 /* STPFormTextFieldTest.swift in Sources */, + 013F991AB34E38BDBA6E4521 /* STPFormViewSnapshotTests.swift in Sources */, + 51044B947A7FDB99451466D8 /* STPGenericInputPickerFieldSnapshotTests.swift in Sources */, + 8532FEBF4F2E0EB282D466CE /* STPGenericInputPickerFieldValidatorTest.swift in Sources */, + 4B0917FC15BF56D0100E0ED1 /* STPGenericInputTextFieldSnapshotTests.swift in Sources */, + 3AD22E0BD44B02D968C6569A /* STPImageLibraryTest.swift in Sources */, + 61E1CA212BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift in Sources */, + D7C555B36C282B99E22B8D45 /* STPInputTextFieldFormatterTests.swift in Sources */, + 617C1C8A2BB4998C00B10AC5 /* STPPaymentMethodAlmaParamsTests.swift in Sources */, + 903FFB756C6ED520BE38EF6F /* STPInputTextFieldValidatorTests.swift in Sources */, + 45FA9B8CC2D18E29BE81CF8F /* STPIntentActionAlipayHandleRedirectTest.swift in Sources */, + F550D4EB3DCFE03D6FC8F023 /* STPIntentActionPayNowDisplayQrCodeTest.swift in Sources */, + 78641CE4011A1C1EE6E35DC5 /* STPIntentActionPromptPayDisplayQrCodeTest.swift in Sources */, + ACC1B91FC687AFD0DFD27CD4 /* STPIntentActionTest.swift in Sources */, + 4935C8B3ECFBAD947E694934 /* STPIntentActionTypeTest.swift in Sources */, + 8F0326E98C74EB62E34B9FEA /* STPIntentActionWeChatPayRedirectToAppTest.swift in Sources */, + 9FB20E559379F468070C7B50 /* STPLabeledFormTextFieldViewSnapshotTests.swift in Sources */, + A22D548084E7DE1FE5ABE8E7 /* STPLabeledMultiFormTextFieldViewSnapshotTests.swift in Sources */, + 331924F0801287BAD413FDCB /* STPMandateCustomerAcceptanceParamsTest.swift in Sources */, + 781EC0163AC001C6A66045B6 /* STPMandateDataParamsTest.swift in Sources */, + B82859A4444B9F735720F232 /* STPMandateOnlineParamsTest.swift in Sources */, + 9FD92B3ADEBEC96660B70409 /* STPMocks.m in Sources */, + A77C5769B20D7884FC8FC4FB /* STPNumericDigitInputTextFormatterTests.swift in Sources */, + F975CE029DF30419B8DB0D8F /* STPNumericStringValidatorTests.swift in Sources */, + 9535CADFFBC9E1FA291E947E /* STPPIIFunctionalTest.swift in Sources */, + 61951FB92B866BA1005F90BE /* STPPaymentMethodAmazonPayTests.swift in Sources */, + 1CD3AB315580606AF87A7B1F /* STPPaymentCardTextFieldKVOTest.m in Sources */, + 77C0FD1BCDA7BBFB88559B44 /* STPPaymentCardTextFieldTest.swift in Sources */, + 86BAF121184D71F5F4FFAD7B /* STPPaymentCardTextFieldTestsSwift.swift in Sources */, + B4719234E4BBDAD260E31373 /* STPPaymentCardTextFieldViewModelTest.swift in Sources */, + 3AAE488F2461A46143B3A687 /* STPPaymentConfigurationTest.m in Sources */, + CAC80EBF2C33339D001E3D0D /* STPPaymentMethodSatispayTests.swift in Sources */, + 829D43B6705D125FEC9926DA /* STPPaymentContextApplePayTest.swift in Sources */, + 97756805F41DDB51B3ED0326 /* STPPaymentContextSnapshotTests.swift in Sources */, + FEF2E0DAC862FF42B814AFCA /* STPPaymentHandlerFunctionalTest.m in Sources */, + 8F5AF9D3566B8DBCA5AB5188 /* STPPaymentHandlerFunctionalTest.swift in Sources */, + 194154708E1A9E013DCE2C72 /* STPPaymentHandlerStubbedMockedFilesTests.swift in Sources */, + 2A528B7B2579E5F977797822 /* STPPaymentHandlerTests.swift in Sources */, + 1BC4044802EE7D3E2643DC84 /* STPPaymentIntentEnumsTest.swift in Sources */, + 37E9160706C9EEEFEF133617 /* STPPaymentIntentFunctionalTest.swift in Sources */, + 61E1CA272BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift in Sources */, + F729E784CFFC1F79EF5F2ABE /* STPPaymentIntentLastPaymentErrorTest.swift in Sources */, + CA189278AD606BEAC62D545F /* STPPaymentIntentParamsTest.swift in Sources */, + 73AFE2A8839EFAB8330F6CF0 /* STPPaymentIntentTest.swift in Sources */, + 5E498CDA0115CF9F8463C566 /* STPPaymentMethodAUBECSDebitParamsTests.swift in Sources */, + D567569568C0D8F2D7B179B3 /* STPPaymentMethodAUBECSDebitTests.swift in Sources */, + CFC1F2B8D48FFF7B0F81B5A0 /* STPPaymentMethodAddressTest.swift in Sources */, + 4993037E5386D0AF87B24871 /* STPPaymentMethodAffirmParamsTest.swift in Sources */, + F5CC4F320D09A06F0B21ABE6 /* STPPaymentMethodAffirmTests.swift in Sources */, + FDADE3E36804A8AD82301BF3 /* STPPaymentMethodAfterpayClearpayParamsTest.swift in Sources */, + C34D0BBDF6553ACF85204ACD /* STPPaymentMethodAfterpayClearpayTest.swift in Sources */, + 0305C1689C25B57C43640173 /* STPPaymentMethodBacsDebitTest.swift in Sources */, + BEC5B2ACC54FB72DEBFB70AB /* STPPaymentMethodBancontactParamsTests.swift in Sources */, + 1CCFC43F7FCD273E2100D321 /* STPPaymentMethodBancontactTests.swift in Sources */, + E0F011E9C6CA368EF87F8E28 /* STPPaymentMethodBillingDetailsTest.swift in Sources */, + 29428CDB658E6F504402D844 /* STPPaymentMethodBillingDetailsTests+Link.swift in Sources */, + 8C977F8D224A7360AE8E15A7 /* STPPaymentMethodBoletoParamsTests.swift in Sources */, + 5170651536332C4842E9D009 /* STPPaymentMethodBoletoTests.swift in Sources */, + FE7C38B95B3B7E028AB21238 /* STPPaymentMethodCardChecksTest.swift in Sources */, + 8378F2A4B0796819BB1C6C54 /* STPPaymentMethodCardParamsTest.swift in Sources */, + 43FFF2881D4EFA7B57A60E09 /* STPPaymentMethodCardTest.swift in Sources */, + F06EAD0F48302B061ED29E61 /* STPPaymentMethodCardWalletMasterpassTest.swift in Sources */, + 7623057AC6AC5369DCD94E84 /* STPPaymentMethodCardWalletTest.swift in Sources */, + 33DF66640B5ABBCB12B46AFE /* STPPaymentMethodCardWalletVisaCheckoutTest.swift in Sources */, + 0185AC6B123CD73E877D4FCE /* STPPaymentMethodCashAppParamsTests.swift in Sources */, + CBAF9C6F87F746F17495ADC2 /* STPPaymentMethodCashAppTests.swift in Sources */, + D4602454AC17D3584BA88217 /* STPPaymentMethodEPSParamsTests.swift in Sources */, + C7EB8FB325BF491FDE25FE66 /* STPPaymentMethodEPSTests.swift in Sources */, + C3AAA4AFEE274B27D3483876 /* STPPaymentMethodFPXTest.swift in Sources */, + B44E4CF6C65522F80C946775 /* STPPaymentMethodFunctionalTest.swift in Sources */, + E9A2C6E153CB480891846705 /* STPPaymentMethodGiropayParamsTests.swift in Sources */, + B8ED1F697519A6FCD3D79431 /* STPPaymentMethodGiropayTests.swift in Sources */, + 319899DEC91B3F88D380DB47 /* STPPaymentMethodGrabPayParamsTest.swift in Sources */, + 7844BB705AEB002965EF82B0 /* STPPaymentMethodKlarnaParamsTests.swift in Sources */, + CAC3A03E2C2F2CD8007BC888 /* STPPaymentMethodBillieTests.swift in Sources */, + 27F1783CBFEC06BFD6C114F6 /* STPPaymentMethodKlarnaTests.swift in Sources */, + C0B59D0A7025A55ECD948D47 /* STPPaymentMethodNetBankingParamsTest.swift in Sources */, + FF0F9BA6FE4B88297A434EA7 /* STPPaymentMethodNetBankingTests.swift in Sources */, + E3E916EB10E19727D6B33081 /* STPPaymentMethodOXXOParamsTests.swift in Sources */, + 37FBCED5F71F03483EA73F27 /* STPPaymentMethodOXXOTests.swift in Sources */, + DD8E2B99BAE917F83258DC35 /* STPPaymentMethodOptionsTest.swift in Sources */, + 5D9EB3E2725C38D7098B9965 /* STPPaymentMethodParamsTest.swift in Sources */, + 42F18560F3DC6980408AF051 /* STPPaymentMethodPayPalParamsTests.swift in Sources */, + 2C6DC246DD12FE0D87156A4D /* STPPaymentMethodPayPalTests.swift in Sources */, + E2790AB17C8C65CDE1E81532 /* STPPaymentMethodPrzelewy24ParamsTests.swift in Sources */, + D7D24DCC9402153965AF7F1B /* STPPaymentMethodPrzelewy24Tests.swift in Sources */, + BBB734F006FAD749678B87D1 /* STPPaymentMethodRevolutPayParamsTests.swift in Sources */, + D54508ED433792AD8AA6610F /* STPPaymentMethodRevolutPayTests.swift in Sources */, + B359F6DCB31EAD0814AD9AFD /* STPPaymentMethodSEPADebitTest.swift in Sources */, + 0684E2ABDA4566356143CC14 /* STPPaymentMethodSofortParamsTests.swift in Sources */, + 5C5E1CE53D89DE8F0B867115 /* STPPaymentMethodSofortTests.swift in Sources */, + 2C9F69E4A384C5743F4EAF69 /* STPPaymentMethodSwishParamsTests.swift in Sources */, + 4059301B0365BD4220E591FB /* STPPaymentMethodSwishTests.swift in Sources */, + A08C2F0E7F642515B1D263ED /* STPPaymentMethodTest.swift in Sources */, + 7A9D7D156B5053638F9B21E1 /* STPPaymentMethodThreeDSecureUsageTest.swift in Sources */, + A01BB7F09134F7081679F9C4 /* STPPaymentMethodUPIParamsTest.swift in Sources */, + AF44725558E654548FED2A2B /* STPPaymentMethodUPITests.swift in Sources */, + 4AAA2CD5AEF1F913395B3B95 /* STPPaymentMethodUSBankAccountParamsStubbedTest.swift in Sources */, + 7D251ABF1EBF65ACA8A4BDD4 /* STPPaymentMethodUSBankAccountParamsTest.swift in Sources */, + 225140E0BD9C0630116DDE4A /* STPPaymentMethodUSBankAccountTest.swift in Sources */, + B71F04D02538FA1723558C48 /* STPPaymentMethodiDEALTest.swift in Sources */, + 9A57C50938A66604FF16A882 /* STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift in Sources */, + 2C7991FDF7B374E0E65E253F /* STPPaymentOptionsViewControllerTest.swift in Sources */, + 07BF3CF1656AF5F5A0678873 /* STPPhoneNumberValidatorTest.swift in Sources */, + EBD436689635CC28A24DECD4 /* STPPinManagementServiceFunctionalTest.swift in Sources */, + 385CAC4D2FF119D2E925916B /* STPPostalCodeInputTextFieldFormatterTests.swift in Sources */, + 9B1AC278FDCDABF26C5E468C /* STPPostalCodeInputTextFieldSnapshotTests.swift in Sources */, + 4E31B1864DA407598FB1BBC6 /* STPPostalCodeInputTextFieldTests.swift in Sources */, + 6BA4B91A2BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift in Sources */, + 91558F51B87C72E745244958 /* STPPostalCodeInputTextFieldValidatorTests.swift in Sources */, + 6FCA954C32AB351F902BA876 /* STPPostalCodeValidatorTest.swift in Sources */, + 3172C789DF2CE133ECA359D7 /* STPPushProvisioningDetailsFunctionalTest.swift in Sources */, + 4A61DC36F10B9C9C24345613 /* STPRadarSessionFunctionalTest.swift in Sources */, + 1948544E75A2E16E46CBA00E /* STPRedirectContextTest.swift in Sources */, + 044B7BECFBDB1F6C8CA08514 /* STPSetupIntentConfirmParamsTest.swift in Sources */, + C32D7ACEBC852CBC295BBEF2 /* STPSetupIntentFunctionalTest.swift in Sources */, + E699508F4DB4D9D4666BAA08 /* STPSetupIntentLastSetupErrorTest.swift in Sources */, + EC4DC8E386544959E1AA9355 /* STPSetupIntentTest.swift in Sources */, + A930DF2880EAD0CB9096E49E /* STPShippingAddressViewControllerLocalizationSnapshotTests.swift in Sources */, + 08ED7A4EB7E64FDAED2C2D39 /* STPShippingAddressViewControllerTest.swift in Sources */, + 724429607B2741CF44D9C2E5 /* STPShippingMethodsViewControllerLocalizationSnapshotTests.swift in Sources */, + AE747ADA2841AA06F32558D8 /* STPSourceCardDetailsTest.swift in Sources */, + 3FD5ABC45AF3A03F4EFE196F /* STPSourceFunctionalTest.swift in Sources */, + 922C0DF37F5AAA29375A5454 /* STPSourceOwnerTest.swift in Sources */, + D8BECFB70834CC42BA6706D8 /* STPSourceParamsTest.swift in Sources */, + 46FF3CC61200F2C27D4F3369 /* STPSourceReceiverTest.swift in Sources */, + 28538CD5885636DC523E8751 /* STPSourceRedirectTest.swift in Sources */, + EA80A8DB806DEF4F519059CB /* STPSourceSEPADebitDetailsTest.swift in Sources */, + D3D654D8376AAA634466D31D /* STPSourceTest.swift in Sources */, + 460B31EDB22BD6B912567363 /* STPSourceVerificationTest.swift in Sources */, + D7956073A8FD3785193E0577 /* STPStackViewWithSeparatorSnapshotTests.swift in Sources */, + 61152B4F2B866827003B69A0 /* STPPaymentMethodAmazonPayParamsTests.swift in Sources */, + CA4F392070740C56FE2BB461 /* STPStringUtilsTest.swift in Sources */, + 9D8354BDB04CEC5D1EFCF54F /* STPSwiftFixtures.swift in Sources */, + E3F1BAD22CC6E90B761B0502 /* STPTextFieldDelegateProxyTests.swift in Sources */, + 58A8B3F57FE98C22D8F90C77 /* STPThreeDSButtonCustomizationTest.swift in Sources */, + EA571ECEFDDF10AF87CE2B74 /* STPThreeDSFooterCustomizationTest.swift in Sources */, + C35CF837D67AE8DB7CBDAD98 /* STPThreeDSLabelCustomizationTest.swift in Sources */, + 590DB84AC15709E3C6F1FC3B /* STPThreeDSNavigationBarCustomizationTest.swift in Sources */, + DD16FC7ABCA7817794ECC407 /* STPThreeDSSelectionCustomizationTest.swift in Sources */, + 61E0A0C52BF31D3C00C89786 /* STPPaymentHandlerRefreshTests.swift in Sources */, + 2AC91F23CF3949ADC60D27F7 /* STPThreeDSTextFieldCustomizationTest.swift in Sources */, + 701C464523173C6809544935 /* STPThreeDSUICustomizationTest.swift in Sources */, + B86EE8C85E6AB6B0A34C1887 /* STPTokenTest.swift in Sources */, + 8AA84A3A52A3D79BCA8C8994 /* STPUIVCStripeParentViewControllerTests.swift in Sources */, + 35E05040EA813C3B9C8EF054 /* STPViewWithSeparatorSnapshotTests.swift in Sources */, + 71116C2D5831E271E12DB059 /* ServerErrorMapperTest.swift in Sources */, + A8B0DB753CAA2223C8BED099 /* StripeErrorTest.swift in Sources */, + 3B237145902E3DB07E747E32 /* TextFieldElement+IBANTest.swift in Sources */, + CEE483EB7B06B3C607BC755C /* UINavigationBar+StripeTest.m in Sources */, + 51D515315F02D4C03BA12366 /* UserDefaults+StripeTest.swift in Sources */, + 8B80FB6FC88D411A90E9D487 /* WalletHeaderViewSnapshotTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 11BFD72CD1F291E0E86EC784 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeiOSTestHostApp; + target = 1628E8B14F1F7C63CF8C9962 /* StripeiOSTestHostApp */; + targetProxy = 32221E5BA07FB5AA4EBFE81C /* PBXContainerItemProxy */; + }; + 4E57D9EF1E91091C57E49111 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeiOS; + target = ADF894AA8F6022D9BED17346 /* StripeiOS */; + targetProxy = D90CE98566BBDA0E7340E1D7 /* PBXContainerItemProxy */; + }; + C4F1FB6EFF8D25D54580374A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeiOS; + target = ADF894AA8F6022D9BED17346 /* StripeiOS */; + targetProxy = 8F25D3DFD6D65DAE4B581911 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 147D2DC1FFDFC99269039377 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD8BED2A6066514B51693172 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 884C01B087B4D820395BD374 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + C980D24DDC884FECCE39139F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + C2427C1CDFA85BFC6570F1E9 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7B8ADF2EA2D5C4C90BCDDDD5 /* bg-BG */, + 26E70469F4032553B4BB62DA /* ca-ES */, + 8ED737CB1253C3C5704B6C05 /* cs-CZ */, + 27A4D83C0E7D4AE0CCD38B89 /* da */, + 2560B4EEE60D40FB31B8552F /* de */, + DE24B3BAD7E890661CCA817D /* el-GR */, + 7B10B1A15063FEDBA4A59953 /* en */, + 90D0900C5FDBB7952BCF2C3A /* en-GB */, + A6F6634AD12771A9BB100DD3 /* es */, + AB1A92C77AC61335184BBDBC /* es-419 */, + 12D757B36030685C401A6990 /* et-EE */, + BAFD938F3A149A56CC99FE96 /* fi */, + 0A16326394D71637A2CF68C3 /* fil */, + 04838ACE779F5CC949C276CB /* fr */, + DFA4E34E459EA612E065DE64 /* fr-CA */, + 7CAE7444CEFE1D1EFB888996 /* hr */, + BFB4A210A30D1D4F3D3100E5 /* hu */, + 084B1B9FCCF4AB727B4ECFB2 /* id */, + ABAABB969BFB7102D5AA5598 /* it */, + 064CFCA8FCEA9E4BAB3547D0 /* ja */, + 1007571188950D7FBF745A4E /* ko */, + 43ADFC4EF612D7C4A46E81B9 /* lt-LT */, + 13DDBEA7D444A8AC14E0F1C8 /* lv-LV */, + 9D3BBCE8C46A38D0E20DBF4E /* ms-MY */, + 04FE0C74090AE8A871CCE5EC /* mt */, + 482693A3D9A527B0E671F757 /* nb */, + D3667F97B7665F699D6704BE /* nl */, + B334078D1C3E629BEB498BAB /* nn-NO */, + 0C9D6F99E303A17A91101723 /* pl-PL */, + 002634603200AABECC9686B1 /* pt-BR */, + C45852D37E323E65C47348B0 /* pt-PT */, + AA3CA5B4B91838CC7ED4D5EB /* ro-RO */, + 121B7EDCCD0957C9A444A8E3 /* ru */, + 0F735744F27D46F005BB5D67 /* sk-SK */, + 618DE183886175AF23C4E668 /* sl-SI */, + 58158E8A117299834246100F /* sv */, + 35ABE696542469B79A9D52E6 /* tk */, + 313263D9DC0629C5EE279FEB /* tr */, + 4077600B9B3C1ABFF38383BE /* vi */, + F13E3DE09A463B4501733B87 /* zh-Hans */, + 4DFDFC019C67AFF7C0F7630B /* zh-Hant */, + EA20E0E29EDC1F61ADA226A2 /* zh-HK */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 006D8C78BFD35DF18042E040 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1D23EB567F573612E0794B3A /* Stripe Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeiOSTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSTests; + PRODUCT_NAME = StripeiOS_Tests; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 008139FD6C1D3C3903E1FBC4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 969E196AB597EEF68C38103E /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 160D95A170FDDD08C46E4C35 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E4EA6394497D1BD57ED0032 /* Stripe Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeiOSTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSTests; + PRODUCT_NAME = StripeiOS_Tests; + SDKROOT = iphoneos; + }; + name = Release; + }; + 2473A3BC44750A2ADE1CE6A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1A2173E0891B7138687D544 /* Stripe-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeiOS/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-ios"; + PRODUCT_NAME = Stripe; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 4CFD88FE3BC957D5059302C0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D609FFD051FC01BF566665A0 /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeiOSAppHostedTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSAppHostedTests; + PRODUCT_NAME = StripeiOSAppHostedTests; + SDKROOT = iphoneos; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StripeiOSTestHostApp.app/StripeiOSTestHostApp"; + TEST_TARGET_NAME = StripeiOSTestHostApp; + }; + name = Debug; + }; + 50C10A6D37E11392F6B0E7F2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E3C8833E7EBA7A1EBF349D19 /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeiOSAppHostedTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSAppHostedTests; + PRODUCT_NAME = StripeiOSAppHostedTests; + SDKROOT = iphoneos; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StripeiOSTestHostApp.app/StripeiOSTestHostApp"; + TEST_TARGET_NAME = StripeiOSTestHostApp; + }; + name = Release; + }; + 610AD56E6CBB9E6781424682 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EB71A4A2762CF864DB198BCF /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + A71DB2DB45060267821F91EF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E3C8833E7EBA7A1EBF349D19 /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeiOSTestHostApp/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSTestHostApp; + PRODUCT_NAME = StripeiOSTestHostApp; + SDKROOT = iphoneos; + }; + name = Release; + }; + D3B5E5117D5D1C186CC75505 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D609FFD051FC01BF566665A0 /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeiOSTestHostApp/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeiOSTestHostApp; + PRODUCT_NAME = StripeiOSTestHostApp; + SDKROOT = iphoneos; + }; + name = Debug; + }; + D88B8DCAA56931FBE4963518 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DA82BB67D434E76B9ABA4CEC /* Stripe-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeiOS/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-ios"; + PRODUCT_NAME = Stripe; + SDKROOT = iphoneos; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2804CCD7A91C99DF32596147 /* Build configuration list for PBXProject "Stripe" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 610AD56E6CBB9E6781424682 /* Debug */, + 008139FD6C1D3C3903E1FBC4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 54F4DB1E9C991950FD9EB4B4 /* Build configuration list for PBXNativeTarget "StripeiOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 006D8C78BFD35DF18042E040 /* Debug */, + 160D95A170FDDD08C46E4C35 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6D8846CE184BBD9139D6D15 /* Build configuration list for PBXNativeTarget "StripeiOSTestHostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D3B5E5117D5D1C186CC75505 /* Debug */, + A71DB2DB45060267821F91EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F0FCB32AE130ABA66178DD9B /* Build configuration list for PBXNativeTarget "StripeiOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2473A3BC44750A2ADE1CE6A4 /* Debug */, + D88B8DCAA56931FBE4963518 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FCBB942DEBE57423D79373B4 /* Build configuration list for PBXNativeTarget "StripeiOSAppHostedTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4CFD88FE3BC957D5059302C0 /* Debug */, + 50C10A6D37E11392F6B0E7F2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 44C6210EB47ACF468D255723 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/davidme-stripe/OHHTTPStubs"; + requirement = { + branch = "stripe-mock"; + kind = branch; + }; + }; + 71086E1440B890A9DC01C9DF /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uber/ios-snapshot-test-case"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; + D244E8B5402CB2CF89CE7872 /* XCRemoteSwiftPackageReference "ocmock" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/erikdoe/ocmock"; + requirement = { + branch = master; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 62887B4538E4E41E735685E1 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubs; + }; + 911CA85A1610303FA0AF0643 /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubsSwift; + }; + C55551F29B99CF6D6DD9EE2F /* iOSSnapshotTestCase */ = { + isa = XCSwiftPackageProductDependency; + productName = iOSSnapshotTestCase; + }; + E804AA8C4156CC85FFD9595F /* OCMock */ = { + isa = XCSwiftPackageProductDependency; + productName = OCMock; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = E63832AA5BB4225708B7C838 /* Project object */; +} diff --git a/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOS.xcscheme b/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOS.xcscheme new file mode 100644 index 00000000..c39806ee --- /dev/null +++ b/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOS.xcscheme @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOSTestHostApp.xcscheme b/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOSTestHostApp.xcscheme new file mode 100644 index 00000000..eb6c42eb --- /dev/null +++ b/Stripe/Stripe.xcodeproj/xcshareddata/xcschemes/StripeiOSTestHostApp.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe/StripeiOS/Docs.docc/Stripe.md b/Stripe/StripeiOS/Docs.docc/Stripe.md new file mode 100644 index 00000000..54a4e714 --- /dev/null +++ b/Stripe/StripeiOS/Docs.docc/Stripe.md @@ -0,0 +1,3 @@ +# ``Stripe`` + +Placeholder diff --git a/Stripe/StripeiOS/Info.plist b/Stripe/StripeiOS/Info.plist new file mode 100644 index 00000000..abe21473 --- /dev/null +++ b/Stripe/StripeiOS/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + Stripe + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Stripe/StripeiOS/PrivacyInfo.xcprivacy b/Stripe/StripeiOS/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..30919590 --- /dev/null +++ b/Stripe/StripeiOS/PrivacyInfo.xcprivacy @@ -0,0 +1,45 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePaymentInfo + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/Stripe/StripeiOS/Resources/Localizations/bg-BG.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/bg-BG.lproj/Localizable.strings new file mode 100644 index 00000000..9b4e5b19 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/bg-BG.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Офлайн"; + +"3D Secure" = "3D Secure удостоверяване"; + +"Add New Card…" = "Добавяне на нова карта..."; + +"Add a Card" = "Добавяне на карта"; + +"Apt." = "Aп."; + +"Contact" = "Контакт"; + +"Delivery" = "Доставка"; + +"Delivery Address" = "Адрес за доставка"; + +"Free" = "Безплатно"; + +"Invalid Shipping Address" = "Невалиден адрес за доставка"; + +"Loading…" = "Зареждане..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Следващо"; + +"Online Banking (FPX)" = "Онлайн банкиране (FPX)"; + +"Payment Method" = "Метод на плащане"; + +"Shipping" = "Доставка"; + +"Shipping Method" = "Метод на доставка"; + +"Use Billing" = "Да се използва адреса за фактуриране"; + +"Use Delivery" = "Да се използва адреса за доставка"; + +"Use Shipping" = "Да се използва адреса за изпращане"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ca-ES.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ca-ES.lproj/Localizable.strings new file mode 100644 index 00000000..b7979c0a --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ca-ES.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Fora de línia"; + +"3D Secure" = "3D Segur"; + +"Add New Card…" = "Afegir nova targeta..."; + +"Add a Card" = "Afegir una targeta"; + +"Apt." = "Apt."; + +"Contact" = "Contacte"; + +"Delivery" = "Lliurament"; + +"Delivery Address" = "Adreça de lliurament"; + +"Free" = "Sense cost"; + +"Invalid Shipping Address" = "Adreça d'enviament invàlida"; + +"Loading…" = "Carregant..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Següent"; + +"Online Banking (FPX)" = "Banca Online (FPX)"; + +"Payment Method" = "Mètode de pagament"; + +"Shipping" = "Enviament"; + +"Shipping Method" = "Mètode d'enviament"; + +"Use Billing" = "Fer servir Facturació"; + +"Use Delivery" = "Fer servir Lliurament"; + +"Use Shipping" = "Fes servir Enviament"; diff --git a/Stripe/StripeiOS/Resources/Localizations/cs-CZ.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/cs-CZ.lproj/Localizable.strings new file mode 100644 index 00000000..a0a89390 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/cs-CZ.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Přidat novou kartu"; + +"Add a Card" = "Přidat kartu"; + +"Apt." = "Byt"; + +"Contact" = "Kontakt"; + +"Delivery" = "Dodávka"; + +"Delivery Address" = "Dodací adresa"; + +"Free" = "Zdarma"; + +"Invalid Shipping Address" = "Neplatná dodací adresa"; + +"Loading…" = "Načítání..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Další"; + +"Online Banking (FPX)" = "Online bankovnictví (FPX)"; + +"Payment Method" = "Způsob platby"; + +"Shipping" = "Doprava"; + +"Shipping Method" = "Způsob dopravy"; + +"Use Billing" = "Použít fakturaci"; + +"Use Delivery" = "Použít dodání"; + +"Use Shipping" = "Použít dopravu"; diff --git a/Stripe/StripeiOS/Resources/Localizations/da.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/da.lproj/Localizable.strings new file mode 100644 index 00000000..8415e65b --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/da.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Tilføj nyt kort …"; + +"Add a Card" = "Tilføj et kort"; + +"Apt." = "Lejlighed"; + +"Contact" = "Kontaktperson"; + +"Delivery" = "Levering"; + +"Delivery Address" = "Leveringsadresse"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Ugyldig forsendelsesadresse"; + +"Loading…" = "Indlæser ..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Næste"; + +"Online Banking (FPX)" = "Onlinebanking (FPX)"; + +"Payment Method" = "Betalingsmetode"; + +"Shipping" = "Forsendelse"; + +"Shipping Method" = "Forsendelsesmetode"; + +"Use Billing" = "Brug faktureringsadresse"; + +"Use Delivery" = "Brug leveringsadresse"; + +"Use Shipping" = "Brug forsendelsesadresse"; diff --git a/Stripe/StripeiOS/Resources/Localizations/de.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/de.lproj/Localizable.strings new file mode 100644 index 00000000..1818eb85 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/de.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – offline"; + +"3D Secure" = "Secure"; + +"Add New Card…" = "Neue Karte hinzufügen…"; + +"Add a Card" = "Karte hinzufügen"; + +"Apt." = "Adresszeile 2"; + +"Contact" = "Kontakt"; + +"Delivery" = "Lieferinformationen"; + +"Delivery Address" = "Lieferadresse"; + +"Free" = "Kostenlos"; + +"Invalid Shipping Address" = "Ungültige Versandadresse"; + +"Loading…" = "Wird geladen..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Weiter"; + +"Online Banking (FPX)" = "Online-Banking (FPX)"; + +"Payment Method" = "Zahlungsmethode"; + +"Shipping" = "Versand"; + +"Shipping Method" = "Versandmethode"; + +"Use Billing" = "Rechnungsadresse verwenden"; + +"Use Delivery" = "Lieferadresse verwenden"; + +"Use Shipping" = "Versandadresse verwenden"; diff --git a/Stripe/StripeiOS/Resources/Localizations/el-GR.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/el-GR.lproj/Localizable.strings new file mode 100644 index 00000000..a95851a2 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/el-GR.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Εκτός σύνδεσης"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Προσθήκη νέας κάρτας…"; + +"Add a Card" = "Προσθήκη κάρτας"; + +"Apt." = "Διαμ."; + +"Contact" = "Επικοινωνία"; + +"Delivery" = "Παράδοση"; + +"Delivery Address" = "Διεύθυνση παράδοσης"; + +"Free" = "Δωρεάν"; + +"Invalid Shipping Address" = "Μη έγκυρη διεύθυνση αποστολής"; + +"Loading…" = "Φόρτωση…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Επόμενο"; + +"Online Banking (FPX)" = "Τραπεζικές υπηρεσίες μέσω διαδικτύου (FPX)"; + +"Payment Method" = "Μέθοδος πληρωμής"; + +"Shipping" = "Αποστολή"; + +"Shipping Method" = "Μέθοδος αποστολής"; + +"Use Billing" = "Χρήση χρέωσης"; + +"Use Delivery" = "Χρήση παράδοσης"; + +"Use Shipping" = "Χρήση αποστολής"; diff --git a/Stripe/StripeiOS/Resources/Localizations/en-GB.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/en-GB.lproj/Localizable.strings new file mode 100644 index 00000000..6d515362 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/en-GB.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ — Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Add New Card…"; + +"Add a Card" = "Add a Card"; + +"Apt." = "Apt."; + +"Contact" = "Contact"; + +"Delivery" = "Delivery"; + +"Delivery Address" = "Delivery Address"; + +"Free" = "Free"; + +"Invalid Shipping Address" = "Invalid Shipping Address"; + +"Loading…" = "Loading…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Next"; + +"Online Banking (FPX)" = "Online Banking (FPX)"; + +"Payment Method" = "Payment Method"; + +"Shipping" = "Shipping"; + +"Shipping Method" = "Shipping Method"; + +"Use Billing" = "Use Billing"; + +"Use Delivery" = "Use Delivery"; + +"Use Shipping" = "Use Shipping"; diff --git a/Stripe/StripeiOS/Resources/Localizations/en.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/en.lproj/Localizable.strings new file mode 100644 index 00000000..40b3a294 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/en.lproj/Localizable.strings @@ -0,0 +1,60 @@ +/* Bank name when bank is offline for maintenance. */ +"%@ - Offline" = "%@ - Offline"; + +/* Source type brand name */ +"3D Secure" = "3D Secure"; + +/* Title for Add a Card view */ +"Add a Card" = "Add a Card"; + +/* Button to add a new credit card. */ +"Add New Card…" = "Add New Card…"; + +/* Caption for Apartment/Address line 2 field on address form */ +"Apt." = "Apt."; + +/* Title for contact info form */ +"Contact" = "Contact"; + +/* Title for delivery info form */ +"Delivery" = "Delivery"; + +/* Title for delivery address entry section */ +"Delivery Address" = "Delivery Address"; + +/* Label for free shipping method */ +"Free" = "Free"; + +/* Shipping form error message */ +"Invalid Shipping Address" = "Invalid Shipping Address"; + +/* Title for screen when data is still loading from the network. */ +"Loading…" = "Loading…"; + +/* Source type brand name */ +"Multibanco" = "Multibanco"; + +/* Button to move to the next text entry field */ +"Next" = "Next"; + +/* Button to pay with a Bank Account (using FPX). */ +"Online Banking (FPX)" = "Online Banking (FPX)"; + +/* Title for Payment Method screen */ +"Payment Method" = "Payment Method"; + +/* Title for shipping info form */ +"Shipping" = "Shipping"; + +/* Label for shipping method form */ +"Shipping Method" = "Shipping Method"; + +/* Button to fill shipping address from billing address. */ +"Use Billing" = "Use Billing"; + +/* Button to fill billing address from delivery address. */ +"Use Delivery" = "Use Delivery"; + +/* Button to fill billing address from shipping address. */ +"Use Shipping" = "Use Shipping"; + diff --git a/Stripe/StripeiOS/Resources/Localizations/es-419.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/es-419.lproj/Localizable.strings new file mode 100644 index 00000000..0ff4ff68 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/es-419.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Fuera de línea"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Agregar nueva tarjeta..."; + +"Add a Card" = "Agrega una tarjeta"; + +"Apt." = "Apto."; + +"Contact" = "Contacto"; + +"Delivery" = "Entrega"; + +"Delivery Address" = "Dirección de entrega"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Dirección de envío inválida"; + +"Loading…" = "Cargando..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Siguiente"; + +"Online Banking (FPX)" = "Banca electrónica (FPX)"; + +"Payment Method" = "Método de pago"; + +"Shipping" = "Envío"; + +"Shipping Method" = "Método de envío"; + +"Use Billing" = "Usar la de facturación"; + +"Use Delivery" = "Usar la de entrega"; + +"Use Shipping" = "Usar la de envío"; diff --git a/Stripe/StripeiOS/Resources/Localizations/es.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/es.lproj/Localizable.strings new file mode 100644 index 00000000..13311015 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/es.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Fuera de línea"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Añadir tarjeta nueva…"; + +"Add a Card" = "Añade una tarjeta"; + +"Apt." = "Piso"; + +"Contact" = "Contacto"; + +"Delivery" = "Entrega"; + +"Delivery Address" = "Dirección de entrega"; + +"Free" = "Gratuito"; + +"Invalid Shipping Address" = "Dirección de envío no válida"; + +"Loading…" = "Cargando…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Siguiente"; + +"Online Banking (FPX)" = "Banca electrónica (FPX)"; + +"Payment Method" = "Método de pago"; + +"Shipping" = "Envío"; + +"Shipping Method" = "Método de envío"; + +"Use Billing" = "Usar la de facturación"; + +"Use Delivery" = "Usar la de entrega"; + +"Use Shipping" = "Usar la de envío"; diff --git a/Stripe/StripeiOS/Resources/Localizations/et-EE.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/et-EE.lproj/Localizable.strings new file mode 100644 index 00000000..afe2d85c --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/et-EE.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Võrguühenduseta"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Lisa uus kaart ..."; + +"Add a Card" = "Kaardi lisamine"; + +"Apt." = "Korter"; + +"Contact" = "Kontakt"; + +"Delivery" = "Tarnimine"; + +"Delivery Address" = "Tarneaadress"; + +"Free" = "Tasuta"; + +"Invalid Shipping Address" = "Kehtetu tarneaadress"; + +"Loading…" = "Laadimine ..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Järgmine"; + +"Online Banking (FPX)" = "Internetipank (FPX)"; + +"Payment Method" = "Makseviis"; + +"Shipping" = "Tarnimine"; + +"Shipping Method" = "Tarneviis"; + +"Use Billing" = "Kasuta arveldusaadressi"; + +"Use Delivery" = "Kasuta saaja aadressi"; + +"Use Shipping" = "Kasuta tarneaadressi"; diff --git a/Stripe/StripeiOS/Resources/Localizations/fi.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/fi.lproj/Localizable.strings new file mode 100644 index 00000000..71096979 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/fi.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ Offline-tila"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Lisää uusi kortti..."; + +"Add a Card" = "Lisää kortti"; + +"Apt." = "Asunto"; + +"Contact" = "Yhteystiedot"; + +"Delivery" = "Toimitus"; + +"Delivery Address" = "Toimitusosoite"; + +"Free" = "Maksuton"; + +"Invalid Shipping Address" = "Toimitusosoite ei kelpaa"; + +"Loading…" = "Lataa..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Seuraava"; + +"Online Banking (FPX)" = "Verkkopankki (FPX)"; + +"Payment Method" = "Maksutapa"; + +"Shipping" = "Toimitus"; + +"Shipping Method" = "Toimitustapa"; + +"Use Billing" = "Käytä laskutusosoitetta"; + +"Use Delivery" = "Käytä toimitusosoitetta"; + +"Use Shipping" = "Käytä lähetysosoitetta"; diff --git a/Stripe/StripeiOS/Resources/Localizations/fil.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/fil.lproj/Localizable.strings new file mode 100644 index 00000000..a4252268 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/fil.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Magdagdag ng Bagong Kard..."; + +"Add a Card" = "Magdagdag ng isang Kard"; + +"Apt." = "Apt."; + +"Contact" = "Kontak"; + +"Delivery" = "Pagpapadala"; + +"Delivery Address" = "Adres ng Paghahatarin"; + +"Free" = "Libre"; + +"Invalid Shipping Address" = "Adres ng Nagpadala ay di balido"; + +"Loading…" = "Naglo-load..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Susunod"; + +"Online Banking (FPX)" = "Online Banking (FPX)"; + +"Payment Method" = "Paraan ng Pagbabayad"; + +"Shipping" = "Pagpapadala"; + +"Shipping Method" = "Paraan ng Pagpapadala"; + +"Use Billing" = "Gamitin ang Billing"; + +"Use Delivery" = "Gamitin ang Padadalhan"; + +"Use Shipping" = "Gamitin ang Nagpadala"; diff --git a/Stripe/StripeiOS/Resources/Localizations/fr-CA.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/fr-CA.lproj/Localizable.strings new file mode 100644 index 00000000..8ba185a4 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/fr-CA.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Hors ligne"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Ajouter une carte…"; + +"Add a Card" = "Ajouter une carte"; + +"Apt." = "Nº d'app."; + +"Contact" = "Contact"; + +"Delivery" = "Livraison"; + +"Delivery Address" = "Adresse de livraison"; + +"Free" = "Gratuit"; + +"Invalid Shipping Address" = "Adresse de livraison non valide"; + +"Loading…" = "Chargement…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Suivant"; + +"Online Banking (FPX)" = "Services bancaires en ligne (FPX)"; + +"Payment Method" = "Moyen de paiement"; + +"Shipping" = "Livraison"; + +"Shipping Method" = "Mode de livraison"; + +"Use Billing" = "Utiliser l'adresse de facturation"; + +"Use Delivery" = "Utiliser l'adresse de livraison"; + +"Use Shipping" = "Utiliser l'adresse de livraison"; diff --git a/Stripe/StripeiOS/Resources/Localizations/fr.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/fr.lproj/Localizable.strings new file mode 100644 index 00000000..191697b3 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/fr.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Hors ligne"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Ajouter une nouvelle carte..."; + +"Add a Card" = "Ajouter une carte"; + +"Apt." = "Numéro d'appartement"; + +"Contact" = "Contact"; + +"Delivery" = "Livraison"; + +"Delivery Address" = "Adresse de livraison"; + +"Free" = "Gratuit"; + +"Invalid Shipping Address" = "Adresse de livraison incorrecte"; + +"Loading…" = "Chargement en cours..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Suivant"; + +"Online Banking (FPX)" = "Compte bancaire (FPX)"; + +"Payment Method" = "Moyen de paiement"; + +"Shipping" = "Expédition"; + +"Shipping Method" = "Mode de livraison"; + +"Use Billing" = "Utiliser l'adresse de facturation"; + +"Use Delivery" = "Utiliser l'adresse de livraison"; + +"Use Shipping" = "Utiliser l'adresse de livraison"; diff --git a/Stripe/StripeiOS/Resources/Localizations/hr.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/hr.lproj/Localizable.strings new file mode 100644 index 00000000..6f9a9a4b --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/hr.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Izvan mreže"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Dodaj novu karticu..."; + +"Add a Card" = "Dodaj karticu"; + +"Apt." = "Stan br."; + +"Contact" = "Kontakt"; + +"Delivery" = "Dostava"; + +"Delivery Address" = "Adresa za dostavu"; + +"Free" = "Besplatno"; + +"Invalid Shipping Address" = "Adresa za otpremu nije valjana"; + +"Loading…" = "Učitava se..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Sljedeće"; + +"Online Banking (FPX)" = "Online Banking (FPX)"; + +"Payment Method" = "Način plaćanja"; + +"Shipping" = "Otprema"; + +"Shipping Method" = "Način otpreme"; + +"Use Billing" = "Koristi istu adresu kao i za naplatu"; + +"Use Delivery" = "Koristi istu adresu kao i za dostavu"; + +"Use Shipping" = "Koristi istu adresu kao i za otpremu"; diff --git a/Stripe/StripeiOS/Resources/Localizations/hu.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/hu.lproj/Localizable.strings new file mode 100644 index 00000000..355f47cd --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/hu.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Új kártya hozzáadása..."; + +"Add a Card" = "Kártya hozzáadása"; + +"Apt." = "Házszám"; + +"Contact" = "Kapcsolat"; + +"Delivery" = "Szállítás"; + +"Delivery Address" = "Szállítási cím"; + +"Free" = "Ingyenes"; + +"Invalid Shipping Address" = "Érvénytelen szállítási cím"; + +"Loading…" = "Betöltés..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Tovább"; + +"Online Banking (FPX)" = "Online bankolás (FPX)"; + +"Payment Method" = "Fizetési mód"; + +"Shipping" = "Szállítás"; + +"Shipping Method" = "Szállítási mód"; + +"Use Billing" = "Számlázási használata"; + +"Use Delivery" = "Szállítási használata"; + +"Use Shipping" = "Szállítási használata"; diff --git a/Stripe/StripeiOS/Resources/Localizations/id.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/id.lproj/Localizable.strings new file mode 100644 index 00000000..c53fed57 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/id.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Tambahkan Kartu Baru..."; + +"Add a Card" = "Tambahkan sebuah Kartu"; + +"Apt." = "Apt."; + +"Contact" = "Kontak"; + +"Delivery" = "Pengiriman"; + +"Delivery Address" = "Alamat Pengiriman"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Alamat Pengiriman Tidak Valid"; + +"Loading…" = "Memuat…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Berikutnya"; + +"Online Banking (FPX)" = "Perbankan Online (FPX)"; + +"Payment Method" = "Metode Pembayaran"; + +"Shipping" = "Pengiriman"; + +"Shipping Method" = "Metode Pengiriman"; + +"Use Billing" = "Gunakan Tagihan"; + +"Use Delivery" = "Gunakan Pengiriman"; + +"Use Shipping" = "Gunakan Pengiriman"; diff --git a/Stripe/StripeiOS/Resources/Localizations/it.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/it.lproj/Localizable.strings new file mode 100644 index 00000000..e59f1ff5 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/it.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Aggiungi nuova carta…"; + +"Add a Card" = "Aggiungi una carta"; + +"Apt." = "Int."; + +"Contact" = "Contatto"; + +"Delivery" = "Consegna"; + +"Delivery Address" = "Indirizzo di consegna"; + +"Free" = "Gratuita"; + +"Invalid Shipping Address" = "Indirizzo di spedizione non valido"; + +"Loading…" = "Caricamento..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Avanti"; + +"Online Banking (FPX)" = "Online Banking (FPX)"; + +"Payment Method" = "Modalità di pagamento"; + +"Shipping" = "Spedizione"; + +"Shipping Method" = "Modalità di spedizione"; + +"Use Billing" = "Usa indirizzo di fatturazione"; + +"Use Delivery" = "Usa indirizzo di consegna"; + +"Use Shipping" = "Usa indirizzo di spedizione"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ja.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ja.lproj/Localizable.strings new file mode 100644 index 00000000..4b98d6cc --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ja.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - オフライン"; + +"3D Secure" = "3D セキュア"; + +"Add New Card…" = "新しいクレジットカードを追加..."; + +"Add a Card" = "カードを追加"; + +"Apt." = "建物名"; + +"Contact" = "連絡先"; + +"Delivery" = "配達先情報"; + +"Delivery Address" = "配達先住所"; + +"Free" = "無料"; + +"Invalid Shipping Address" = "配送先住所が無効です"; + +"Loading…" = "ロード中..."; + +"Multibanco" = "Multibanco"; + +"Next" = "次へ"; + +"Online Banking (FPX)" = "オンラインバンキング (FPX)"; + +"Payment Method" = "お支払い方法"; + +"Shipping" = "配送先情報"; + +"Shipping Method" = "配送方法"; + +"Use Billing" = "請求先を使用"; + +"Use Delivery" = "配達先を使用"; + +"Use Shipping" = "配送先を使用"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ko.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ko.lproj/Localizable.strings new file mode 100644 index 00000000..2e2e2d79 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ko.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - 오프라인"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "새 카드 추가..."; + +"Add a Card" = "카드 추가"; + +"Apt." = "아파트"; + +"Contact" = "연락처"; + +"Delivery" = "배달"; + +"Delivery Address" = "배달 주소"; + +"Free" = "무료"; + +"Invalid Shipping Address" = "잘못된 배송 주소"; + +"Loading…" = "로드 중..."; + +"Multibanco" = "Multibanco"; + +"Next" = "다음"; + +"Online Banking (FPX)" = "온라인 뱅킹(FPX)"; + +"Payment Method" = "결제 수단"; + +"Shipping" = "배송"; + +"Shipping Method" = "배송 방법"; + +"Use Billing" = "청구 사용"; + +"Use Delivery" = "배달 사용"; + +"Use Shipping" = "배송 사용"; diff --git a/Stripe/StripeiOS/Resources/Localizations/lt-LT.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/lt-LT.lproj/Localizable.strings new file mode 100644 index 00000000..83a835e9 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/lt-LT.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – neprisijungęs"; + +"3D Secure" = "3D saugi"; + +"Add New Card…" = "Pridėti naują kortelę..."; + +"Add a Card" = "Pridėti kortelę"; + +"Apt." = "But."; + +"Contact" = "Susisiekti"; + +"Delivery" = "Pristatymas"; + +"Delivery Address" = "Pristatymo adresas"; + +"Free" = "Nemokamai"; + +"Invalid Shipping Address" = "Netinkamas siuntimo adresas"; + +"Loading…" = "Įkeliama..."; + +"Multibanco" = "„Multibanco“"; + +"Next" = "Toliau"; + +"Online Banking (FPX)" = "Interneto banko paslaugos (FPX)"; + +"Payment Method" = "Mokėjimo būdas"; + +"Shipping" = "Siuntimas"; + +"Shipping Method" = "Siuntimo būdas"; + +"Use Billing" = "Naudoti sąskaitų siuntimo adresą"; + +"Use Delivery" = "Naudoti pristatymo adresą"; + +"Use Shipping" = "Naudoti siuntimo adresą"; diff --git a/Stripe/StripeiOS/Resources/Localizations/lv-LV.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/lv-LV.lproj/Localizable.strings new file mode 100644 index 00000000..c06a3481 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/lv-LV.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ — bezsaistē"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Pievienot jaunu karti…"; + +"Add a Card" = "Kartes pievienošana"; + +"Apt." = "Dzīv."; + +"Contact" = "Kontaktinformācija"; + +"Delivery" = "Piegāde"; + +"Delivery Address" = "Piegādes adrese"; + +"Free" = "Bezmaksas"; + +"Invalid Shipping Address" = "Nederīga piegādes adrese"; + +"Loading…" = "Ielādē…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Tālāk"; + +"Online Banking (FPX)" = "Internetbanka (FPX)"; + +"Payment Method" = "Maksājuma metode"; + +"Shipping" = "Piegāde"; + +"Shipping Method" = "Piegādes metode"; + +"Use Billing" = "Izmantot norēķinu adresi"; + +"Use Delivery" = "Izmantot piegādes adresi"; + +"Use Shipping" = "Izmantot piegādes adresi"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ms-MY.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ms-MY.lproj/Localizable.strings new file mode 100644 index 00000000..a4c59d97 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ms-MY.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – Luar Talian"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Tambah Kad Baharu..."; + +"Add a Card" = "Tambah Kad"; + +"Apt." = "Apt."; + +"Contact" = "Maklumat Hubungan"; + +"Delivery" = "Penghantaran"; + +"Delivery Address" = "Alamat Penghantaran"; + +"Free" = "Percuma"; + +"Invalid Shipping Address" = "Alamat Pengiriman Tidak Sah"; + +"Loading…" = "Sedang dimuatkan..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Seterusnya"; + +"Online Banking (FPX)" = "Perbankan Dalam Talian (FPX)"; + +"Payment Method" = "Kaedah Pembayaran"; + +"Shipping" = "Pengiriman"; + +"Shipping Method" = "Kaedah Pengiriman"; + +"Use Billing" = "Gunakan Alamat Pengebilan"; + +"Use Delivery" = "Gunakan Alamat Penghantaran"; + +"Use Shipping" = "Gunakan Alamat Pengiriman"; diff --git a/Stripe/StripeiOS/Resources/Localizations/mt.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/mt.lproj/Localizable.strings new file mode 100644 index 00000000..d7513a3c --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/mt.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@- Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Żid Kard Ġdida…"; + +"Add a Card" = "Żid Kard"; + +"Apt." = "Appartament"; + +"Contact" = "Kuntatt"; + +"Delivery" = "Kunsinna"; + +"Delivery Address" = "Indirizz tal-Kunsinna"; + +"Free" = "Bla ħlas"; + +"Invalid Shipping Address" = "L-Indirizz għat-Trasport tal-Merkanzija Mhux Validu"; + +"Loading…" = "Qed jillowdja…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Li jmiss"; + +"Online Banking (FPX)" = "Bankar online"; + +"Payment Method" = "Metodu tal-Ħlas"; + +"Shipping" = "Trasport tal-Merkanzija"; + +"Shipping Method" = "Il-Mod tat-Trasport tal-Merkanzija"; + +"Use Billing" = "Uża tal-Kont"; + +"Use Delivery" = "Uża tal-Kunsinna"; + +"Use Shipping" = "Uża tat-Trasport tal-Merkanzija"; diff --git a/Stripe/StripeiOS/Resources/Localizations/nb.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/nb.lproj/Localizable.strings new file mode 100644 index 00000000..336da04f --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/nb.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – frakoblet"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Legg til nytt kort…"; + +"Add a Card" = "Legg til kort"; + +"Apt." = "Leil."; + +"Contact" = "Kontakt"; + +"Delivery" = "Leveranse"; + +"Delivery Address" = "Leveringsadresse"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Ugyldig leveringsadresse"; + +"Loading…" = "Laster ..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Neste"; + +"Online Banking (FPX)" = "Banktjenester på nett (FPX)"; + +"Payment Method" = "Betalingsmåte"; + +"Shipping" = "Frakt"; + +"Shipping Method" = "Forsendelsesmetode"; + +"Use Billing" = "Bruk fakturaadresse"; + +"Use Delivery" = "Bruk leveringsadresse"; + +"Use Shipping" = "Bruk forsendelsesadresse"; diff --git a/Stripe/StripeiOS/Resources/Localizations/nl.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/nl.lproj/Localizable.strings new file mode 100644 index 00000000..d31b0f2f --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/nl.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Nieuwe kaart toevoegen…"; + +"Add a Card" = "Een kaart toevoegen"; + +"Apt." = "Adresregel 2"; + +"Contact" = "Contact"; + +"Delivery" = "Bezorging"; + +"Delivery Address" = "Bezorgadres"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Ongeldig verzendadres"; + +"Loading…" = "Laden…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Volgende"; + +"Online Banking (FPX)" = "Online bankieren (FPX)"; + +"Payment Method" = "Betaalmethode"; + +"Shipping" = "Verzending"; + +"Shipping Method" = "Verzendmethode"; + +"Use Billing" = "Factuuradres gebruiken"; + +"Use Delivery" = "Bezorgadres gebruiken"; + +"Use Shipping" = "Verzendadres gebruiken"; diff --git a/Stripe/StripeiOS/Resources/Localizations/nn-NO.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/nn-NO.lproj/Localizable.strings new file mode 100644 index 00000000..559c25dd --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/nn-NO.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Fråkopla"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Legg til nytt kort …"; + +"Add a Card" = "Legg til kort"; + +"Apt." = "Leilegheit"; + +"Contact" = "Kontakt"; + +"Delivery" = "Leveranse"; + +"Delivery Address" = "Leveringsadresse"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Ugyldig sendingsadresse"; + +"Loading…" = "Lastar …"; + +"Multibanco" = "Multibanco"; + +"Next" = "Neste"; + +"Online Banking (FPX)" = "Nettbank (FPX)"; + +"Payment Method" = "Betalingsmetode"; + +"Shipping" = "Sending"; + +"Shipping Method" = "Sendingsmåte"; + +"Use Billing" = "Bruk fakturering"; + +"Use Delivery" = "Bruk leveringsadressa"; + +"Use Shipping" = "Bruk sendingsadressa"; diff --git a/Stripe/StripeiOS/Resources/Localizations/pl-PL.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/pl-PL.lproj/Localizable.strings new file mode 100644 index 00000000..a5946070 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/pl-PL.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ — offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Dodaj nową kartę…"; + +"Add a Card" = "Dodaj kartę"; + +"Apt." = "Nr lokalu"; + +"Contact" = "Kontakt"; + +"Delivery" = "Dostawa"; + +"Delivery Address" = "Adres dostawy"; + +"Free" = "Bezpłatnie"; + +"Invalid Shipping Address" = "Nieprawidłowy adres dostawy"; + +"Loading…" = "Ładowanie…"; + +"Multibanco" = "Multibanco"; + +"Next" = "Dalej"; + +"Online Banking (FPX)" = "Bankowość elektroniczna (FPX)"; + +"Payment Method" = "Metoda płatności"; + +"Shipping" = "Dostawa"; + +"Shipping Method" = "Metoda dostawy"; + +"Use Billing" = "Użyj adresu płatności"; + +"Use Delivery" = "Użyj adresu dostawy"; + +"Use Shipping" = "Użyj adresu dostawy"; diff --git a/Stripe/StripeiOS/Resources/Localizations/pt-BR.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/pt-BR.lproj/Localizable.strings new file mode 100644 index 00000000..7cdcc17e --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/pt-BR.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Adicionar novo cartão..."; + +"Add a Card" = "Adicionar um cartão"; + +"Apt." = "Apt."; + +"Contact" = "Contato"; + +"Delivery" = "Entrega"; + +"Delivery Address" = "Endereço de entrega"; + +"Free" = "Grátis"; + +"Invalid Shipping Address" = "Endereço de envio inválido"; + +"Loading…" = "Carregando..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Próximo"; + +"Online Banking (FPX)" = "Online Banking (FPX)"; + +"Payment Method" = "Forma de pagamento"; + +"Shipping" = "Dados de entrega"; + +"Shipping Method" = "Método de entrega"; + +"Use Billing" = "Usar cobrança"; + +"Use Delivery" = "Usar entrega"; + +"Use Shipping" = "Usar envio"; diff --git a/Stripe/StripeiOS/Resources/Localizations/pt-PT.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/pt-PT.lproj/Localizable.strings new file mode 100644 index 00000000..0b428ecc --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/pt-PT.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Adicionar Novo Cartão..."; + +"Add a Card" = "Adicionar um cartão"; + +"Apt." = "Apt."; + +"Contact" = "Contacto"; + +"Delivery" = "Entrega"; + +"Delivery Address" = "Endereço de entrega"; + +"Free" = "Gratuito"; + +"Invalid Shipping Address" = "Endereço de envio inválido"; + +"Loading…" = "A carregar..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Seguinte"; + +"Online Banking (FPX)" = "Serviços bancários online (FPX)"; + +"Payment Method" = "Método de Pagamento"; + +"Shipping" = "Envio"; + +"Shipping Method" = "Método de envio"; + +"Use Billing" = "Utilizar endereço de faturação"; + +"Use Delivery" = "Utilizar endereço de entrega"; + +"Use Shipping" = "Utilizar endereço de envio"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ro-RO.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ro-RO.lproj/Localizable.strings new file mode 100644 index 00000000..666d8c8a --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ro-RO.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Adăugare card nou..."; + +"Add a Card" = "Adăugare card"; + +"Apt." = "Apartament"; + +"Contact" = "Detalii de contact"; + +"Delivery" = "Informații privind livrarea"; + +"Delivery Address" = "Adresa de livrare"; + +"Free" = "Expediere gratuită"; + +"Invalid Shipping Address" = "Adresa de expediere nu este validă"; + +"Loading…" = "Se încarcă..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Următorul"; + +"Online Banking (FPX)" = "Servicii bancare electronice (FPX)"; + +"Payment Method" = "Metoda de plată"; + +"Shipping" = "Expediere"; + +"Shipping Method" = "Metoda de expediere"; + +"Use Billing" = "Utilizare adresă de facturare"; + +"Use Delivery" = "Utilizare adresă de livrare"; + +"Use Shipping" = "Utilizare adresă de expediere"; diff --git a/Stripe/StripeiOS/Resources/Localizations/ru.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/ru.lproj/Localizable.strings new file mode 100644 index 00000000..9a9d68d5 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/ru.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - не в сети"; + +"3D Secure" = "Метод 3D Secure"; + +"Add New Card…" = "Добавить новую карту..."; + +"Add a Card" = "Добавить карту"; + +"Apt." = "Кв."; + +"Contact" = "Контактные данные"; + +"Delivery" = "Доставка"; + +"Delivery Address" = "Адрес доставки"; + +"Free" = "Бесплатно"; + +"Invalid Shipping Address" = "Ошибочный адрес доставки"; + +"Loading…" = "Идет загрузка..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Далее"; + +"Online Banking (FPX)" = "Онлайн-банкинг (FPX)"; + +"Payment Method" = "Метод оплаты"; + +"Shipping" = "Доставка"; + +"Shipping Method" = "Метод доставки"; + +"Use Billing" = "Использовать адрес выставления счетов"; + +"Use Delivery" = "Использовать адрес доставки"; + +"Use Shipping" = "Использовать адрес доставки"; diff --git a/Stripe/StripeiOS/Resources/Localizations/sk-SK.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/sk-SK.lproj/Localizable.strings new file mode 100644 index 00000000..fb8fa880 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/sk-SK.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Pridať novú kartu..."; + +"Add a Card" = "Pridať kartu"; + +"Apt." = "Byt"; + +"Contact" = "Kontakt"; + +"Delivery" = "Dodanie"; + +"Delivery Address" = "Dodacia adresa"; + +"Free" = "Zdarma"; + +"Invalid Shipping Address" = "Neplatná dodacia adresa"; + +"Loading…" = "Nahrávanie..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Ďalej"; + +"Online Banking (FPX)" = "Online bankovníctvo (FPX)"; + +"Payment Method" = "Spôsob platby"; + +"Shipping" = "Dodanie"; + +"Shipping Method" = "Spôsob dodania"; + +"Use Billing" = "Použiť fakturačnú adresu"; + +"Use Delivery" = "Použiť adresu doručenia"; + +"Use Shipping" = "Použiť dodaciu adresu"; diff --git a/Stripe/StripeiOS/Resources/Localizations/sl-SI.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/sl-SI.lproj/Localizable.strings new file mode 100644 index 00000000..77eccc7d --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/sl-SI.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – ni dosegljiva"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Dodaj novo kartico ..."; + +"Add a Card" = "Dodajte kartico"; + +"Apt." = "Stanovanje"; + +"Contact" = "Stik"; + +"Delivery" = "Dostava"; + +"Delivery Address" = "Dostavni naslov"; + +"Free" = "Brezplačno"; + +"Invalid Shipping Address" = "Neveljaven dostavni naslov"; + +"Loading…" = "Nalaganje ..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Naprej"; + +"Online Banking (FPX)" = "Spletno bančništvo (FPX)"; + +"Payment Method" = "Načina plačila"; + +"Shipping" = "Dostava"; + +"Shipping Method" = "Način dostave"; + +"Use Billing" = "Uporabi naslov plačnika računa"; + +"Use Delivery" = "Uporabi dostavo"; + +"Use Shipping" = "Uporabi dostavo"; diff --git a/Stripe/StripeiOS/Resources/Localizations/sv.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/sv.lproj/Localizable.strings new file mode 100644 index 00000000..77af2122 --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/sv.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ – Offline"; + +"3D Secure" = "3D Secure"; + +"Add New Card…" = "Lägg till nytt kort …"; + +"Add a Card" = "Lägg till kort"; + +"Apt." = "Lägenhetsnr."; + +"Contact" = "Kontaktinformation"; + +"Delivery" = "Leverans"; + +"Delivery Address" = "Leveransadress"; + +"Free" = "Gratis"; + +"Invalid Shipping Address" = "Ogiltig leveransadress"; + +"Loading…" = "Laddar ..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Nästa"; + +"Online Banking (FPX)" = "Internetbank (FPX)"; + +"Payment Method" = "Betalningsmetod"; + +"Shipping" = "Frakt"; + +"Shipping Method" = "Leveransmetod"; + +"Use Billing" = "Använd faktureringsadress"; + +"Use Delivery" = "Använd leveransadress"; + +"Use Shipping" = "Använd fraktadress"; diff --git a/Stripe/StripeiOS/Resources/Localizations/tk.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/tk.lproj/Localizable.strings new file mode 100644 index 00000000..e69de29b diff --git a/Stripe/StripeiOS/Resources/Localizations/tr.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/tr.lproj/Localizable.strings new file mode 100644 index 00000000..f660bf8d --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/tr.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Çevrimdışı"; + +"3D Secure" = "3D Güvenli"; + +"Add New Card…" = "Yeni Kart Ekle..."; + +"Add a Card" = "Kart Ekle"; + +"Apt." = "Apt."; + +"Contact" = "İletişim"; + +"Delivery" = "Teslimat"; + +"Delivery Address" = "Teslimat Adresi"; + +"Free" = "Ücretsiz"; + +"Invalid Shipping Address" = "Geçersiz Gönderi Adresi"; + +"Loading…" = "Yükleniyor..."; + +"Multibanco" = "Multibanco"; + +"Next" = "İleri"; + +"Online Banking (FPX)" = "Çevrimiçi Bankacılık (FPX)"; + +"Payment Method" = "Ödeme Metodu"; + +"Shipping" = "Gönderi"; + +"Shipping Method" = "Gönderi Metodu"; + +"Use Billing" = "Fatura Adresini Kullan"; + +"Use Delivery" = "Teslimat Adresini Kullan"; + +"Use Shipping" = "Gönderi Adresini Kullan"; diff --git a/Stripe/StripeiOS/Resources/Localizations/vi.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/vi.lproj/Localizable.strings new file mode 100644 index 00000000..3234969e --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/vi.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - Ngoại tuyến"; + +"3D Secure" = "Bảo mật 3D"; + +"Add New Card…" = "Thêm thẻ mới..."; + +"Add a Card" = "Thêm thẻ"; + +"Apt." = "Căn hộ"; + +"Contact" = "Liên hệ"; + +"Delivery" = "Giao hàng"; + +"Delivery Address" = "Địa chỉ giao hàng"; + +"Free" = "Miễn phí"; + +"Invalid Shipping Address" = "Địa chỉ vận chuyển không hợp lệ"; + +"Loading…" = "Đang tải..."; + +"Multibanco" = "Multibanco"; + +"Next" = "Tiếp"; + +"Online Banking (FPX)" = "Ngân hàng Trực tuyến (FPX)"; + +"Payment Method" = "Phương thức thanh toán"; + +"Shipping" = "Vận chuyển"; + +"Shipping Method" = "Phương thức vận chuyển"; + +"Use Billing" = "Sử dụng Hóa đơn"; + +"Use Delivery" = "Sử dụng Giao hàng"; + +"Use Shipping" = "Chọn Vận chuyển"; diff --git a/Stripe/StripeiOS/Resources/Localizations/zh-HK.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/zh-HK.lproj/Localizable.strings new file mode 100644 index 00000000..9e17b1dc --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/zh-HK.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - 離線"; + +"3D Secure" = "3DS 驗證"; + +"Add New Card…" = "添加新卡…"; + +"Add a Card" = "添加卡"; + +"Apt." = "公寓"; + +"Contact" = "聯絡資訊"; + +"Delivery" = "收貨"; + +"Delivery Address" = "收貨地址"; + +"Free" = "免費"; + +"Invalid Shipping Address" = "收貨地址無效"; + +"Loading…" = "正在載入..."; + +"Multibanco" = "Multibanco"; + +"Next" = "下一步"; + +"Online Banking (FPX)" = "網上銀行 (FPX)"; + +"Payment Method" = "支付方式"; + +"Shipping" = "配送"; + +"Shipping Method" = "配送方式"; + +"Use Billing" = "使用賬單地址"; + +"Use Delivery" = "使用收貨地址"; + +"Use Shipping" = "使用收貨地址"; diff --git a/Stripe/StripeiOS/Resources/Localizations/zh-Hans.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..6b428ddb --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - 离线"; + +"3D Secure" = "3DS 验证"; + +"Add New Card…" = "添加新卡……"; + +"Add a Card" = "添加卡"; + +"Apt." = "公寓"; + +"Contact" = "联系信息"; + +"Delivery" = "收货信息"; + +"Delivery Address" = "收货地址"; + +"Free" = "免费"; + +"Invalid Shipping Address" = "收货地址无效"; + +"Loading…" = "正在加载……"; + +"Multibanco" = "Multibanco"; + +"Next" = "下一步"; + +"Online Banking (FPX)" = "网上银行 (FPX)"; + +"Payment Method" = "支付方式"; + +"Shipping" = "配送"; + +"Shipping Method" = "配送方式"; + +"Use Billing" = "使用账单地址"; + +"Use Delivery" = "使用收货地址"; + +"Use Shipping" = "使用收货地址"; diff --git a/Stripe/StripeiOS/Resources/Localizations/zh-Hant.lproj/Localizable.strings b/Stripe/StripeiOS/Resources/Localizations/zh-Hant.lproj/Localizable.strings new file mode 100644 index 00000000..400a7f1c --- /dev/null +++ b/Stripe/StripeiOS/Resources/Localizations/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,39 @@ +"%@ - Offline" = "%@ - 離線"; + +"3D Secure" = "3DS 驗證"; + +"Add New Card…" = "添加新卡..."; + +"Add a Card" = "新增卡"; + +"Apt." = "公寓"; + +"Contact" = "聯絡資訊"; + +"Delivery" = "寄送"; + +"Delivery Address" = "寄送地址"; + +"Free" = "免費"; + +"Invalid Shipping Address" = "收貨地址無效"; + +"Loading…" = "正在載入..."; + +"Multibanco" = "Multibanco"; + +"Next" = "下一步"; + +"Online Banking (FPX)" = "網上銀行 (FPX)"; + +"Payment Method" = "支付方式"; + +"Shipping" = "送貨"; + +"Shipping Method" = "送貨方式"; + +"Use Billing" = "使用帳單地址"; + +"Use Delivery" = "使用寄送地址"; + +"Use Shipping" = "使用收貨地址"; diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/Contents.json new file mode 100644 index 00000000..4a264e40 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_card_form_amex_cvc.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_form_amex_cvc@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_card_form_amex_cvc@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc.png new file mode 100644 index 0000000000000000000000000000000000000000..aabbbb016ef8877556217b9eb47dfb9922a1b74e GIT binary patch literal 1795 zcmZ9Nc{~%0AIH}mxk@=69u^{rmzC=wSMDv$(TXWYo8)+Mgcz2Q%{+64FwF7rgeN_F z9HVK&NY2dFQ_NW=-T%Tet&$w@9*dM|4Vdsgq{$Q69E7KCv2};!;e^V zG$BHQM;kV6_uCPUg~Oqi016AV2mlB++ge-Ph+?l4)rNp>CQ%(E|KK2}E0ND+Xi zYVo!KQuf!%^m3P#&q!Q61$KETvjlsTrPD#|Xa(fh%Ngr+l$BcR3kbLxT|Y+)YrBKh zrDHId7pyTlW}d!(CBP?)ZlLn6q-2V<+(InFx;2bdp_W$XbMIkRTMr}5(je8(1uC}~ zciN5uJqPz><2m36VQ7niQ>(a(n9M`#oLQkaeqe*qft?Mar_FP^)tgki67KE)co@mZ zj)hD{Zztk*QZ9M86R}hSTDz7rhdd#AvH%Sa-a5N>?I6)X87x+m6gfnfC7WpE(gH>T zI6E^dX*ljF32y zjoTGd(u^db4Lmhi&|8zIKNC(MN!=DrB-Q7miBCUq+sFrHi4{LR@DtL4-HIHekM52o za*vfhXg19F^ogW_L#Z5}^Dcfn?)_-3rE&8RV{#Z()G z?GH1bvEx^OaaPhh;+fLICgg^2b0ohYKLq&?{(F4a{|g}hSGiDeH^lH#@XMud9Sti~ zojACyXUQUoHPj@O%Jj3IP@jKw9bI(07>S9`;~7V|+_ywVfv3mESHFG@DH2WkQ9BMD z)bHZIXY$9Jk{j^%enu7HY#qhDD{p0hDqqaYgXs(k+o=Wa)4JdhTM6cE+LdbqUukb( z$v^bn8@)~OymXcwF%5-lZnVVWnL-{RoS=}34b45)l=@`lp7U*Dhl{m0KjGq&%El3b zUydsZeq_m&2Rf!Q2W@C5(EuyyOoNO)*syR~1Vo^srsskLJn5C&%u+*Ic}w;fQRjF5 zr%#o(F_;3D{#!Fw9piQGvM?;Pb;zpPw@3N4ZAGu6J;_@%m;XBvc0|3mAL6!BCK*QZ0Z zaZKGaYKz}0F?`rnNu>)3XSui4CR&Qm`Hz}E{CyLNsqE};zgk^GkrOX?H#k8)PYR%J z1Q|yDOwsbp$qv&U6Q(AS5tD7W9X#|m(fhvsW6^6qLN&)*0&hAM=&w0mwo?hkhVmf? z@l=#<Z@WnusT literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..599e1fb226758e5c8fade69ad5ecd0e21b6f8846 GIT binary patch literal 2975 zcmbVOc{r478y{;KS;i6}q(PP;V~MhjEHl}%oU&xg9j54t1OUhb5o`)DAPJrpC;^$2JpoAmt_52I zaeo)kaDOkb3bX~pfB?X^_o_fEFz!D9$N;ka0ub+a-YapYbczDcx-`_k5mL}G zP1$0wpRb~E+=%p;KR2{d8;Cy6&y=+M&=Pv+*;)dZt9}UwRh_c=^Y~@txtGlY}7dq?wwe3gpmFoU%O_|sE zPxRmQ8YaOYkb@l&nt|KX|VUq7y#?k1q^1FO!KT}rf(6EK0$-=8@>?{7iQsA%jzB!DH zp&X+`rx0VEIBe98HE+LlOpC>!KP4(c_J7Izn7)bBF?V-y?S8Do_sp(1hG)u~j%q*?bPBTjbuPL9afaf^NAd zr$@#e=%%Z^D1tx7E&!>cuA}PrW{A?aX1kpc!A~H(hWX0A3=??bUods`P2%$4L~YOF z1>7=hG(1aeJXNqMTr}tX{K8jG-ix=kUPdeV^%b_*J-e1d;=ZG%G(45;$$7**Ez&Ze zJWE+5$G*KT2Hv1?*-Dl8&v83kH=Yuo$cyC`@6Nx(Y-+rbM~QQ=`;ueX3GNMA5RRF+F)owJL=uN>j#%7WnVk4d)}UG+ ziw+h2IuUia?#z3s5Htq@th4Ud5f~a>hA>igGooS*As{+<{rG7 zzc<<#c6^L$;*R*Sgx&(V(N~x*URiuxz<3{uIbU_KRvO)f;uKT$5@#Yd)M}J z#Ko1ldQFRt3O7qfdi7V)e8T(j@YWaUG`>a}fi@p{T)wECIN0@Q5YuQgquSM=8uX|= zDm^f2#vaQ7kX?-@h^3+L}jIg|)yY+-0tbJg2WK_A&Mmi`;)`lbWJSKb~XMtxqPtmxqI?eT3} zN;t|*o(FJc`PwHaocC-e3oa2#@uEtvZpZuxVw{fjtzmA`G+kAXeu-MG0-(ui3B!bX z4NCc>kUce%jD!@SWckhEk*~kWBA3ml~l!X%xb?;@d$E6hL2kw127A@$YhU_Xe?*o}cHeKg4A^(i+yNDx?>kcwQ^2pDm3@way% z4~5Xy$7|N1cMlIf)p>g1(CysBBz+O zj7z}B1kM~$#m0F|_1$61%rR@%2>8mI)ra>T<%7?qWK)s?dPuM=`g^n2!{*4O6GYY8 za&vAb%!e|!GiQ-$4mhV;>3VO@gND$tWscET=-s1xAHCCy?l-}Y9{oTihPyrZlK_tt zM9WT1c2-!@K7${A#yW_iaey~Y>}1m{IxI^&zB?Ef7DPdgb@EPgd%suG{Dv0CLe}9? z&*1K9aw(RNOSF=tTi^_iq8iGD5l*eaiVofKakHz%l&-lL2vh}^Tni6=BfxVuTJw_H zL+M%)FVT)E*9*5}asj$vw$YACF*(5FVScro+8v8~Z})fOU6QV%W4EwPpOB=uz^aiu zlGCmpOBy!5Ly^=X@GES5SCQGNp-(o;Uo1MQm)Gx^G1kJV5biAdlBFKKb9vdSsxIJ0 z6O5B6JH(n;3(lm!)%Kb;^`qn2S=$NS++Lmt(dCEu&aDw>s)Ink#A(aztClsuPQV| nZrjN7VU7I!DxXM2Br_nCze4Ysf0Xt6`A-{TWMNos;F|DX+2@|G literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_amex_cvc.imageset/stp_card_form_amex_cvc@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ef2077b73b55e96b8319c746495cd05a0f61c76 GIT binary patch literal 4550 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 T*85}s`#UF_Q;79Psfqsu3B!8U literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/stp_card_form_back@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_back.imageset/stp_card_form_back@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..00b054591fe4af878ae7bd9390de86347ae93371 GIT binary patch literal 4810 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 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/Contents.json new file mode 100644 index 00000000..943a813d --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_card_form_front.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_form_front@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_card_form_front@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front.png new file mode 100644 index 0000000000000000000000000000000000000000..08271ba1a8846e375b6e164ca9493f4fbf2d8a9d GIT binary patch literal 964 zcmeAS@N?(olHy`uVBq!ia0vp^8-O^UgBeIxGwzxOq&^4ugt!7}hN}Mz)&D^xoKf?i zp#dfW5eIUC2q+HW0?Ab{b|+XIXu^N68XyCx5=a6S14#%2$gcd)(D9#PCQKZnruIKW z9aufYaEQG?EfALg8Er6IAcAmlAQysw8X-=CC;*Z`Gl3!y5~5}jOa$ztWm$4BfW8ha z3GxeOFlyX)^Yi}@r4x^*c&hO@ym^|!{A(kx;zUNK;}T{*p^+|L8vLy4H^c%Bodpb3 z-Lzu=ZO9V+@J0TRXH2WPN^-0p(`Jy#;9jSotbUomBp}3E1Dtg^Uk zj6Va!V|p3ZDK2ZxZkQNj)7yA++0iX!#>~e)eGp~(ta~i?xr4@h2f+X14?N*R zk$|gU){{FRZLFMTwr6MR&xMj1vB4|m2&dj#856v)?!&h}^S?*d{f-O#bLL;;az5jQ zn+~1Y6TEYo)87Zm<@Yx{7XBWr-ErTt_u|2l zv0I$7AGQ}nZ~yph)>ccqxnItH49)FpzkRm9{IA0IeMg)+ehXQO+uU=G`LJP|gL#ls z+`@}<=l465*Ohp_G`=}~ira0a`>VK0c;84H{owxYy6S%uC_8$(`njxgN@xNA5@y3o literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e5a9048780b8854158cebe7c27ace154fbf1ab28 GIT binary patch literal 2424 zcmcIlcT`h(7Jd*CAOZ@cJ&|8LvGX*NggabOBO-~pi<7QzD{0}(JF^MJ68Qqp>`NdWOp zCKy8&!vlE#(E<(uCx8YqK~DU73dunrR0T_rV)zC*Z9^ACBK|&elL>W!q96eTAsRRq zoSC)J0>+zibsH*>DCDp508j#qAtpEzydhA!d4XU6a-ca-0I7f;1UE|%A3D|MG>`z{ zpofH@6vQ`0H#uQEROkU4@K1>eup=G=V0!*J4Sk$Hc?g^0Y2lEoo}Vo{79L;3D{bYA z8PBywij>Y#MHSkYGo_{2ihm64Z(<1MM;n?SJ?=CtZ|-& z^2oLc?Wyg$YI1&N2xWoLfsyO#&t4f7ur#=<#?Eyg!=vazR$k4cwQDEQD_>nOD)reo zrb$^z3K;+dQ3R~1eJ~%V6&7O7heohyvmY*;|M-NqoLh)0oxl1;$h=TBeQ%`&lRczz z^byP0r}-sliljktmELPW`Ef6fl2|*@i?Y%q)-M4fsuUcKoyu zg;hhEYU2d$yo9u!Jgi@zRlx5QS>A;nmYCDI*<_kB*CrM#O%0>ClD8+Ix$+gqsdT(% zLju~ss}V3QG5HmF-~Zk(Ot%-_9hQcHDH)M0lYK}fvIR9r$9a57-tpItyYle}fI;7X)n2A--e`&|P!qNlaOItKx4CFxD--9bF;ehS}}%odFfaAIcMX zK@&_S%`ajAH)YX;(MzJ5B_7fcRnkmNotr^gV)y;-^_y~ve$8DcXL`-kLl1W>l@NU8 zw>5+i-y@?PbbUU-wCE_=FYfAR@BSQZquQa7g>(F(r^5lr7OUfjdkcom7Vp^;RZ>1Y zIwec#yt$^w>b^O5z>G0p@hP%{eyYl&vn}`Sw&LZoL?M#HM5AS(celL#n4y4=wf{c4 z&6ajpxFPILaln?5GshX z?vQzW`~B{|f$PQpiP0|zj}V~%HAc9IQThaCH)v& zqbRulaB^;DVM}&@=vsdH+UI>wkCjT=ex(}SDC;Mlsin_Q+;WsIrjbLI-xe({4iG02 zPfSG*j@3(kx?e15e`x9vZBn1S^w4-kY+|^U1b0)d#e~)0ejQ@^8OH17Voa`1a&$O9 zbnU!8nQU{dnp8OZV^lWJC(Gu^@q+Ox7D3*mP<}? z-`|m%o#e4@Cp?R@1J79jzmgCEp2sNr4l2LD?ipd@4wSkV#!1@i`T0HnwgT(!+cq5i zyIbFn?~L+6MQHQRd1A!dYTg)7#^&)Xef7(6aU-5GwNV8R51(u~#4S)xj2pf7?_DOg z#7@yH5#qKM76YM0USl@QbEvS`DmG@9qGtFqx)x)#eC^%nzffZ)WpPnu{(z4gJA&f< zbFBL~!51#xYDrvh7xn7)=8Se_Q3KtA#QDZwXFRsMogE}ts=n$)4zz9`6QaFmmx(e> z8+~6u4|CM89X-X)unX&27qg0Fk=J~0MOJ2}2JH~r^ZmI8G)qv-rX$50ZwZMe=Z@k_ zs0X;*n=xCW_ounK^4%bv(O1th>l@hJ+|3_dAUk24EBO=F-w9>UubHzo%Mpvgklx8! zaYvI5xz!|6L$~bu<5BtL*OrzWH2)F@T1x8r{QBN zNz%1>1S5~f-L&kQgrSloqE*YA*YTvFvmxGd&eJM1@530PY@p~FGl%QdNz~!iR}Buf zh=BQQTNi@d{@|{n3?+J#g$k`&OX$^TrtQ#HdYsBjnv87p7DfiGI@n8KTYb~LL<<*# zWNQ(zETwaA@4%u1COFcUJxOSsvUN^!M2B$-{|zE%_>3L|PuAVX)U;LoD;E9Io lMb(t0V3!M4fyYH~VK6(@8^Y>Ey|UoP8X(|^*b*~$>R*X&hA034 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Cards/stp_card_form_front.imageset/stp_card_form_front@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6512912e9f5a1847667085431c008e0cb40dbfde GIT binary patch literal 3414 zcmZ{mXH-+^7KW365PCwdLkmq22-0hSfI(_#0;3cuD$>LOX+nS?p-3H26p*4^I$n`B zR2xJoK|}%JN@&tM5m149h6pli-Fwzv-`elq&wjss*7=<_378n8I_Temb1mJ-L*nx16P;IHLh(i+S#vb}p4XJklFf}ukOPK1MQ z&G+qenmOB)nDthXA!H+gCms6adlEbBh3s?Ew)eHZQN`+J)>5lCV7Qcu!FdNXx2hVC zu$l0qvvxZCEcEisC>udqxf6DG5cc*Ac}kWXl*F?}E*k0=8OML?HWiZ8RhBle<%8kI(s(ciVS+nUAB5*NBQI7Vle zb|*O)Zt4WC`1!X~uLvV@zU}NqFWftAhY+m!)a|d{M8mKp7}SuoYBCybes^S&vO}X6 zY>(hw{oedreOJ7IzAIQRKfycaifkUpHx;dw^{N)b*n2Kfy374Wh)B+xnWubz#$Q!4 zBNS9v%7X7=O6^e&4mggJwto)Hvxtm(4P28hwIi2!GD%dbz^ZCBrLQWF6FhmcorIxV zblUE>&D2Wz^*xp7PKZpx6)PD9MNCt(dr@$rcY>)c?iSMhi=HKvJx81)Mw`j~S%@Wb z`k+|GZK0F!{RcC8dYFjh{rHnZVzO&&O`G`$WA0e+cFI~#d-pRR%`l(e9lnj!`3P9F zTn|W7Ef0k!|8>3X@-{?Qc|tToHV;!5CgUk8HC?-fm_Q1%UHxGUY0G?WBNw$2X(ZWJ zPjdTsqj?W3oE@>}1esQ7b{3sq5KetAELbR!Q{XDh7qRVVW5}Q{&c02@_gVNczd(wG zsd=U-le&j>2FKbOvh3Yf+PDBi64~!P`?lbmr>Aj1Rb&L%q1WNw$`&Rdh;B~jZoAT+ zmGu0id}{HaxO!a!?I|^r#4bb&_|eXU8#Z>^x*0RT*k4hVwh+%^|8f{pk#VgYQzKR; zoviDf5V~cJZmuy5Xad_^52`cvZHXaRjB(Y*x0heUh7mrAHMW7f)CA}e^{`qguPv^D zf|6x+bVa(>$(X>Lr*7%x1nD{MKq$p%04f$bNEEk!qb@lZ3SY}AQ3WZaiO++I`BFyZ zV={A|-s*o4HHWh4rNwbzD_7c;One`{u0Krw)eT3yck>HIN+uQx1})<9%kzd*mW0XRxZpVmzFr< ze>lH6rIyts7D<7ZaZxx)=ntZseA$jTx5ilH`WgH@TKnYm;SPWljqXBpLG`xUNX))-n5Ft1hs zV=|=}vl1DLN%QcpIB!(EcOII)tt!!0V?wXY9jgEWE#NfkM0)CRY`*uLII4%0vfn8z zwEiGAUF+BvIim|j21R(MTrnYxy&>f~D@nXkb08p>e=M`MSRhkJbi~S&LuG{#pbD{dWBIjt2f8t?@r;STx;n zOKQ%4WcDkWNO5>N@Kym7ePt|~6EO+99!D^F9RgY(#2`kOE?yt18=JnXuDO_6w&(pj>l$d@xMoDnhH>eBuC}H$ z7zA>G4Yg4&F;B_G=q~xuMv1^^qfsy@L{9|zw?K0qR|l(5+0+fYkJ|ik06^7<=Eam1YBv_x}SNRu;Lr%IOI2_4|$Ch<5vFt3aUeD8ouOfF8Wm#Hs3nZUd zW?(7RXbJAJ=VEfdtk1mG1qw3!&>EAE4Df(w6@0ZLB57N!PkHgQlM@S7;7ar90<1~y z>H?266Y|Zl@~jj6OofiHeie)5cFac=^|9NjPS{9&pLnkb4$&Kkn}WQZ0d>Z`qEkJd z*xxNll@gMD@)@HS@U9iFS<<`WWu~u@#^0uw+|)HBYIY&c+qrU{W4VkvBo_WAjPT&6`Y z2hUb-qI7M?z7s+u&pVRCYET1%x*{D5bH$<+%W`Z;-SE?8Gzr$Q*AO+H-LIk$yg-`2G~q@RuVDMs);OfRXHR?aerPjFE^F{iU~Jon zkKF|qg z{!>eoX};bYfwwj$muIRwr4T#|b8Ca7CSldV^G`^^XIxn$%xO#dK)_>cXpXMZcZvQl DPdo00 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/Contents.json new file mode 100644 index 00000000..182babd3 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_affin_bank.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_affin_bank@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_affin_bank@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/stp_bank_fpx_affin_bank.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_affin_bank.imageset/stp_bank_fpx_affin_bank.png new file mode 100644 index 0000000000000000000000000000000000000000..c55aad7c142b023a95a8d59ba46b972dc444c094 GIT binary patch literal 627 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 Eg8Y#4WB>pF literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f642076bb5d0e0de8472a4673d0ebd551143d3 GIT binary patch literal 806 zcmV+>1KIqEP)00001b5ch_0Itp) z=>Px%;7LS5RA}Dq+22c3VHm*i^M`cVO*1kJ96?F6m0GkEi;#+lkRZ~aZ8>!c&3bg1)g=bYy`=RMmS@56)W#a_-= z_L<--yWEC4BBlCA^DwiBl?7#FrIiW|L8n$0eY0@W8eEg8_f;2M>7#@3^ zw{D{Gq^$ua?Hbq2D!{y5gPNHHSY_9UX6pl>c7Lv>&1kkh0Ax3{w6gPX@7(uAv}TI~ zpmw{i+$XPUyEuy0Y;gd{?wb!^$p3WZ7f!zXgwt$6K+T~Zgxy>&*L3Qd@*MTY`+u!v z5Sq8{>oglsS=)}Vdv{_|^?7o0{3$|nh1;k5KW(jmiAPg7yG~C~6|iM*2Tt=?Jc$e- zcSJi5cBAdmi_@%oLsc~3(w{pbv|V~}ngjhYaDZ}0WX3MNIL+iYLSkS6${mqOyY%8T zHx|F4l9<}1*K*AgYrCv9OU3LSJsY_kNf_L7J)^)YoDt)PpYRTAXqF1vEzd2e`JVt^ zII6nntgJmxlC-zN?RU7m;C1_SPu{f0uX>)-!4be;Vlqq4nuT@iOSOxRDqSQZKm>?@ k|2v@kM+Arf5y1KU2C}}y$+3sfR{#J207*qoM6N<$f^`3W5C8xG literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_alliance_bank.imageset/stp_bank_fpx_alliance_bank@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f28c253c1f7454d365e9b766627dc224a91291 GIT binary patch literal 1067 zcmV+`1l0S9P)Px&Ejloag<`e!&m8 z?oa72jqXU4fC5n<3Pgb@5Cx(@6o~iC&i`y|8>B#Sr?R0hIkW6t=raYA5$m)R8wW&p-F$7X^yioVKSbHjupj#(oME zwOLh1UU9VM97wTQ-qc;bVDnw2(f$)HZV#lgx$V~Q!a_K^phhUIjRiVY-I06bBGm54 z4=OD@g>?&yVflz>FQ%eDXjx^u4>tgw_~6NF+(7aftQR0VIuTM%De|`?ozgQf=Ey*& zgB`J_e3_m}$vI1bN)(&Few$Stbmis{1&Z3t9;DLRy@7HHYa=$Z6RWj*1I60R4!G71 z3#77{9dNCk6iCx%c3`!3QXoy6v9)$gARU`gwRTJ(eVf@i>A&HkK=LniMxPQ&YX<~M z&}Jm99S|r%n~}5@J&+-rF|-yvkWrh#wH7*%37f&S7B|plPlY*~*}2^HXblFsYSw00 ztpyEa(Pluc1r0PZ@gDq-4d%2KF%XE&P+AKY2+(E_t%VB&Xfue`Vg&-XnVqNOlMW7q zVY5Z81qy^@vqh~%350616|F@Hgle-Dt%V3=gUzP379x;6Hk;F$e<0gzHm5b`KsMWK zLTk=}Y_{2i);t4ovDv8BJOlBvSwrip>%s*Bv03)ZSKo`OHJ?CGHWvl{e;a7*)dxJS z`2+&7d1N9Kc{T5$`tZ^pB&|6F0}ziEo}2zY zhoQCIfr*_t+AN6Hl8(*$jy1#DJo++ZS(PI==_Dgc>mgAtJk+FyWHq*+*zKKQ3ANh z8Kov3L*DfKV7#|B66czPu@B7Vv%E9w);o89cVd1XM1d#}1)@L{hyqa{3gqHIE*uI( lfhZ6KqCgag0#P8h&Oh3v`dAhczx@CJ002ovPDHLkV1o1Z@_YaQ literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/Contents.json new file mode 100644 index 00000000..5025fc70 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_ambank.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_ambank@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_ambank@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank.png new file mode 100644 index 0000000000000000000000000000000000000000..2de2c6608ade2beadc4116eb25a2b9c0343a2eee GIT binary patch literal 585 zcmV-P0=E5$P)Px%14%?dR7l5Tcq^kqQ^YW8!Kej8)q+onX+Jk_`}^$Kzn?$KLXJbL`WC-}X++wV(P zuqya3`e+=-yB9sec|k#H9eJ9nB6D3vjs|832RjQ%vH&zkhKl z02+eb0${kKJLlemAFJ2>K6e34{M)oyz|a6GxO?yC*6qMJKvRGm9|#M8v5Ktd`>eUZ z$b?9J&dmGw?;pYwFJ68rtNv161Jx_9{(Zq>WWB&R$6^66%^=+V@DWfOvQK|(*oo;%} zzpvdux1h8V#e%?aGzGt}UPFZ8>o-^|0OlrSBY>d{mjVU|ehWSql%gc0!$+}L0L-gs ziQ(tYJztyKzb{=0Of6U~_>`3XrM?9irDz&K*&a(iP|^bCRqWa4=`*Yr0249R438*O z&@vjZB>4CJ2MHFeT8o+0frj8Ki#~=#0rM(m0YHoeK#f4{giF(RI>x}f`uF(@gaxT%ux$QEkH*A X_4SPVFc&W200000NkvXXu0mjfm5~uB literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6c152301352df0d1f2bef4e14a035aad2fce504a GIT binary patch literal 1097 zcmV-P1h)H$P)00001b5ch_0Itp) z=>Px(14%?dRA}Dqnfpr=Q547hLwZ=`BTKFD5haR6Wkgn>{*aOuYVb3cV=fhOQw~3@7$Snjp$zH$Gzv?&%O7Y z^F3#Mm^q97>CcA(Pyh-*0Vn_kpa2wr0#E=7FlzvYwHwXnN^DKdQg6S5*y% zDODti+=BGJ+qflhW&niTql(?`)IRJjt!!unSP&o9>zJqF@C{+ z%z{UP--P(!k?Uv%2H|J$M- zWkXy%$^t%PfP^6e0G)#};rRNE4O)%~YUYws%npDMJOQVaLLwY2i;QMX%|rm8(vZi0 zpn--7Fsv3MttW0K=wjnD~M6KzCBYFBo6= zfoiZ;RKj}9Jbzc{3=W5rlRppJTiZ1NELc1he3yn<%E}d4#w`jSJT3hrwGjRlP2Gl( zhkZ|~AWZC$gAm*TqQmww-IGHNI+1~)jEyHHqHVSYBjgDf{ zdjPH$f##CZHxR0FF9IYclbU_o_N&8asEI*ZVUpVH0LUYwunqlP2`(}k{OoT5NIiYr z${3FuTmB)ohNq-=J#zrAR)AWgQbTKz-;s`y*?IsHwj&R|Z-7L2Yj5t(^@Cgpl_4Z! z_h(k$;0XX^6|wFypSk}>e)ML0-8ay%As+4ow7a?Jv?)8+)intY|EWaw2zkfdelTk) zLM)2};b}fqEF3=OY7Ri1IoV64{|`_A3P1rU00p1`6o3Ly017|>{tJNb%k?w;VR*%O P00000NkvXXu0mjf&eZBZ literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ambank.imageset/stp_bank_fpx_ambank@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb86b8d8173d701cd76df942f18f2e8db778d60 GIT binary patch literal 1638 zcmV-s2ATPZP)Px*AW1|)RCwC$oB3}QRTRhjhbTnt61q|#2-pY+E`SCwEC#SNq%8_o(1H{qn5c29 z1*(ERdvX{W+|$DB5s*NDlD=j$;Bzu&-cS)M*|Qy{(k+!cOeg&Qnn|29QX$x24t>} z5BrhJnCvYe->f-C_qC8`O+WWl|GoAdB&}IzS(fYL8&|vdnU8q|2=yw+x)A?6JvCxoy3QX3YHy;qOFWuFF52oCP+~fBBw_Nt;LpDpt!fBx1Z40T4u?)U)>V`rj<(r%WFhXH25+iA73zrKRh1n@H2RlT?WsHD8Ii(E_$b0bD%cN##?Ro~8 zg8c2mG8qAp>s25X(=H%TQc7yt-r3FoNLa9hTn1qT1gblcnJ`Vb$f*U%m3gFZc;5jw zIT#*{$ffS+WCR2`2_gg3cb#DYzPSrXZ{oUoHnr5eaf9du2NXs?kW?X>K$ckm&7MmN zsJ?*>kR^&lFW4ur9 zTTkt&vqke%Y90uW{Ir=7O@mrlMF;52msCLpWVET?t0TEX*;_#;1_`3P_0=JB2Jq-m zvj96koY?UK5Qs$PHM+!>4k> z_DM*C00(MHppXITp5Yy;M~*4G%fYV0LxV>?z;x0B5b=|AdE0A)C-T4h9()}G#V2s9 z!byjM8|j6Ygr~Bhpb0;R!tccp5CTF#2nag@LO=)z0U;m+gn$qb0>X}f5D)@FKnMr{ kAs_^V9RVRA1Qgx=H;60jwuTvY761SM07*qoM6N<$g8Ay{y8r+H literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/Contents.json new file mode 100644 index 00000000..4749fb7c --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_bank_islam.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_bank_islam@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_bank_islam@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam.png new file mode 100644 index 0000000000000000000000000000000000000000..ccff189ddf3f950bb8060828d6e8e95b09a49da1 GIT binary patch literal 463 zcmV;=0WkiFP)Px$iAh93R7l5TIL=;7Q^YW8!Kei^wcxBu^WCh~_wv?W51f0=ch&{N?sMv`XA~Px z@RU(z0axj*xFzp)Tzgo(<&0bdm3?s8cH*}iFQ0VnJ;7g2{UEwuwEoYh?^iu%&?FA- zX0H18=kImj*))mdvr0{Wzx{f-;3Tb5(er6X{{R1fUb~%E7JR+>{MVZ=G|UHozWn%p z`!x+M_OP2jW3qBsXM=ejBmZ?Y22hS!R z0NU}mWhYfkKd0G-Zovue(s$dh0quCyu$>aqPl?yu2%d+dEaEJAG4m+UjyLNs0<#_& zre8Ge1(pZ|O4DmzGrrw?2~4<;+jpN;Cf1sB8m*7&x8Dp~KvaDI46d6|i#{B<4J@7B z@4WG#Y||x+{u8|D)c{cBqDjyFl8rBB9Rrf5M5;)x(ay*>+=^fFa_$LW0{ijs-OuMA zzg&F!<=o>>$M3)2asBPa%fLDVXg8j;H(KeBS}+7G002(UQ|rM{NnZc}002ovPDHLk FV1g`9-}V3i literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_islam.imageset/stp_bank_fpx_bank_islam@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d17971a24b71f436fd3fe17a4410c51a368b6595 GIT binary patch literal 992 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%0DrJI zmw4U>020=_1fbF?0?n-iSm06W8tg=XrtTrI-Rl6#EhLcW8ZPmy?+|#^-T+Z;2GHHN z0d_cvx`tUFV7?0oyyQlJQkw%LL{hm_xvdC1}KEg#`ekJVIR^j7^(1ZCV5WLSU-tu~;m2#J>V08(DZ6 z8HNDtF{X)HT@iS`+xY=WQd9;bpZU(98cA2Qb<5np0P! zPx%4M{{nR7l5T5GdS9Q^YW8!H}_FYrgA)e04aQRuGd_P=zdVI^RsPa5I?}#1}Rq zn{_>3*SKK8#e5@V!K8wEGJPOZxbbYh39?zG1x1YoS;!*$^6iBRx07i>b3q2OS;z7% z4GS0D%GX8~3@qp&GY)`e-O1NNHY>QGYes$~vIxXaWLmJSAOP8*)dfB_g|pC12cm2F zdMX90NVXuLpaV+$TZiE|mJ6{`^ z8MhT6io(Q#2Exe{Sh@kT7%p@pUl-ZQ%M1J+3#UWad-5I73MK3oC>E~0oNtIHpwJMo z{Fqq)DnGmmCJ>v^(hF*cx8Qibg;Bwx`2`^)Ka1*U8kS^Tl hU(h?+@EX(>004_TDX#hfkgEUy002ovPDHLkV1jry`V9a8 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe59be10c8068d95923500e17c9dea79ee3d603 GIT binary patch literal 1098 zcmV-Q1hxB#P)00001b5ch_0Itp) z=>Px(1W80eRA}DqT4_%cVG!mgcxzNXi1C1;A_x?N0SzQ7A|{9+7`z4Xm>E<&5dje~ z9`!&8;Sdv%GvHCd7){iO8a2kC##?`iLorP)bauP#c5SlBd}z9pedl@KnR#aB-HjnUrNcLxtC|Lg< zqC=@?A>H@^;ey&l*l5fN6ytt*`*&a|tF>UJpz{SHXH$m(0A^9gCzvl-_7IkiqrMcp zB7;1JdyI*JTF8?vOQp-K)(5GA&O3;iPPYvJkVMyH>l-1P)$*uHRx7|`24tbJ)ZdRO zqFY-cuyr!oUxHOodkTpY=)M5}wqTjOuSan&tL;+?@qDZ>fCoacmmOCy>lMULp}RAv z`vWYH0W?9D0RXD7KsGCvs)m>gw#sV5!Z|9S0HUcyrlek6&T5Lx<&+1wj3X*=0NQc2 zV9hU>n?MbU%;hGV@kD?CWa1gw6q}$q*SW)3BUm?=Hxw6Y`95Ym^+e4y2)=y#=4bg^CWBVtX#dSOsO6L+Nxpede zWPk1MRkvM&k9IECe&f{k1KaVh(r;&axr{zA_ z7vX4TZwYf5z@(AQ|NQ`u7`{V#tu8V`W`m_e65rUzC8-=_wJbksSE zw_-!^0dQUrdX+l=ua7+vseTZn*qws-Yn51aWsmu}jS0X6U;;1!m;gq|ABs_?zf>Wu QMF0Q*07*qoM6N<$f|TRz1^@s6 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bank_rakyat.imageset/stp_bank_fpx_bank_rakyat@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..00d9c0f404531fe92db403d7f279428efd66bc13 GIT binary patch literal 1583 zcmV+~2GIG5P)Px)=}AOERCwC$T>EcTMG)?vprY2O^^u?;uS$!yMg+lBMOzBiDkNCYmNtMgGbjqa zkc#D@t)&_?L`vlmpr}w45PV0Y_=A8N1;y8&;^#f~SlWAUclMsW_nflHgtX;McK17b zX1;ms7F{4my=0Lh4}k~B1LOhn0C|8sKpr3ukQWb-2gn2D0rCKOfDGh9`RFiL{Y_)c zWi?ZvyO(Nzr*Y=ez^%yxXpyW{p>v!nFOv_TSi>DE#ZUS6i{wbc04kBb6I8BR=R8>t zMJ)9*nyyl;kyn%ViM_-vszn~4S%KnWeom#hpR1E+gUWcvA9REHBLAYXm9hB7p z1`xiHGc-{J(7nGF{%VV}^qS<4%#D zxC8W#lD~w}la?8#6$E{;2B@AlsMd3uCSEVSfo?C=dtX7z84A z%+R&VcLEUd$6ESZoT9%L%+N60jS)b|GiV)99dEK`hOS+H^Z;$62kjE0m&^Nh%rLub zq{bKlRK>4r{V~`;j?8e_A@YAY!BQbha zzkyiCEvKl=Tu>2IjWt`^9Rh@G4jj#}f(2{e?OcAvZr)3kFT>j98v!&`wmRDUU(r;% z{1!gv&uN-!tYua%-}Dp9F*wR1%jGF8G{FzZ$!cBx)q0n23eafo{e>pE00^}mbE<+T zck!I?SN!bSa-RvUjfji`odPU*$LF3gDC$b zm4}6$Hf~mpwTEjRx_QI|1*9tIVi@|dSR^e~E?>=qv{kC8<~^U=(l#|fTKHLaF25)b zc|vU|b*P|w>N~0!^n4%R9r`tG4#3>y+c0U^R7aaA)4Vf$c7H@Xs@@)Wuuf+gv zT>j*t^G@YY(sT3^1FZE9s&nD;En_jfYDb%B11%mjRyWm~JYOn1RRP*Dn&A*23#M-< z;5Fz%3kSMWUK^GoAjuy_yo&Zk?r zn}_Tnrp*SGZcv19(iyQKnFhl%-fXIbl87kwoo|}Uy-Njv5 z9y>Fj0GAJd@Wwu-8IC-fHlVAJ|2~`^zCY7rpj7!zx!$4dk8&pm`?)pxug<2UPN45rjdONRb$Bd;k;(jMuwpQ$ts0AY`mr24P$@5ysdgxDQyiOh>3 za~hLP)YfmUv0n!My_!1j|>^It?>)9%`&`R{YQk zPVU9A!wi*DBzDEZjMJ4iT3X=k_b?4Ctxw~UG&50!CJYoR?{mazQ7U4mS zWZ(X>iaC2@==ly1n)6|aO=`2}@BKm5Ll`MKZT$sH=Qj5910gy{NR7(vGMo_z&{)|U z<`2;0;jidQBg{_%owvEp-(i+9+l!Ou!BKk;P{v!W{4IF?nm`Yb2gn2D0rKJj@&I{& hJU|{G573B_e*o2fcQdqz1L*(&002ovPDHLkV1jRf{D=Sm literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/Contents.json new file mode 100644 index 00000000..9f546f44 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_bsn.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_bsn@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_bsn@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn.png new file mode 100644 index 0000000000000000000000000000000000000000..686dc52b64aaad79e51df5df50b158846ab0cf6a GIT binary patch literal 885 zcmV-*1B(2KP)Px&FG)l}R7l6|R!L}6Q53CKaUp_eoe-Q5v0_((f^Ni(A_}62A_xlNu(7&uA*kR& zMZ_vN(N=4lCQX~PP17Oilr~M;v^HroHAyqiX~v(qP3Xqse_JX3n5+~oA^+ub&wb~f zb8pI4)v`+(W&aXDMH7EA6fwjTmAsyk09+ZCce%V@=WWlF+F}B%u4R9in^y$GmxpBL zL@J!io$iy|kSew}w-*QCs6dRoPn4Ptr{|`0vcYTvQDTo@8Hp7EU~?mHM{|cQl@2ZC zz8EdnM<(h`R$Stf!E8yU{ubbT|7awan@=S7bA=~_1FA^u(bO!;m-rW6spn3155Aby zS1VM71AI1EYIG*qLI{Q9@$s}eu(*_7UM4C6X>E45HWokCGgKfz6~7ntN&~^!D7B@D zk4qrCil%=3H!jYR-J8uGT2ZyX2Pm)OywMuiRwwe}@qt1G747B-0D!6wwsniWf#bpf z)P8kXes6qwQzMTVz&Y`#G8|=D9bCI>O&td(t#%j7;s5|zg2I7;!wn6szU(5*!Ld1+ z0UT%*I#OxqGV9DoY}|if{*}!fDiD_o)!U6seB%vc!x1X$a0w9i|^~|Ti$Ao zP<@}rkL<>H0w>RBbV!ZDuSv$ADm9QYGXQwXmC1CwedRPiL_(x|O^iZ6K;SV!Kh9Bv zGo8V8_qKE~06_M;@*hYC$)6EP$RVg87&v_Imud2U>MoHpKwo(x7a}7XbBV zMI%U8qcg6p=iFDQ2mpS)H8us(#sVSm0})RI!&_PH3;;0TL|%U^pXWK;{oY^HtD}^N z{(`{$-sw%igB9??YmEW$%zCi#ds{S@$E|0gv7PLWi-Thr2^c{H0OiAjun0y4-{D3} zE(}N+I~hYCrUe^NJxG9N5Ci~WO{V6k1UU3$vM_TMt+zs^={8n73V*LR!C&@Nnq)U6 zk{DBv6XxBKPEjRoy2AGX78yi>0SE0PJL&~#@WdbPH3lp54lP^^*HSJ0YFei9(gA!l zn2AP91)!(mq{%Bxp27ik(aV4YS#U)LfUV$LcpDVK9RL5D7BPMTA$JrrEco7U00000 LNkvXXu0mjfwgrk5 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aea604cb4990c7865538a8315fd58d8ea2146fa4 GIT binary patch literal 1874 zcmV-Y2d(&tP)00001b5ch_0Itp) z=>Px+3`s;mRA}Dqns;d3RTRf-#VsmY_ZAha78OMWsZy;29MC$d;shsZYu(hXhzpC< zQQLIbq}hA#(d@jJm%aDiyGfI#|M7>idwJ)3&-$M8%d!)8 z?00Kf<^kXV-~r$Pa1{B%TGBXnuxD~Ipt0rreZg)9;H|8JgGY`iiEZn2GXUvbJ?Qai_0q0T4&8 zA02xy$4cg5{d{UR`vpv=LbSJqfI7SCR7_bG+qB&1`glVj1B8KmFrmHkv|av74dB_7EWW#Kd;)FW&n;xVr?x?z zUh5yp4oVl=LxL=%PS4yO06}82d}w&su*|0g#Y+tUw-%p%H6t&grJZ$JXOI4XrEfqF ziR-j7e0M~GyQQl^}4Fnmk|N~b#)fLw}iwl9f0%x4f>>@ zhzou&CPm+WEIu7=$TpA0r}3-eaL6+$S*l?7Pfd}s?hKFr%uQI+ z!4yO`<`(LwUmJXZ^n%B)42)8bK_$pDLXCbYsnqi(6qSTaDD!7uO3yJJz>eyANuBX+ zoh-Oc45CiaEULAGeJGG_f$wZ(dATbdaG3jch^4^BR*qn=SNewkQ#JFK{Tf=#1pozT(qaILF9}C(ov**xClszP4x`Ij zE#hf+ek-j~l~@-lwCPz0C;1@H_G59y>c~X3dFlSpXv6zqov&o%-V>Rqt3`mSC#e-2KT4p3ehaCy>rmc^N?+Z_hX!Jqh?DQ*qmph zYm?Bw$}b!buel^HVxvbE`Dcqy&m^mN_J159FfEXl_>zI zJA4Oyt<~T;^e*$Zwm6sr09-{{d&=DUIbM#rRPT>$Evq&QfP@_>kNZ@>=S zgHWxogTs@T_=KtsZjC_@@h-Cf2#3MS0Y=xW1cN-4oT)!aTAT~u!+e|m4AmN2(cCCE z+iaN5L;z1GXDUOpcq20(QXOd|AB4=PdsOA@5=$MBE}kUDSjB25+td)fSwlEAF$+Pw zC}E+^ZClH#ZV!u7y=Cv@jCTP>4RLeqaVbKrfJXtc-=6+!Mn-82mK)6-)DSY;IzF+8 z&St@N0A?orlvFP0F}|yM15yp1fKKJJIWsMb+%$!P=ocD78^BCJoaaLCkfmUwXYC1) zY)Jhd)l5HcS#=Pq^^0jaicoAcdNIA&nO+B^6!2wrqhpw!pcLnpFUG}J1;@asWrpP- zTshUsp`yzXML>pm+pFqK2e2|ITGILJnfVDu{)XNGhQWzfb^R7gF}Z~lq{drHm|37P zjEKxbGC0Z0SN;^HBlOoLX2`f@DLOJDSg8e)TSH^FSJmmdl5H{3vE3=FyZ0LZ2wBtO z2ulxH)U<2C0O+lLC@NxVzC&JloZ$-7X(c!SeF99A^ddWYKM-n< zPHHwWKm|Bn+MD>2ug2)Jj@Hh*!CT5)jSDT5;=2R@oeTY3Qt5sGgrAB~q}>d_3Omo4 zcRK9`03L};bzpLC27r0GLm@j?8$-AO0QpZ3UDhvrZhA1Hc2o17IQYA3t4VPAoEO3jhEB M07*qoM6N<$f{#RaRsaA1 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_bsn.imageset/stp_bank_fpx_bsn@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9365facc6c0acbfdfbe1b5b423018c423f107a3 GIT binary patch literal 2235 zcmV;s2t@aZP)kOT!v*F~g*Mvs|TB=kJH!h`ufrB?`FcuC#kZn-{R}|`04lf zH?O$Z@bSy)>~6-)wBqG7uD9*^`QP*O@%s8TueZYz`fWiH((dn$(%0kfAEmE8vbqzXs(j1Pui)b_t+he3yUgqDO}M{o#mlbW;vA%|yX59ty~N@4 z^_11wddkqc~YQ)Mwvbrg$vZUMJ z8>Fq0)Y!M<=D+6X51*-7y~OJH_%5uqeaq00)7RSZ@^Qz_Q@X;p;^kq$$D-Tb)$j0E zyu+;D;%LLk5TB`n&C*1)yd|iyA*Qf)${xi&uhfW9;L6E*V~EE z)xqZJ9i^_G+1%dp^V9C{m)6=bt+sN<%_FCqubxm?e2)r)ET3#rrh9;($;s$&yCU6PPo5P?h=Lo00pB- zL_t(|UhUS?llC>mrfIG969qkt*x15}YWr^H!1>0P?qZL>^p%#|P9S^{I=?u)5JL(~S zU``6mP`FKh=b8JX<&OlP2RNZHt8N20;(gtNB`ro1Pc+9VY;qzozR=`3z$*W#6kz2< zh08%GS+^GwQ&kO#-l9@0r2`Zu3*jaucwn1aAxn+GexOQWvK;Q$W)D=G0B;SP)*Ilv zs_^&>Jj>gl^r{swM-eber+@_tk3Ys@tdbS*f>NOK>`=H%y_fUDCvJ;ZOD&d1CfQGL zJGNWZYr~fTvHP_7;R8#vVUP((D8Z=mN`Cc9bOy#0Ny;3-v?wcJEgd&^rEgfBnLFsO zzGds86uL$rrLMc&HbGY_l$Ovei?;!Iq|8nAg5HZ}_~Gu-h-G9~Qf&@0g7(qE(?u9o27`Y&S~a zf>%0+B~?=YACSLg^4Qt1p-7=wlm%RQPR2gqfOG>OUs6#85!)pt{CJ~oR6g;5SB^be zhh=@dj9I{9Z3KvwMo;6Gk7XQd1UQrv$`?vLuuF!21aw42nSk6`5c!a7jTl?y(1XB8 z?2wKaqRv-tQIP`a3!H-jb^Cd>W%}ZvnpY-~b&r6Ch=gjOk_3$#Q5c zmXEyZ_AWsKbJzNSl2<0lk~}K|5(mkV3D~}^pwo2Ql?JfzRoVW5an|W+wEV*cxGPRU zyZB%2e%XEnERkl0=A`@hZ8MZbBV0OQKHLzF?OsvD=89A>CV7z_9 z#RPm`k_HUUA_yXL<;0>U;Ci#9Ut|N>pG#61woXa!w*z?6P8T*}iHMdn@uP3b8+>q_ zr0GY1f(Ip4W1Ff+FJqW0nY*wi@h1c2RLm@ZuFs|OF9OSc_H&)Xb}5B`+7Duqt2g!c zDg?j8C~fb=R=4$ef*5o6WA%e(f;r#slye(B5IGa0$wvSf@<=Dk+fTqK7 zS^Qs%-o$oUAxQnxkUzf`r54DzduI|ojJa(Dq-hnF+J_XnC!tFE3q1@_Jv2!ZQt-@p zO1-ccBHrH?3B~CIybTnp5(>;j&Qz~#j=)w_M!@kQKoanh!l4l3Z_=|EiE(T4v2^@h zVX>kOnjV+*8O9K4ZWB-ut#B!N)l8GK7+WMIzgIZSuW&n#@uu7|45L|Yc^RWcZfU`o zC%0U~SRuE3h4C-Bwli{9CR_@DNZSSF9kY zRc?5iphCG}H$iX74Tlry_8+-o!Q;ghNwXu5zBEqXEPeX)`7h127HGG)A=m%_002ov JPDHLkV1l$K!f^lq literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/Contents.json new file mode 100644 index 00000000..aff081a9 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_cimb.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_cimb@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_cimb@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb.png new file mode 100644 index 0000000000000000000000000000000000000000..5c3471ca77a3d95981845e40914b432105cc5158 GIT binary patch literal 305 zcmV-10nYx3P)Px#>q$gGR7l5TsAS-vDPkD4U_@F_!_G%Ms$?YHf|Il6{wEroU%H%R3u-y|pIy8} zydCS(vPh1DUUl6cpFb06!LRS%C+eAy9LX!9;)(af>zlXgd4x$$MO29ld=`)#8L62h z=L51mTNR@g3?&NyZkx2`U>YI_00000NkvXXu0mjf D+t+`% literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..85b966c3eca07a0bf0b603ab83c42b1cedde20bb GIT binary patch literal 462 zcmV;<0WtoGP)00001b5ch_0Itp) z=>Px$h)G02RA}Dq*}X~wK@s)t$Yz1UqL}zAw^oT5j2=i8=I(w zy&zh|MlFI^nL;oYqD86%EOeegnV&QF3MbqLm@heJXC|9$C!_y*5&{SyfB*srAbGl2rWN4k9$LPRrd8ya!f(#cMTQN#}vbqKW zeD(YL%PTRSLJG@Z2T)iB>wN%QhV{)D8=0yyIPS!H%HRmlQwI05kdiWZ9p*HY!S^&O zUIt$PwhWE48216$GK9I%2$mt(jqJ8o7b!z9fGNYv^-ZtSLCjsP4hK{R$J+VAXK4{J zmxUr6(9C8*tA8raR|dcxXiR3H)xXV^cZMdgCEgoO-!ZE zebKiATutTXS4Pa^Ovx?!e9cM#0R#|0009ILK!Aez0U`X;ip#c$rT_o{07*qoM6N<$ Ef`)3oG5`Po literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_cimb.imageset/stp_bank_fpx_cimb@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..79eb5e52e6207848af9be103678932e76e0a6138 GIT binary patch literal 622 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1SE5RJ;(=AjKx9jP7LeL$-HD>VAAk(aSW-r z_4c;CzH_3;@sHjdUaH}@M07J<_ppe?F*ED8w7BdN5Zd@7*wAQ0m{L-r;f4^OAkJWE zq3$nb6C|WOr-~qXD++#fA2lF^MAdz>h6sCW6X>V3@i!^j2sROOag2m z0?KDrU})vpBIuoWEO_DhUG^FxFUr#Q>pl3j?keB%G!>>)>)&zQ--UXbie`Q-zEab? z?XqyZqr$-#FaJ1YEb&i!{@Q5W>7}9z{S7}GW{IAt>EXEMxGmXbbAXAA@}{X<7D)K| zd9!SiQa)9_@xDap>SIYRe>Q1pxOJa)y8o>AXouX1h@OaV%{oUrvJby0$vf!QBT+8$ zzO0xn=?I64hTGC-$wzjp=PJ0pnf>in@{>m)&ZRvu3@iI4E-p6QYgklXu)@U6&p*uc z*a^)E$0T;z&CWlfQK3_3x4*@XznWdBu_*Shyz*mF#^Sf?M;O;`b@w`)$!T@;NX9nb zxMr@8OIYT9zj7nYefhfa|kL0R5jB{O+fou+s z{Hhp+zf08@o7n0#ge-5D^{B3@XyjSaE_lMNN}@4mNjvumpQ?xkou%zSUsb(m>{;3_ z;o(=+r)-xSY8!p~%%{4{d{f&Lhp@}e%u}|{mSa-5{W236WK~hDyCfy5ywjZKAM;J} u;{RvP4~r*Y6fr@g3=~^D;3)H)Eq`Al#&6Dp^X0$<#Ng@b=d#Wzp$PyC=JnG6 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/Contents.json new file mode 100644 index 00000000..9b2f9334 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_hong_leong_bank.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_hong_leong_bank@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_hong_leong_bank@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/stp_bank_fpx_hong_leong_bank.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hong_leong_bank.imageset/stp_bank_fpx_hong_leong_bank.png new file mode 100644 index 0000000000000000000000000000000000000000..46e2202e41f464d82965682e3014cbf5e157dd7d GIT binary patch literal 923 zcmV;M17!S(P)Px&RY^oaR7l5T_)lZNFp@0Tb@7R3%Z^`v{?WvOL)Ts~h0I|Hnq9f{G?gv*^!?Yv z*I)kp{byFb5oiI>n!T5wQO$z0_ui=Htg@=w^!MNY>rX!LMlS@~!5KM!-N^^kv0&Pc zYd{l$Xu_5&K<>uV4a1GJJH3modU0`(WpKLKPvdHYo$b`g-PmA~fM`)@#z4W}OR#Vmx_q2bn* zWLGuOqw2@EpCnk&zU~50Gi%t~(|2A2xl0b+ff%QpwerEM&p?ser*2O7t=%N)cTT_& zfe1Ixpe^H<}y^fZ*`;7mUH6u=Z}< z`5PRYlQ&*SHZD9Y;s!VLg1Gy==$cp4R$rR8?a%K&B&VXf6=xx?j+=Dg$Ni@_?9$*y zFO~~QaINoKb`qFH$WNv~XNUFf1zKR_)OuXP6Jqokt?)444v57-B>ND@SonMo(MPeypZ#q;Id~O z3+}Gk{})HL#%;l$zqfq~A*OuTau_HuY3cE$@}Ur;fq~X>jD?rkJ|2}V< z4{-sRk^H=C(RpDPNZ`DlyAfi&-OAnn|NWz=!g=04A5`W@dH#Cz0u*#fO%T&> zdFB6p`HmWO@}IxYI~Kf}x*EuQHDevbbf9nkefvS>R>jZzPl5gbntt6n`QMLU)Ni%` x^TAEGoC^}}->==LS00001b5ch_0Itp) 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 FV1iEx{Br;R literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/Contents.json new file mode 100644 index 00000000..ba1d3ca0 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_hsbc.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_hsbc@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_hsbc@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc.png new file mode 100644 index 0000000000000000000000000000000000000000..36da803e01e85bb4627bbd6ec64d9d7cd068fd7d GIT binary patch literal 411 zcmV;M0c8G(P)Px$RY^oaR7l5Tc%Yz1Q^YW8!KekJ7T~quvAN5enwIxHlkuQ8Rn0(U8 z%g;mN|NZ#!9|!pR>GLz6aG)}vV$v*lsA>M=;*~$|KLFXUGK>EG{flWj(BkLei9iKF zWkAJ1Eks+OZ20ZKVW1HZ3xMctV<)=lzka<;%!bGUl>tS*?LQ3ENR$PiSFMMb0=EE& zKF(c?F#Ye}SGi?yDToC?0Mtm>g7=eV!VN;U;DMt4m#w=X;%};&khQ=q00N+Pd=|Ve zu0{?A6bpbz&E)&(bMHF((TqU000@AF;I!b0lkeY;AMsgm>g>CY33x03Y6lvEFNj`5 zr~do>o4gC8;dhqiONQ_G*%&bxo+_rBtD znH?tE=DxV;z`%lm)cF)@?PDr_$LV_NrPr0&)#|@7cX)O&Ia66G*Eagk$It7#H-3NV zz2qs6lj4GUhLzFVLql7&?6@1{EY&vc)Yq#$uInJs;^ffsm07{ChWEVeCo6_!6^RS} z{61fJclxyOmTe2<7#DuvJ*2c_JHzaSj;h;NTd!b9IL7XNCF$bMYE31M#(SrpS~0LS zIUmw)h}huor^4?W!kAH6TI&-kqWPl*;X<8XRIGiGct4ZO zm!+D~-=CJa?WtJ$>`)ei?x$B)Pj)bEVfiLLE$i|EJ_h68HMb7g8l}Ac6!16HltE%b zvxa)Zva&DL%{FFC2j+h+e$R7vvBx9l#ce`g86*T;P52mgPS5Y*z9`^OA9G(U`>uCT z(U-QFEs6|mOYSnIESop8OvspFZ^AV*zPs99ub#EEc1bd@@oZqu>GzG=>Qvm&Jnd?{ z-`f}e@9g}1;8qJO16$4phK}d@4|f_fCoGDa>ZB-Kmo%I08p8%gV;z5m?^SK=2ZE?w=;;1~6sJU@=`Hm?Ybfu?V&G>dyyI!nV;8U7?!K&o< zhuzmtzdWH~Lb&v-7cbXcIyI|YT8gE;WqY*OM(Y-n4@urDQ^U(o^!wKSdByQ<{T%bJ zs+@^`t;250?LYlJf|c>F-3_PZk4rZ^-IICgq7tJ313D_0{hvX2mBrrN&gTe~DWM4fDJNAW literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_hsbc.imageset/stp_bank_fpx_hsbc@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..abae989ded38bea076820da5cb4e44af6b8dfcd7 GIT binary patch literal 1205 zcmV;m1WNmfP)Px(Z%IT!RCwC$+U-kIQ5*;G{*<&7l29|lOkbpBDP$2v)|*s_NFxaC0eaRVmL%D8 z66TAHAPVY*FAOxS1PLnAeCP@1Hr<>~H+AdovA);I9B#Y2ciww<2EW63(VOo1o{zid zoO{k$rCps)RiLgS0s7 zIz9Kb5C5LIc~@B-pKtQiMKpxw&?uT_9cVNyhwC3ibUzU4?DJR30a@xB{H>rlzR~_c z1Yj6wZI+|E#~(caq+PSY(e+*lotdVGfu{XHC!rG;#E#&+Cl}glgPh?&G+X=r}(1YEw6cJ~#w2-&E+mUfEP0E;W ze=AbQZ8YigiiPtC0P(-Rfnfwz5hx`KgJqQQLKp}&HT&P zR{Ro1JP@j>qT1(n2e0wWC(cOfhIk_?Q~_#I8VDUh zcjO;~OyyPB09NG{yQ&-zx`2)-Hj7=+B>EtYE-1SWnr(Zb#x(DCD>2O@yz(n)>@oNB zCgMU7u@PR4PYrB4xc-c&@u@L4KVEuJ79a!`*e#L&YaXX`9;+=;K2{Mm=3~X!_k&D_ zjs%aa%x-reB%;P=M-fY2_BYI8SeMJWCv9tKlf30x-=Nx-E8@lbR3kal@$Q3qoAzlE zHti8C^nfT1$3Fb_>yKZo9s3|s%-gK;9~>I;OEBHAb8yJdefcG99X<2E4yTKSqi4iG z7#1%t1mBoa{PIHUvq(UQ!S4Ma>w^~cuD{%V6bgg@Y~L_T%CL8I#p?nUnnnQDui)W@ zat;hFUcqa;r}!1T)0g}}Xf)EvJ#&{h^|?j(W2$$Fv#JKoq0xl82Ao@@>NVia+vy@X z-Lxkl0wN#+qDnvnL_h>YKm literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/Contents.json new file mode 100644 index 00000000..aa870e59 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_kfh.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_kfh@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_kfh@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/stp_bank_fpx_kfh.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_kfh.imageset/stp_bank_fpx_kfh.png new file mode 100644 index 0000000000000000000000000000000000000000..bebc29a9bc093e9f0d9fe93cccc0dec769bc3d76 GIT binary patch literal 1088 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 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/Contents.json new file mode 100644 index 00000000..1c49aa5d --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_maybank2e.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_maybank2e@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_maybank2e@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/stp_bank_fpx_maybank2e.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2e.imageset/stp_bank_fpx_maybank2e.png new file mode 100644 index 0000000000000000000000000000000000000000..11be27be9276d745f0bca95cfaed12571f8d299b GIT binary patch literal 1376 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 Ef?KJaJpcdz literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/Contents.json new file mode 100644 index 00000000..17d19707 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_maybank2u.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_maybank2u@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_maybank2u@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/stp_bank_fpx_maybank2u.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_maybank2u.imageset/stp_bank_fpx_maybank2u.png new file mode 100644 index 0000000000000000000000000000000000000000..11be27be9276d745f0bca95cfaed12571f8d299b GIT binary patch literal 1376 zcmV-m1)utfP)Px)8c9S!R7l6|mU~cCWf;cY8@tP8mt}WZSlDHznhcW^O(n4;NQ1l~$k3>O7Z7y} zM7(4OYA9(GiKe20f|8h+3Yd691w=(q>0VRW{X%6{niXB_cR0EMv!D*sUp4d1neTk( zobP$x_j#W8J3X#7_4rpukN*I`g;y0DGqjV!O`(Ba{d!sZ-)jvU;x%QgDSwIfqe?}$ z0A4Ru#*Fe9>~C2#O@BDg@x3yKOEt2K)v`BVQ65JG$ zwl2=nFmL+^q@e@6H)Uv^Zu+ zA~jk)Yxu$6($I|u|!4q1b(c* z@yaP_OKZHSfZZQ^$ct-n*7dJ8WN69EKEMYR4gq~EEjKj7?r~#G)P!>Py2}Uc2M_Wl z(s9E*Npay3x#4kRLd>Qb3N2LM_smr+5r%6br&%C5UDlC3>P(fL+8*>U(vD)dIV?xN=SGZhmr6*~;yB&bxfnMyz1g@@}6kLdyOrg+ z&eVXUg9dur0icnwJFz^QqDxQI!y(B=J?cqi@HPY@Q;Kjr-!UY>Yu1e-|d`jBXJn7tBV^zzl$xjkE=qFb;LO@>`q!32IieX0x8Vgyv?^Fwkx409xb z`*?HEeS%4MX1k=s+9MfJr^Xp=w?v9p!C(%bJc0l^^Ho8+61>fN&1N;-iEGYW9kh!U zn;Gp6h8=)U4=C>MZ56d|8z1gd>|5hRazsisARKyVrTB}ds4Fo_2sU=K2LPa=B|$7l z<@Q@e!Z;IKv|SOL+1pBG5DZeuPSvr9au>M52UpPt9fo-1(NO6b)Lm5bZU44F z7g-}a656rSg?KSG13A*BrbU^sH~yfxcI(IMVvVdU*Cj2%5FgPVor!Or6(FH i);No9`2XLuo8xcUTo{V*e1{YO0000Yi- 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 Ef?KJaJpcdz literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/Contents.json new file mode 100644 index 00000000..46838072 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_ocbc.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_ocbc@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_ocbc@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/stp_bank_fpx_ocbc.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_ocbc.imageset/stp_bank_fpx_ocbc.png new file mode 100644 index 0000000000000000000000000000000000000000..39c40013ec5046825d7fe8090de7eddc057e0ae5 GIT binary patch literal 880 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 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/stp_bank_fpx_public_bank@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_public_bank.imageset/stp_bank_fpx_public_bank@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..844f65bee24ad5fcae740be75d092012de855890 GIT binary patch literal 927 zcmV;Q17Q4#P)00001b5ch_0Itp) z=>Px&SxH1eRA}Dqn)y!?K@`XP_b5<6qA8;OYzT;GAjoNmN6>ggO3UG2povH1G-6bY zCQ39!LzGkv;tv|6TOd7w)&%JRy|*p2+v01Qph7#{*>-o^WH$3tr*HTBY2SPE=FLjf z`wH@UNC*G{AOHk_01yBIVD6O17-pN@#n1s`Q*#ahV5$Fyp(Q5(%% zK&WQ|0o%w!^cRfHt)c;-)klLtQ)?R=$P5`Iyr>U_OlRBK|53)4HbDU_UCLNAno|Ad zrm6iLuCsAwzy`1=km|R#md;<`7BC5y;};;E`tb6_j?BY`F93_O8=*cH4lj3HVii1y zp8%f$0|2dl!5%(y~+x!U4duVL^b;=gTXCMk7`s~K)8=TOYi8qNG6nYy6J_x*=xE@T@X321c)m0^biF&ofoJt~#>St!-DF+NU}9Pmz0 zV_D@2fb-2;_5MPzqNF9B&tLKkz&SCIP@fo-w0N(phyW0AI@u_2y?YO#x!b&`*!|%n zEAz=oz5ukxD>qkH(?5Q%`ot)d!YtZ6u($|I3SR*8vU13MXa$g$5O=T+4WrH7Q&SK# zd;x?^OKg0=`k+6lIqZ*F<*rw+#Sh}Od-r$Q+D9MfSsML)=k8ziPoC!AIuK{LbsMap z$jjY@j8?0mKZPoI=G2H;D5AVyXG8;_DT*pCH8eCbJ|Bl-4hmQ(W8n_|pL5~>?DS~$ zr|TkaHp%5Ho(ip}v8` zmNKb&ak3D^fWEFiE9yl{qYPx(iAh93RCwC$+*wZ(Q4|2+{vJg@#24eUVw4b(_^MB!AaOxaLE{n;QQTM*6i`7E z;t~x}afwQBsUp~7!WOBOr7g9EZUNFxTfBtQR71<0nLE>a>zUlgY18>S%Q@$sI~v`J zBpfk@1_=-dfe;9R5D0+~2!U`Q5CS0(0wE9rMH$1nc>%9?PTn?gvmr5ohI4nx*9jRl zG$tAlMIH5x6bBm4+f7#|WYpM{2tZJsU?3oVXKT|fzJZ`R)?2p?yYd5GA5%t30YMM- zcXNxh4=Qb>VM6EbJvkp&)QCQiVK?mXY^|;GiOJ>32M}22{sUU4q4oA1)`1N91)xK+ zKYB#>QdPo86EB^~$4_)CS5d#6S?@Hn2xKVO1Jh}16Qjd|kw$%;si)6$Ny+L24euXL+YR12^4tJlOI2%z)&wHzG@MmzyI-@Mf&r>Y#L za|7h;=+LL6s-Md60p#j@uTNPWmvoNT+1E>V7cUp$>|aMqXc;9A&_7wouj0c&zaN2OWg*6*gF!(+pgHHes9DI*efwn^ zF#!||1|=VWtFtq97V@)jFQ}ZtBG5=lskHxjJ_`|%qe07+e^qr^DY=qZc6Ju@>)oF| zg>y7X1B6(z%&vfuiZs|m+ArbIb0+<7%pdOTV_$TU!7+aY;Q(bb9Uyz1MS z5p70*^-FBx#?A0<fYxg$=8o2Qs%xqL!m zhP<2(Pmg(XRkiv$oW((=%U7uBY!IcxU!WK{r>=qymyz^%#d+0qSOFr>a#%ibwp_Xn zq;lrz`6j;8SlN;**yv1|(&vt+yQepOEklju6ZJPhMx&=rbD=|XDFVodWKSY%#&Df( ziaL}Rvp|hVp2@fl{pC<3w*VLg0;HFNn$Fj+y=&G|583H2r~?}nA7NHUsU?|Y&I>e$ zWjuIDmQGifLOKdA>a|?E4l_DX#GsC{%fewsf@=&1B^6Y558?Pqm#)O$USnVylFI;y^4$y{ASLq|)ch=H%@LLdY} tAOu1n1j2zp2!ucggg^*{KnV2Tjz8xT+`BC^GH(C?002ovPDHLkV1n(TI)MNH literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/Contents.json new file mode 100644 index 00000000..1e732798 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_rhb.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_rhb@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_rhb@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb.png new file mode 100644 index 0000000000000000000000000000000000000000..7a560ebfb2159f0438c7b86e6d089b07ead0bdce GIT binary patch literal 681 zcmV;a0#^NrP)Px%V@X6oR7l5TU`XFcTQq9Hs0E`I3^xl@YxlHmyE)_FeYXk6fL!j}EiGGb8nqk% zvYq>m)^5BGQKnF}t99GWxkny`&OD8*T_AN8Q~Cy+7I;oN{`2?WGY{VX{rm6#|Nmy~ z2TRsm0W#(teaM-!`R$jVK(=)GPMfa7-+unN_UzM#Z@+*dK2uIWw2LJ#Kj|7W)h`F# z4hY4VzVZI+ukXM9V#(Ze@AVg;&g4bs*Pnj|WK7(97wC!qV8EZh_1wb`Ko^J>ZwGo5 zC{nieD%|w!e*TYGG2fF!JD@Q z$awSlCs51b+i!qulhy;srem=KLb>!G1*+b2{grVm$g#Vwz69Fz_2+LOJ95sM=B+n@ zjN^CT!o70#;d>yt_{1aL)YWLFLr|Z80g45gOD{rn{{Hh97znJHo21HiKtvwD`vwej zAY;ji$KhbFY`OG8t^(xan=d{C^)?2UqM81}!PPNlGKvMgyYE1P2uK2h*sx{4ZTDdy z`_JEhK+6u^0)bGiRVA`RbqwbkpG$IQAX^raTB+ zr*R)JhP4~^0kZ-`M5u6^aM3m(0~l`Z6OREi9WaJQ3%gMZMlBe%fO-f35Q;h$laL(m P00000NkvXXu0mjfddo8XP)00001b5ch_0Itp) z=>Px(%}GQ-RA}DqmuYNNRTRfd1jSv9Chi|xJ}97m!1Y5vSmP2EMT{|MfM855lxopJ z6;Y5x+Ni{0S&ATFfEuEOu8b{hnJI0j3oTt~r|Wc`(ssH`+i5!s{$KC(aJ_Hly@`p5 zntO9Uy!U_i-FJTPo^$TGAtB+Z|LraTB!C2v01`j~NB{{S0VIF~kN^@u0!V=WCBU5U z)Ca#f6SlMP-q=6XPjB0B`o)BzTRzU@#oIqo#G60L^gG)_Th6@lWAVbRd2=Ju{#k$< zqcY+Q7KhVydA7A&8VmilfCfCZ!#FrL1-+@yacyKe6Q7MY4UJDjY_vJ9S(i?OasAoJ z`r(Obnhn>h4pnGa7WUq*`5&bGO@N2L%IzI?URE`a$NSj!d^V=8dzAE76U|Jt*c>Ec z9hi7WjG>X!`S$L?a=!+wcqh&t8kX|%YJCQ^bZ3mgIylZgkb0r_{a-4_Cavb4}d#p0ezAe3hB@i+K4Iu&1P}@gJS^E%sH^1S^LKQYE ztGAf@*%ekFKvs>7Qo@rvjKNs1#UR}CZoq8i*91$!90RG7#9zz0)C+Z_s6+T2Xy4~F z?=!a1ZOm`A!CN1YK>*<0Al_hsvftEB0ryOt2?|`<<@V69BvTtUJES0fp|+%6R7XX} z;Kby#n_Xjudp0SXT1&14y9=>5x9 z#hlw{86X9lBa{WvSwcn*o}p50y#R1sWCpnb(g=c#K8C%FtvGtbu`1qH6h9C*_)Dxk zv)lQ^lzWaU%ayj-`LAm;__tmF;FCsjX%~6F5Uk&2H>r1`-KxZT{rU`2YP(0&S#HnS zR>7J;3<0ed0G917q1m@*HsaUv}!c;=WG_ruImeA6VISy zy#T=b49hvFEcoa|DF#=DLz5DI^a6Vq#Wsm+jwp39>gPZ?3cK=6{tgTRgTD0w0Bxn2 z)ix4eN+@QLjZztIeb+_q=`3u+g#b#p+dSr4nvKEa230_=#yowGtSR8D<)W1x- zhyojD19>Kv01`j~NB{{S0VIF~kN^@u0!RP}AOR$R1o#*9KLP$??h#&3x48fS002ov JPDHLkV1ny~X)OQ% literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_rhb.imageset/stp_bank_fpx_rhb@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..988620427f447f30de2a6577c9eaa2c7596679a4 GIT binary patch literal 1876 zcmY+Fc{Ce{7RH02QClpvM?*T6+KQl)QbcUEuSFwhQlwG_(N0yWwrpwU%v-V4pu_PWsd^@03jP|qzh;J z{b(RJXI|T%3FR!V2p1~^fYv9!1OOZpwn3V?{WiW@a1(#RLY~=nAM!>Iy=jLme*;yf zIO_z#K;j@IFHBUVVv?JC0SL^BgWOY6lK_F9i-Oo@A{T5gr7CO$**c~U#=AsqmC-9z zZbzb3wilaTgicp0=N&3F#0e$xLd77 zRwDOkvS>^W2+ENH72Kk~Tw*v^*WlPKD^&*AMHYUy#KJkyZI`S@x^j_bbHb62xX%(T z92%Y&9+@0`sP+d6d$D!O_8_e_aP0HZPaw(owCJfieS@avEn>6$RAjvCX|UevGj^WR$E?lGRWSuq}yvK-lp|4s_*3I6a4y|GcYPhYLo zJGG$IB#rg!kacd#=pen%0o8H_22F?;)vaH7X(O8En5I>;pXneq+W89e=?RQ~u^cmjIjFU!U{uz-v(|mYh?eCWZ?&mS z$-;Xr))O4+?Jn{#?gV_a=- z5ybESyZUp9`Q3}~bRU`!tRC64w@M+PlB0yS;z)_thlx->Uu&FM0#}(rl9*x})mhDG zE<;{UKBk1Vfwpo6#L+1kTEhw?&mPm=v~rajlL(1i?O71{pmDrIA3CBC3R|GDQ&i8k^h%PZM?c1$g( zUM%CP%zC?{>rWA{QE$ShW)V*=FCd5kBW6!%CN8SOU*ZYJq;fAQK2895M;!jHrua;R zfafHw%D(9rUXjiaS9f)t-eEVWl9i77-Q*McFo?u)!>(7%9!Su~^~db7unE1AeXRh8 zAev+x@7+)I($ql9fIC4u$!rhnX?#!rDHMrE+ietgdQg?>Fc$WZTeeyGLwlQyfqYBq zkr-Gc`5j>3UGU6uYcAKtN<}9qNehF(RTX>8M@O19vnNs}Ps*1j#+!%O5fM>OUDvt? zcs?~`rn0$s4p+I`aTIY2m|$s~5gOe#?HfwyAy;N?I~H24Z=Jps-I@x zUNfW>6AZq{uy9`$UfhWSoph9I%#&^Rst_HqUwm5pc1=BUqqd|Z;!QbC;2*!>fLW*vAL;=kcyabq^P?9&K?mv zYZf0CJKo#X0ohJ>v|KG_LF#)ii}`;>6{%J{4(x<5ae<` zd9=AK5p2a;Ed|4(g5lY}o>}nJ7{iBB`~vhs;>e#Ar}y_USOd7RbzZfM(W%t@A9@JrySS;X%JdYj^`$lF~Fcp-HA0B z)okF^^Pc(mvw{!Q#ca!>!$OJ}W*w&UIiATHX;t6_;jiWCJ1&PvGTQ37Sf|H0>6_wr z%W>XqW^3=ZiT0TvOnIjFH(CBhM_Azh!G4(42gP6@XJV77j=n H!Y}SG*Ee-N literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/Contents.json new file mode 100644 index 00000000..4ecaa2a8 --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_bank_fpx_standard_chartered.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_bank_fpx_standard_chartered@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_bank_fpx_standard_chartered@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered.png new file mode 100644 index 0000000000000000000000000000000000000000..db55641e80a6d3fbb6762a646c8726087a7571d7 GIT binary patch literal 890 zcmV-=1BLvFP)Px&G)Y83R7l6wmg`HCQ5?tZP4o{``O$39;UPKqDgaUgZXc4lO zmu`wtB%1ibrJK6V={9%uGMmQcrCoiVZR+OSo@cJPi*rtQw?l{Km}~T`1$Fqo80_%< zJm+^gzmv21msife1&D2K%1y3Lxj7VxWgmcf)|(`APkPlrjA?5&0fBIoqr#uEG|(N0 z%$bR03!t)k>Ue0}8me{x$-)VoYr(cLQ<60lS8e-K0AlxEYnP>T@0wVQCIrannzOCq zgaLUC;#@N48C#oZ{gRKOHg9~7YuzcF<#j6CyVM=vHvxbW^Xw?61D$y{{6?gM?!5{O zWE*Y>MuL-8IaNgg?oiX%1OY(57KZ?+^G%XhVx2D#iN}Pg?T%YReKAW3046Q2L;%zt z5}PbS@D;BDpQ3AMNdS|3o>&e}HV$rQ0PU*mz1K6PS#lYcb!?Oc-DQ?>!b!Gx{0Hm! zDH%`&tx~bUj(II+&q9WMun~?_i;>+Vb{foydQqPi%MhSgM!(`y%WLjdnepgrVH(VB z+GHdeBL?7GrDbE)9U#Oiio`T*_bu^tOBY){|B6vB(GvhTHe5%TMW@-ZRI9)VrK<236M+X5&+PQyaV)npaQ}DZiLaPq)=6#W}RE#sI+*hxZ3?SyhKJQ z68&93i=GYvP?rRE+h(XMK_C2g(dBo=v;4T4x3a**}AR3G23rn{%fDSP(myVO=4h^{VTBSw+sNo_PnnzQ{ zCtVeRJQs-{rer`}87kviMlp|n+U$U#L0)UjWzC&jAZiL|mchK87L&@vE8V3}L9}u}JRs+wu$kr$p=#Yd_ z9ACb$zggFPid_N#phbu!*%t^8qIU3t18tT5q38|y9#1vSuUh_x$8QGfI}$Y~`Q7a) Q2LJ#707*qoM6N<$f>BzMPXGV_ literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..134a74391b8c7f4233f0291370e5316deb8b831a GIT binary patch literal 1807 zcmV+q2k`ibP)00001b5ch_0Itp) z=>Px*$w@>(RA}DqntM!BcNE9nE7_7|{d{hOhk}mD$|(h6bX-lErn8`ypcEZD1DdG+R_KmLLbm~>21r-`ZIUca_jA_ zw?IPk{ezI+%lUB6>vzswF}K7&E0zJ40Tw^Nl+(u=G-%AOr3ZioLk%+1iAu>)w&-}J zSTZ)VlmHY)M{fNvn2zfdS32$XF9CoVkH6@SHqvxlT;cel1_*}cI+W)0D#^UogU<5g zix)s^abIj6Au=7;Y#_Mk0Nk^Ia=sz0Li|wcK>>J7iwdBB)OxyFM#gkdZoOg=xsY+j zd!_9zNz;)at1cD*p3HcRoX$7U*lMWx@Ju#2l?@kiH zcZ~kWer0CV%E((^*sFE=FoMh*l$bUD~bgLNTs48yKja@fKX`e#Zz1a@LXC&PMtiq^~^!N%NLkG^ZH%wqqlL>aW$&y zXawlen345JFQixH+~4isy3jN-YH<6)WeVyV%7U*yb%WRsFIaS0jgLAq%zT>w8-48;aOdh#%9M?p`o)`G4Yvl}>a zHc9Il!ot~rk1l{_;~&1?V&M(3?hI%BYTu0rc9L+6(PU zN4St#J(g}=C;Xe|wI1Rd3;O9TWK5yq@k6Y589*zKRUYCBb+-Td1uy0i8i!btvPkRe zm9@TjoQHZVk=-&(0RRt=orS%9IxE~K5DKU)%IaYbQ^HVMD7M)YyV1->`O`<(lrcab zUTS5F$541cnp%K%S~=zX^zZgW7(lDCYX9m~C3472^hDOFF>kxs{lsBbA{eh`))WnD zlMR4{0?-#gV*@{{EiNb#&!$x*g0ba#C!92r1R%63as^k51O<>uL`H!Z^TIEuJ^pj8 zikJVWrp)j`UW?T|OFKZNw)xHSEyc1j06=1hih6Wi(_R}ICzkMa>&`b(W7v4LgI<8< z@!nO;ji53)3kSf}bhiN-2lUN$_0;G2T@;E2WGA`+9DdijsxJ{hF8@k201_OsMl6az z5JSmoAB+FY?{VZ@NN#iiTvM=+^`N)eTg&BaA_h>6g6j|5>kG^-Uq*xM`SdC>06-fI zg=hiT-f{p3Kn$0K`K6-b2TX^MfKCAl?E{y4vB0G^RSJpfp|!amJpk0px7b@F0gyi7 zrRGbDZ;2VW|JZ$Y!b7Rb9G=e&rJobfI5Qcq;(MEyMA z#N64Uf1oW*1mm5`ZIBqo-BSy8A~Sg`x#An|a&{zw@kaS)=fpQW@sA`D1(?Nx$Z_Fs z7pk})^HLMR*wnCZ!edSn0C;WpU2_Ik3grnFtR}k`HA{`-Jv(n7rp&ORZZ~))?Eq@C zD>{+i=+casoahS@o9dT4Ij@&*roaFdgGK;&34+n5i?Nb{y~e<(6$_^oqsN>hy1th4 zMZEDa>9x`V06r7rC5T9!sgXg~fR;;c%v+%1e8k&Do}o-rM+-n^jV!LcDwHR98u(g? ztG~UeVJ{hg^r6eN0NfhTleZq?yH{KP5J=>apjt#KCH;Wbkc!7!hR@m;vVl4|z zLtnKWAp%Gnyhv9NL&l74dUoBgY{E|RZhVV;Pv^;KfP7^oT>$W=4AH>~-kYcqPf!@s z%BR&o_GCr^7+fZL>%gGamd*Qr#SOI#UP?r@jPj@7EJ+E6`1f5INopm`PT#Q6VX%3q xwZZc=CP9C$IA85BB(<4a@Xs>9GC+c#e*r)8ji|msX)XW&002ovPDHLkV1jptSgrs7 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_standard_chartered.imageset/stp_bank_fpx_standard_chartered@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8d75605ad116a298f03adb01ff1f432a82ecaf4 GIT binary patch literal 2596 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 VN~XOd=8gaW002ovPDHLkV1o09kyZcz literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ed32e64498ffdcb39582acdd94736419bf17aa6c GIT binary patch literal 560 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGH$#^NA%Cx&(BWL^T-@$DNus#!8YBj_<#VHEd>|VI@T>P%k7;cwzY%j)V8a#ENh$AIsIBXWkHX|qD@y? z1zOX*W~61BKDID^enb4?pAOf}8+7hHT=Pu!`5ETViahzgYqq`gUoQo;5D4n{3hZA^ z(qFe@)u!j-FF#mZytmf%qucBkHxv2$=M<~f&7K=?^3TB7QaRquB3Wm)@b=o0jbf^| zc758%Jgs`sZLNvfyEZj3y!^2z!@_7?q^=ilpXgclHS1ixr)HdySlWK9*DN{2f;&_o z-~+pF!%ItDFY7r=8-9h!7ECr^l<|`_uw{q115O^;&!kfQwyD}V;^w96tJ-bTd}p^F zD*06Fsop5mu;Ja+Ahl<`g0X8{ON5hMqW8=-fA{*H$2Z;xwhPzWldj6NY~fkZFLRao zL2FYfLvkofK*%R14nOx^&VmUeT}}iwqn^_U``p%rE$E+W(F$iz}BG&R*`m iET94u@eB+PZod|lI-;MhF1jZU#P@Xdb6Mw<&;$T%lIh_9 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_bank_fpx_uob.imageset/stp_bank_fpx_uob@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1adae8c70c11633aceb676ec9766a174d109a278 GIT binary patch literal 757 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1SE5RJ;(=AjKx9jP7LeL$-HD>U|R3#;uunK z>+PN0*};ht$3L#q;<_iU9}&Hk&rej}b$8WTg*?4SD*qVY>G4@b6khCWTH748=fbh1 zqpk<_WVPbhvWsn(cot_1O`bE`f7J@x_tjP>G?%o$KW+WbqVlt)z@i+}?IBZ%0@$h0D~(MWv#6*aZDKR3PJ)t5QzV@l=&$DG%7rZcT-7Fk*4r52w% zw5j;#s~gdeZ%CTXym2>Ht|m#1_xL82%(G8dPc%PvF*wU;TW0>pa}AcZk4>bC7lyfgbyD2SqYr-_*Z=jT&Cfq5&V&C-^||+F7@3#$&AGB|?V3BOZjWz; zE&k6kX_~ruiTACIOw5zoHhf_8JROzb(3jiJ?{VuF6LZnw9Z%M=fA3gotXOko)6N48 zTIZD&|4fpT<>&Fx();Pfef9jGFFjZEPDg62yZbd=`uO=@VqO1_r09ogXr; zJ$q#?m)NF0yOJ%I_a$5UgwWjfs8pYXUw@X~`pvmJB5U56O;YpsiT!?m-0c13GLdli zIP>EydS7g#Hs3te9sbL(QI`Gb;*Z%r3C1(eh0JsPlFVfOZlYmhZ~p!IeomdU|0}*H remj)P=4;d`3<-4+NccxB{H=aFY&~o6_6OR)bj;xC>gTe~DWM4f>M%(( literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/Contents.json b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/Contents.json new file mode 100644 index 00000000..8d8210fc --- /dev/null +++ b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stp_fpx_big_logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_fpx_big_logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stp_fpx_big_logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..78e2899583b39bd7d4ec33ddbae0948a2178f2f2 GIT binary patch literal 3536 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 J38`Qm@?S&Z>5%{c literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/FPX/stp_fpx_big_logo.imageset/stp_fpx_big_logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e157d8274f4d1b29351d60909092909492c48aa2 GIT binary patch literal 12473 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 KMNUMnLSTXAz|EF*i2kuTN8~d?+HnSydthg zX@?_4(F}z+mA$YU(Vt#j+%e&_tY-*dj-@86%BX9zYhIg}g#0D$37Svh`lukQ~5 zeN)QU(ZFv8q&eE)01x>)rT_pi5N~Bc3KP!Wo^~ZUt29eYt1}A4diSx1-Z&mGd=%z$ ztFVamfWgGITkdX#oEei~uq{>2;Cz-LF{zI@N!7=v*!m1B_z!}U3}MM*60Ek!$o;gybk53%{ZfCEpP>u z23N5KoR{Zae`mrW(nu+Y`3h&w-={$>82ARpb%mW=(%^qedf(5lt$ZU9fX*1P& zZfxgi05j8lZLv$rGezX8i@@yZW~_ds)y#oot&y82IF~$!L)e*S*G`tdH8B52gH~ zf|~4bA0I1tZfl63g}YA@^Vqn%i9Bg&rilP2T?$p>^0_7UW+xUxr6N9WOn~$%{Kq`A za@U)U)P>TG*FA_i84h;7>GqWYeCt=s3Ai&wR4wAvF5jX+Dtx+`Gfo4? zi%AxwXs?-0-=&Mxz7IfF*7CTi_RYgzZNU_@nkzOrzg}Bh9yp_^u7&^zR_{|i9nFoe zf#lW8{57&o`U2~`fM?5mJ)HVmq$VA!+;TG7ln3M&fvifHX?-oPtl_Ws3AEk3zTtdS zAFg(}A_?|b{KaBc>%!N($rKFcjzl*_{I$*}7-JkKiX0c^J3~b*TdWyK(jQlvAdkQE z5xG5C&#j=r^}gmcy-n1IaA`TkmDy}{b9WL}QRT5XU2jB|3hNLLPQcgz27JAzRVEFS z%%4!OFBS1C-3+8<8j4-(PBFZwy_Sga!2<%H-r58+N0OrfRHMCBU((^mIMkwl7h1{> z>J!}?nj4)_YO8rZy22iNm9jfH(H0xj4oS3)x9_yW^HPJ?JS>RzO_$#V?L_U2yjm%B z2ljFT9WjHSkT{fOUX9W=jjES{)O;_7Gqq@vBb@TH zx9PGL8tZftwQ5RksaXk$Z-;Wz+jV0N>3r{aVEYTBoTyaqPn^=pC^V&d!^Ey?EiaHd z=+1CCe&Y3DW3@tA5?U5)v!RMLj@B<4kxs1LCu_>p5dy~%V8H-;;aTf3O1D1nAcOzp z^|3hKLthW)#p0}fbPMPY;c)TeXPnhblesgWXUSp)c&XKf*67^tRAPwm+aI|=f0Hiw zlt;b7127MCo=_ku9&KxZ@q-8R65Gtz`9YXiEg7xgd>KDD=P8(?1UYx3c|Xdk_ZkuG zGJhdQ95Ib(r1H8ABT6195ufYu)>F2i<+GbVZg|4l1}mT%!-G2co#^NA%Cx&(BWL^R}%APKcAr_~T z6ArMi`uD#*dDFoaip;qjo1BV1Dj21hbhYnPWN7&Qbmk#8hftt$22WQ%mvv4FO#o3W BA5H)O literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c695a3d2106f1efe7f47e78472502f6907ede44 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^N+8U^1|+TAxeoy;#^NA%Cx&(BWL^R}ww^AIArXh) zUfIaWV8Fw?;m+OXli6=AZB|rh4qtpSbid`@>&KXxKwuN!Cn+CsmK6RY{S9D{qR3n~ XXIfu)_1CpPgBUzr{an^LB{Ts5<(w$) literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_add.imageset/stp_icon_add@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8a108a76ef305623a757f1e27fe6d30627776ccf GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^#vshW1|+Q(8fXD2#^NA%Cx&(BWL^R}!JaOTAsP4H zUfamYV8Fw?;m+OXmxXVs3N-V~>op7aeNtjCEi8coX4KA@|8UBo10c|obVm|JQeuX| a8P=5nLKiRHoMiPx#{YgYYR5;7+lQ9m0KoCXOMmz0{m9e$(4tAcw&O3MwWr5udQrTG1lGtiOX~O|b zyaBPYBEk7Y$clsrp)zjrL*@_jm}N4IRUF$s_DoZ)ITu$v5wp4{Ns6qS0L(QPDK|TQWWxduL7c`h9Uhb$fu-_x*Xy-&XB&eBgOSVd6zNj!C_>CO&}!- z0Xw$=qwY@W2r;8&U6C1Rg^!oFEiCp1KJpqYfCmmbidtHyh_$rjsh%nQ Vt?Qpia(w^*002ovPDHLkV1oWTi9-MY literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/stp_icon_bank@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_bank.imageset/stp_icon_bank@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6a02de3cae4e5260d8e18da2d093e9076bf27b35 GIT binary patch literal 492 zcmVPx$rb$FWR9M69mp@CxKp4hv5d^_SCw0`pE;@^#A3<>OBXn{08#v~Y+@)!7hy<5H zzkr)d=MLQz)S_EKIu#LGeV??#k$={bG#T#Tfsj1+zQ5ePx!f_9L$mGX0q5c#!ZjIb73IIads(4J3pWpVB4~qd;h7UO)6TO z^i>?xRLFr`$e9Y{`~IqF$}4)4VbMSdqc=ouFhuG|UQl88mk$&jLD2_G2t_yQWdkLs z&S;K5ss;uofW1_3#GVPParVcrW!V~mDXcRo+NU5bGCV+Z-OV`neg68UlJ6PbH`P1qoAwf2C4UP{1SWdbJHCk?rejjU ibLE6p@Jt!1vS2^#Gq4kW!XQ)t0000Px%I7vi7RA}Dq+P_NzVHgMSi=c)gXb76ZF=}lRiN=PStF5U$`UhIt#MARV&4vbv z6v0Udw;F{Or(7%t2SIQM92&yw`D%tg?(EDT=e<2IJcOsud+(=v-}Cj}S5c-^H0@zA z7}PIRHC)7ByjEl>hGER9s(zxXq7(>(jdtKQj^lk-1L>;m5$3rQ*Bf{rpK~2f)2_Dy z0g)Mm>kpAZ-pu24U0PMd zm71sj{uy<(%9wDyUATh!aUMs2!Js&x2E0rL*Vm2dJo@3(aCFigI_cvya3=AD^H1>W zAnKuUa!k0h zCLO5C1nhuT;Zug6NV_dW20w@1Wk$jWWLa`#8FSDpnUU~)f9Z>aq~kHsHA$cY+o6y+ zrspoFZVx!%QMhp7;=V$+-(8yQ?%N2j)r8+>zRx1xYbPJ=9N}p|_fziyk+*nMQm~m{ zu*lEb{N+(%BqJHgNJcV}k&I*{BN@p^MlzC-jASGu;Fj$le0(VtN#BtW)gktez}DGg^OI$BOlfkKeR(0>nFT2P_?tY_OG9T f-RgTe~DWM4fBRF#N literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..875e0a1fe05a23c18b778ffdab1394c993405a3f GIT binary patch literal 491 zcmVPx$rAb6VR7l6QmAxxPaTv$HUJ|Zk5M?1*Ew>nKCbB4rlEEM^lN8ePoTD?4q6|v@ z0Gq*JVKJC3H^Op9!p%Z?>3aDZE_LJle&^zMo_e;^@AJHUzXymrJoZC)3grcS4#y3f zg76(ee&%JIu<1NdE|tgez?2g|U^!rK9I!zs-$H;}?qR|%2(uxbT_9kM>(1Jtdv9qwS;EP-;@urorr?D|MHEU**; zmYH@?3FR0(j$?`S1;S>GFyS+Nw%W9_o>$o&JoZFbR-ZxGUpX69?bWw&yujyB=(j%G zAbhlp@(=-wquk*(^ujz&ek-gI!fQ)s_XyZjh^wzyDxUk?b2Wq~78oHMDO9*s5MCM| z^0)})o)xCt`Q3^89Ehmi2D?JQ+P~;(|4+cEtG0}`*?NKD-EI8WJ?4J_JjWU<{_Fgu zP^JoiWg$$u@yMLGUn532=hm3*+B^VR%h$VzcD4+6R@9@}-SYv^m hP^O@~fpFBtlOMKIqR6^iJ|6%8002ovPDHLkV1fnf;h6vc literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_icon_checkmark.imageset/stp_icon_checkmark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..25831dc8898afc78df5cffbd4e85878a00d5f09d GIT binary patch literal 701 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 ZQAA+WM#9T)LkIux(0Ul%N*#yr{{Sjo?o|K) literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@2x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dead3f4dd623f4decfdc5dd3f26967039ceac0 GIT binary patch literal 6000 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 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@3x.png b/Stripe/StripeiOS/Resources/StripeiOS.xcassets/stp_shipping_form.imageset/stp_shipping_form@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..34ede1407a163baa68bf3b8ae06aac1d610b4c5e GIT binary patch literal 9692 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 literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOS/Source/Enums+CustomStringConvertible.swift b/Stripe/StripeiOS/Source/Enums+CustomStringConvertible.swift new file mode 100644 index 00000000..a7f24c21 --- /dev/null +++ b/Stripe/StripeiOS/Source/Enums+CustomStringConvertible.swift @@ -0,0 +1,63 @@ +// +// Enums+CustomStringConvertible.swift +// Stripe +// +// Autogenerated by generate_objc_enum_string_values.rb +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import SwiftUI + +/// :nodoc: +extension STPBankSelectionMethod: CustomStringConvertible { + public var description: String { + switch self { + case .FPX: + return "FPX" + case .unknown: + return "unknown" + } + } +} + +/// :nodoc: +extension STPBillingAddressFields: CustomStringConvertible { + public var description: String { + switch self { + case .full: + return "full" + case .name: + return "name" + case .none: + return "none" + case .postalCode: + return "postalCode" + case .zip: + return "zip" + } + } +} + +/// :nodoc: +extension STPShippingStatus: CustomStringConvertible { + public var description: String { + switch self { + case .invalid: + return "invalid" + case .valid: + return "valid" + } + } +} + +/// :nodoc: +extension STPShippingType: CustomStringConvertible { + public var description: String { + switch self { + case .delivery: + return "delivery" + case .shipping: + return "shipping" + } + } +} diff --git a/Stripe/StripeiOS/Source/PKAddPaymentPassRequest+Stripe_Error.swift b/Stripe/StripeiOS/Source/PKAddPaymentPassRequest+Stripe_Error.swift new file mode 100644 index 00000000..ab0f3bbe --- /dev/null +++ b/Stripe/StripeiOS/Source/PKAddPaymentPassRequest+Stripe_Error.swift @@ -0,0 +1,30 @@ +// +// PKAddPaymentPassRequest+Stripe_Error.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/29/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import ObjectiveC +import PassKit + +var stpAddPaymentPassRequest: UInt8 = 0 + +// This is used to store an error on a PKAddPaymentPassRequest +// so that STPFakeAddPaymentPassViewController can inspect it for debugging. +extension PKAddPaymentPassRequest { + @objc var stp_error: NSError? { + get { + return objc_getAssociatedObject(self, &stpAddPaymentPassRequest) as? NSError + } + set(stp_error) { + objc_setAssociatedObject( + self, + &stpAddPaymentPassRequest, + stp_error, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} diff --git a/Stripe/StripeiOS/Source/PKPaymentAuthorizationViewController+Stripe_Blocks.swift b/Stripe/StripeiOS/Source/PKPaymentAuthorizationViewController+Stripe_Blocks.swift new file mode 100644 index 00000000..590d3342 --- /dev/null +++ b/Stripe/StripeiOS/Source/PKPaymentAuthorizationViewController+Stripe_Blocks.swift @@ -0,0 +1,187 @@ +// +// PKPaymentAuthorizationViewController+Stripe_Blocks.swift +// StripeiOS +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import ObjectiveC +import PassKit +@_spi(STP) import StripeCore + +typealias STPApplePayPaymentMethodHandlerBlock = (STPPaymentMethod, @escaping STPPaymentStatusBlock) + -> Void +typealias STPPaymentCompletionBlock = (STPPaymentStatus, Error?) -> Void +typealias STPPaymentSummaryItemCompletionBlock = ([PKPaymentSummaryItem]) -> Void +typealias STPShippingMethodSelectionBlock = ( + PKShippingMethod, @escaping STPPaymentSummaryItemCompletionBlock +) -> Void +typealias STPShippingAddressValidationBlock = ( + STPShippingStatus, [PKShippingMethod], [PKPaymentSummaryItem] +) -> Void +typealias STPShippingAddressSelectionBlock = ( + STPAddress, @escaping STPShippingAddressValidationBlock +) -> Void +typealias STPPaymentAuthorizationBlock = (PKPayment) -> Void +extension PKPaymentAuthorizationViewController { + class func stp_controller( + with paymentRequest: PKPaymentRequest, + apiClient: STPAPIClient, + onShippingAddressSelection: @escaping STPShippingAddressSelectionBlock, + onShippingMethodSelection: @escaping STPShippingMethodSelectionBlock, + onPaymentAuthorization: @escaping STPPaymentAuthorizationBlock, + onPaymentMethodCreation: @escaping STPApplePayPaymentMethodHandlerBlock, + onFinish: @escaping STPPaymentCompletionBlock + ) -> Self? { + let delegate = STPBlockBasedApplePayDelegate() + delegate.apiClient = apiClient + delegate.onShippingAddressSelection = onShippingAddressSelection + delegate.onShippingMethodSelection = onShippingMethodSelection + delegate.onPaymentAuthorization = onPaymentAuthorization + delegate.onPaymentMethodCreation = onPaymentMethodCreation + delegate.onFinish = onFinish + let viewController = self.init(paymentRequest: paymentRequest) + viewController?.delegate = delegate + if let viewController = viewController { + objc_setAssociatedObject( + viewController, + UnsafeRawPointer(&kSTPBlockBasedApplePayDelegateAssociatedObjectKey), + delegate, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + return viewController + } +} + +private var kSTPBlockBasedApplePayDelegateAssociatedObjectKey = 0 +typealias STPApplePayShippingMethodCompletionBlock = ( + PKPaymentAuthorizationStatus, [PKPaymentSummaryItem]? +) -> Void +typealias STPApplePayShippingAddressCompletionBlock = ( + PKPaymentAuthorizationStatus, [PKShippingMethod]?, [PKPaymentSummaryItem]? +) -> Void +class STPBlockBasedApplePayDelegate: NSObject, PKPaymentAuthorizationViewControllerDelegate { + var apiClient: STPAPIClient? + var onShippingAddressSelection: STPShippingAddressSelectionBlock? + var onShippingMethodSelection: STPShippingMethodSelectionBlock? + var onPaymentAuthorization: STPPaymentAuthorizationBlock? + var onPaymentMethodCreation: STPApplePayPaymentMethodHandlerBlock? + var onFinish: STPPaymentCompletionBlock? + var lastError: Error? + var didSucceed = false + + // Remove all this once we drop iOS 11 support + func paymentAuthorizationViewController( + _ controller: PKPaymentAuthorizationViewController, + didAuthorizePayment payment: PKPayment, + completion: @escaping (PKPaymentAuthorizationStatus) -> Void + ) { + onPaymentAuthorization?(payment) + + let paymentMethodCreateCompletion: ((STPPaymentMethod?, Error?) -> Void)? = { + result, + paymentMethodCreateError in + if let paymentMethodCreateError = paymentMethodCreateError { + self.lastError = paymentMethodCreateError + completion(.failure) + return + } + guard let result = result else { + self.lastError = NSError.stp_genericFailedToParseResponseError() + completion(.failure) + return + } + self.onPaymentMethodCreation?( + result, + { status, error in + if status != .success || error != nil { + self.lastError = error + completion(.failure) + if controller.presentingViewController == nil { + // If we call completion() after dismissing, didFinishWithStatus is NOT called. + self._finish() + } + return + } + self.didSucceed = true + completion(.success) + if controller.presentingViewController == nil { + // If we call completion() after dismissing, didFinishWithStatus is NOT called. + self._finish() + } + } + ) + } + if let paymentMethodCreateCompletion = paymentMethodCreateCompletion { + apiClient?.createPaymentMethod(with: payment, completion: paymentMethodCreateCompletion) + } + } + + func paymentAuthorizationViewController( + _ controller: PKPaymentAuthorizationViewController, + didSelect shippingMethod: PKShippingMethod, + completion: @escaping (PKPaymentAuthorizationStatus, [PKPaymentSummaryItem]) -> Void + ) { + onShippingMethodSelection?( + shippingMethod, + { summaryItems in + completion(PKPaymentAuthorizationStatus.success, summaryItems) + } + ) + } + + func paymentAuthorizationViewController( + _ controller: PKPaymentAuthorizationViewController, + didSelectShippingContact contact: PKContact, + handler completion: @escaping (PKPaymentRequestShippingContactUpdate) -> Void + ) { + let stpAddress = STPAddress(pkContact: contact) + onShippingAddressSelection?( + stpAddress, + { status, shippingMethods, summaryItems in + if status == .invalid { + let genericShippingError = NSError( + domain: PKPaymentErrorDomain, + code: PKPaymentError.shippingContactInvalidError.rawValue, + userInfo: nil + ) + completion( + PKPaymentRequestShippingContactUpdate( + errors: [genericShippingError], + paymentSummaryItems: summaryItems, + shippingMethods: shippingMethods + ) + ) + } else { + completion( + PKPaymentRequestShippingContactUpdate( + errors: nil, + paymentSummaryItems: summaryItems, + shippingMethods: shippingMethods + ) + ) + } + } + ) + } + + func paymentAuthorizationViewControllerDidFinish( + _ controller: PKPaymentAuthorizationViewController + ) { + _finish() + } + + func _finish() { + if didSucceed { + onFinish?(.success, nil) + } else if let lastError = lastError { + onFinish?(.error, lastError) + } else { + onFinish?(.userCancellation, nil) + } + } +} + +typealias STPPaymentAuthorizationStatusCallback = (PKPaymentAuthorizationStatus) -> Void diff --git a/Stripe/StripeiOS/Source/STPAPIClient+BasicUI.swift b/Stripe/StripeiOS/Source/STPAPIClient+BasicUI.swift new file mode 100644 index 00000000..00e78896 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAPIClient+BasicUI.swift @@ -0,0 +1,178 @@ +// +// STPAPIClient+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments + +/// A client for making connections to the Stripe API. +extension STPAPIClient { + /// Initializes an API client with the given configuration. + /// - Parameter configuration: The configuration to use. + /// - Returns: An instance of STPAPIClient. + @available( + *, + deprecated, + message: + "This initializer previously configured publishableKey and stripeAccount via the STPPaymentConfiguration instance. This behavior is deprecated; set the STPAPIClient configuration, publishableKey, and stripeAccount properties directly on the STPAPIClient instead." + ) + public convenience init( + configuration: STPPaymentConfiguration + ) { + // For legacy reasons, we'll support this initializer and use the deprecated configuration.{publishableKey, stripeAccount} properties + self.init() + publishableKey = configuration.publishableKey + stripeAccount = configuration.stripeAccount + } +} + +extension STPAPIClient { + /// The client's configuration. + /// Defaults to `STPPaymentConfiguration.shared`. + @objc public var configuration: STPPaymentConfiguration { + get { + if let config = _stored_configuration as? STPPaymentConfiguration { + return config + } else { + return .shared + } + } + set { + _stored_configuration = newValue + } + } + + /// Update a customer with parameters + /// - seealso: https://stripe.com/docs/api#update_customer + func updateCustomer( + withParameters parameters: [String: Any], + using ephemeralKey: STPEphemeralKey, + completion: @escaping STPCustomerCompletionBlock + ) { + let endpoint = "\(APIEndpointCustomers)/\(ephemeralKey.customerID ?? "")" + APIRequest.post( + with: self, + endpoint: endpoint, + additionalHeaders: authorizationHeader(using: ephemeralKey), + parameters: parameters + ) { object, _, error in + completion(object, error) + } + } + + /// Attach a Payment Method to a customer + /// - seealso: https://stripe.com/docs/api/payment_methods/attach + func attachPaymentMethod( + _ paymentMethodID: String, + toCustomerUsing ephemeralKey: STPEphemeralKey, + completion: @escaping STPErrorBlock + ) { + guard let customerID = ephemeralKey.customerID else { + assertionFailure() + completion(nil) + return + } + let endpoint = "\(APIEndpointPaymentMethods)/\(paymentMethodID)/attach" + APIRequest.post( + with: self, + endpoint: endpoint, + additionalHeaders: authorizationHeader(using: ephemeralKey), + parameters: [ + "customer": customerID + ] + ) { _, _, error in + completion(error) + } + } + + /// Detach a Payment Method from a customer + /// - seealso: https://stripe.com/docs/api/payment_methods/detach + func detachPaymentMethod( + _ paymentMethodID: String, + fromCustomerUsing ephemeralKey: STPEphemeralKey, + completion: @escaping STPErrorBlock + ) { + let endpoint = "\(APIEndpointPaymentMethods)/\(paymentMethodID)/detach" + APIRequest.post( + with: self, + endpoint: endpoint, + additionalHeaders: authorizationHeader(using: ephemeralKey), + parameters: [:] + ) { _, _, error in + completion(error) + } + } + + /// Retrieves a list of Payment Methods attached to a customer. + /// @note This only fetches card type Payment Methods + func listPaymentMethodsForCustomer( + using ephemeralKey: STPEphemeralKey, + completion: @escaping STPPaymentMethodsCompletionBlock + ) { + let header = authorizationHeader(using: ephemeralKey) + let params: [String: Any] = [ + "customer": ephemeralKey.customerID ?? "", + "type": "card", + ] + APIRequest.getWith( + self, + endpoint: APIEndpointPaymentMethods, + additionalHeaders: header, + parameters: params as [String: Any] + ) { deserializer, _, error in + if let error = error { + completion(nil, error) + } else if let paymentMethods = deserializer?.paymentMethods { + completion(paymentMethods, nil) + } + } + } + + /// Retrieve a customer + /// - seealso: https://stripe.com/docs/api#retrieve_customer + func retrieveCustomer( + using ephemeralKey: STPEphemeralKey, + completion: @escaping STPCustomerCompletionBlock + ) { + let endpoint = "\(APIEndpointCustomers)/\(ephemeralKey.customerID ?? "")" + APIRequest.getWith( + self, + endpoint: endpoint, + additionalHeaders: authorizationHeader(using: ephemeralKey), + parameters: [:] + ) { object, _, error in + completion(object, error) + } + } + + // MARK: FPX + /// Retrieves the online status of the FPX banks from the Stripe API. + /// - Parameter completion: The callback to run with the returned FPX bank list, or an error. + func retrieveFPXBankStatus( + withCompletion completion: @escaping STPFPXBankStatusCompletionBlock + ) { + APIRequest.getWith( + self, + endpoint: APIEndpointFPXStatus, + parameters: [ + "account_holder_type": "individual" + ] + ) { statusResponse, _, error in + completion(statusResponse, error) + } + } + + // MARK: Helpers + + /// A helper method that returns the Authorization header to use for API requests. If ephemeralKey is nil, uses self.publishableKey instead. + func authorizationHeader(using ephemeralKey: STPEphemeralKey? = nil) -> [String: String] { + return authorizationHeader(using: ephemeralKey?.secret) + } +} + +private let APIEndpointFPXStatus = "fpx/bank_statuses" diff --git a/Stripe/StripeiOS/Source/STPAPIClient+PushProvisioning.swift b/Stripe/StripeiOS/Source/STPAPIClient+PushProvisioning.swift new file mode 100644 index 00000000..1acda148 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAPIClient+PushProvisioning.swift @@ -0,0 +1,40 @@ +// +// STPAPIClient+PushProvisioning.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/27/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI + +typealias STPPushProvisioningDetailsCompletionBlock = (STPPushProvisioningDetails?, Error?) -> Void +extension STPAPIClient { + func retrievePushProvisioningDetails( + with params: STPPushProvisioningDetailsParams, + ephemeralKey: STPEphemeralKey, + completion: @escaping STPPushProvisioningDetailsCompletionBlock + ) { + + let endpoint = "issuing/cards/\(params.cardId)/push_provisioning_details" + let parameters = [ + "ios": [ + "certificates": params.certificatesBase64, + "nonce": params.nonceHex, + "nonce_signature": params.nonceSignatureHex, + ] as [String: Any], + ] + + APIRequest.getWith( + self, + endpoint: endpoint, + additionalHeaders: authorizationHeader(using: ephemeralKey), + parameters: parameters + ) { details, _, error in + completion(details, error) + } + } +} diff --git a/Stripe/StripeiOS/Source/STPAddCardViewController.swift b/Stripe/StripeiOS/Source/STPAddCardViewController.swift new file mode 100644 index 00000000..853908fd --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAddCardViewController.swift @@ -0,0 +1,1001 @@ +// +// STPAddCardViewController.swift +// StripeiOS +// +// Created by Jack Flintermann on 3/23/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +/// This view controller contains a credit card entry form that the user can fill out. On submission, it will use the Stripe API to convert the user's card details to a Stripe token. It renders a right bar button item that submits the form, so it must be shown inside a `UINavigationController`. +public class STPAddCardViewController: STPCoreTableViewController, STPAddressViewModelDelegate, + STPCardScannerDelegate, STPPaymentCardTextFieldDelegate, UITableViewDelegate, + UITableViewDataSource +{ + + /// A convenience initializer; equivalent to calling `init(configuration: STPPaymentConfiguration.shared, theme: STPTheme.defaultTheme)`. + @objc + public convenience init() { + self.init(configuration: STPPaymentConfiguration.shared, theme: STPTheme.defaultTheme) + } + + /// Initializes a new `STPAddCardViewController` with the provided configuration and theme. Don't forget to set the `delegate` property after initialization. + /// - Parameters: + /// - configuration: The configuration to use (this determines the Stripe publishable key to use, the required billing address fields, whether or not to use SMS autofill, etc). - seealso: STPPaymentConfiguration + /// - theme: The theme to use to inform the view controller's visual appearance. - seealso: STPTheme + @objc(initWithConfiguration:theme:) + public init( + configuration: STPPaymentConfiguration, + theme: STPTheme + ) { + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(theme: theme) + commonInit(with: configuration) + } + + /// The view controller's delegate. This must be set before showing the view controller in order for it to work properly. - seealso: STPAddCardViewControllerDelegate + @objc public weak var delegate: STPAddCardViewControllerDelegate? + /// You can set this property to pre-fill any information you've already collected from your user. - seealso: STPUserInformation.h + @objc public var prefilledInformation: STPUserInformation? { + didSet { + if let address = prefilledInformation?.billingAddress { + addressViewModel.address = address + } + } + } + // Should be overwritten if this class is used by STPPaymentContext or STPPaymentOptionsViewController + internal var analyticsLogger: STPPaymentContext.AnalyticsLogger = .init(product: STPAddCardViewController.self) + private var _customFooterView: UIView? + /// Provide this view controller with a footer view. + /// When the footer view needs to be resized, it will be sent a + /// `sizeThatFits:` call. The view should respond correctly to this method in order + /// to be sized and positioned properly. + @objc public var customFooterView: UIView? { + get { + _customFooterView + } + set(footerView) { + _customFooterView = footerView + _configureFooterView() + } + } + + func _configureFooterView() { + if isViewLoaded, let footerView = _customFooterView { + let size = footerView.sizeThatFits( + CGSize(width: view.bounds.size.width, height: CGFloat.greatestFiniteMagnitude) + ) + footerView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) + + tableView?.tableFooterView = footerView + } + } + + /// The API Client to use to make requests. + /// Defaults to `STPAPIClient.shared` + public var apiClient: STPAPIClient = STPAPIClient.shared + + /// Use init: or initWithConfiguration:theme: + required init( + theme: STPTheme? + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(theme: theme) + } + + /// Use init: or initWithConfiguration:theme: + required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + /// Use init: or initWithConfiguration:theme: + required init?( + coder aDecoder: NSCoder + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(coder: aDecoder) + } + + private var _alwaysEnableDoneButton = false + @objc var alwaysEnableDoneButton: Bool { + get { + _alwaysEnableDoneButton + } + set(alwaysEnableDoneButton) { + if alwaysEnableDoneButton != _alwaysEnableDoneButton { + _alwaysEnableDoneButton = alwaysEnableDoneButton + updateDoneButton() + } + } + } + private var configuration: STPPaymentConfiguration? + @objc var shippingAddress: STPAddress? + private var hasUsedShippingAddress = false + private weak var cardImageView: UIImageView? + private var doneItem: UIBarButtonItem? + private var cardHeaderView: STPSectionHeaderView? + + @available(macCatalyst 14.0, *) + private var cardScanner: STPCardScanner? { + get { + _cardScanner as? STPCardScanner + } + set { + _cardScanner = newValue + } + } + + /// Storage for `cardScanner`. + private var _cardScanner: NSObject? + + @available(macCatalyst 14.0, *) + private var scannerCell: STPCardScannerTableViewCell? { + get { + _scannerCell as? STPCardScannerTableViewCell + } + set { + _scannerCell = newValue + } + } + + /// Storage for `scannerCell`. + private var _scannerCell: NSObject? + + private var _isScanning = false + private var isScanning: Bool { + get { + _isScanning + } + set(isScanning) { + if _isScanning == isScanning { + return + } + _isScanning = isScanning + + cardHeaderView?.button?.isEnabled = !isScanning + let indexPath = IndexPath( + row: 0, + section: STPPaymentCardSection.stpPaymentCardScannerSection.rawValue + ) + tableView?.beginUpdates() + if isScanning { + tableView?.insertRows(at: [indexPath], with: .automatic) + } else { + tableView?.deleteRows(at: [indexPath], with: .automatic) + } + tableView?.endUpdates() + if isScanning { + tableView?.scrollToRow(at: indexPath, at: .middle, animated: true) + } + updateInputAccessoryVisiblity() + } + } + private var addressHeaderView: STPSectionHeaderView? + var paymentCell: STPPaymentCardTextFieldCell? + var viewDidAppearStartTime: Date? + + private var _loading = false + @objc var loading: Bool { + get { + _loading + } + set(loading) { + if loading == _loading { + return + } + _loading = loading + stp_navigationItemProxy?.setHidesBackButton(loading, animated: true) + stp_navigationItemProxy?.leftBarButtonItem?.isEnabled = !loading + activityIndicator?.animating = loading + if loading { + tableView?.endEditing(true) + var loadingItem: UIBarButtonItem? + if let activityIndicator = activityIndicator { + loadingItem = UIBarButtonItem(customView: activityIndicator) + } + stp_navigationItemProxy?.setRightBarButton(loadingItem, animated: true) + cardHeaderView?.buttonHidden = true + } else { + stp_navigationItemProxy?.setRightBarButton(doneItem, animated: true) + cardHeaderView?.buttonHidden = false + } + var cells = addressViewModel.addressCells as [UITableViewCell] + + if let paymentCell = paymentCell { + cells.append(paymentCell) + } + for cell in cells { + cell.isUserInteractionEnabled = !loading + UIView.animate( + withDuration: 0.1, + animations: { + cell.alpha = loading ? 0.7 : 1.0 + } + ) + } + } + } + private var activityIndicator: STPPaymentActivityIndicatorView? + private weak var lookupActivityIndicator: STPPaymentActivityIndicatorView? + var addressViewModel: STPAddressViewModel + private var inputAccessoryToolbar: UIToolbar? + private var lookupSucceeded = false + private var scannerCompleteAnimationTimer: Timer? + var didSendFormInteractedAnalytic = false + + @objc(commonInitWithConfiguration:) func commonInit(with configuration: STPPaymentConfiguration) + { + STPAnalyticsClient.sharedClient.addClass( + toProductUsageIfNecessary: STPAddCardViewController.self + ) + + self.configuration = configuration + shippingAddress = nil + hasUsedShippingAddress = false + addressViewModel.delegate = self + title = STPLocalizedString("Add a Card", "Title for Add a Card view") + + if #available(macCatalyst 14.0, *) { + cardScanner = STPCardScanner() + } + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + estimatedHeightForRowAt indexPath: IndexPath + ) -> CGFloat { + return 44.0 + } + + @objc override func createAndSetupViews() { + super.createAndSetupViews() + + let doneItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(nextPressed(_:)) + ) + self.doneItem = doneItem + stp_navigationItemProxy?.rightBarButtonItem = doneItem + updateDoneButton() + + stp_navigationItemProxy?.leftBarButtonItem?.accessibilityIdentifier = + "AddCardViewControllerNavBarCancelButtonIdentifier" + stp_navigationItemProxy?.rightBarButtonItem?.accessibilityIdentifier = + "AddCardViewControllerNavBarDoneButtonIdentifier" + + let cardImageView = UIImageView(image: STPLegacyImageLibrary.largeCardFrontImage()) + cardImageView.contentMode = .center + cardImageView.frame = CGRect( + x: 0, + y: 0, + width: view.bounds.size.width, + height: cardImageView.bounds.size.height + (57 * 2) + ) + self.cardImageView = cardImageView + tableView?.tableHeaderView = cardImageView + + let paymentCell = STPPaymentCardTextFieldCell( + style: .default, + reuseIdentifier: "STPAddCardViewControllerPaymentCardTextFieldCell" + ) + paymentCell.paymentField?.delegate = self + if configuration?.requiredBillingAddressFields == .postalCode { + // If postal code collection is enabled, move the postal code field into the card entry field. + // Otherwise, this will be picked up by the billing address fields below. + paymentCell.paymentField?.postalCodeEntryEnabled = true + } + self.paymentCell = paymentCell + + activityIndicator = STPPaymentActivityIndicatorView( + frame: CGRect(x: 0, y: 0, width: 20.0, height: 20.0) + ) + + inputAccessoryToolbar = UIToolbar.stp_inputAccessoryToolbar( + withTarget: self, + action: #selector(paymentFieldNextTapped) + ) + inputAccessoryToolbar?.stp_setEnabled(false) + updateInputAccessoryVisiblity() + tableView?.dataSource = self + tableView?.delegate = self + tableView?.reloadData() + if let address = prefilledInformation?.billingAddress { + addressViewModel.address = address + } + + let addressHeaderView = STPSectionHeaderView() + addressHeaderView.theme = theme + addressHeaderView.title = String.Localized.billing_address + switch configuration?.shippingType { + case .shipping: + addressHeaderView.button?.setTitle( + STPLocalizedString( + "Use Shipping", + "Button to fill billing address from shipping address." + ), + for: .normal + ) + case .delivery: + addressHeaderView.button?.setTitle( + STPLocalizedString( + "Use Delivery", + "Button to fill billing address from delivery address." + ), + for: .normal + ) + default: + break + } + addressHeaderView.button?.addTarget( + self, + action: #selector(useShippingAddress(_:)), + for: .touchUpInside + ) + let requiredFields = configuration?.requiredBillingAddressFields ?? .none + let needsAddress = requiredFields != .none && !addressViewModel.isValid + let buttonVisible = + needsAddress && shippingAddress?.containsContent(for: requiredFields) != nil + && !hasUsedShippingAddress + addressHeaderView.buttonHidden = !buttonVisible + addressHeaderView.setNeedsLayout() + self.addressHeaderView = addressHeaderView + let cardHeaderView = STPSectionHeaderView() + cardHeaderView.theme = theme + cardHeaderView.title = STPPaymentMethodType.card.displayName + cardHeaderView.buttonHidden = true + self.cardHeaderView = cardHeaderView + + // re-set the custom footer view if it was added before we loaded + _configureFooterView() + + view.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(endEditing)) + ) + + setUpCardScanningIfAvailable() + + STPAnalyticsClient.sharedClient.clearAdditionalInfo() + } + + /// :nodoc: + @objc + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Resetting it re-calculates the size based on new view width + // UITableView requires us to call setter again to actually pick up frame + // change on footers + if tableView?.tableFooterView != nil { + customFooterView = tableView?.tableFooterView + } + } + + func setUpCardScanningIfAvailable() { + if #available(macCatalyst 14.0, *) { + if !STPCardScanner.cardScanningAvailable || configuration?.cardScanningEnabled != true { + return + } + let scannerCell = STPCardScannerTableViewCell() + self.scannerCell = scannerCell + + let cardScanner = STPCardScanner(delegate: self) + cardScanner.cameraView = scannerCell.cameraView + self.cardScanner = cardScanner + + cardHeaderView?.buttonHidden = false + cardHeaderView?.button?.setTitle( + String.Localized.scan_card_title_capitalization, + for: .normal + ) + cardHeaderView?.button?.addTarget( + self, + action: #selector(scanCard), + for: .touchUpInside + ) + cardHeaderView?.setNeedsLayout() + } + } + + @available(macCatalyst 14.0, *) + @objc func scanCard() { + view.endEditing(true) + isScanning = true + cardScanner?.start() + sendFormInteractedAnalyticIfNecessary() + } + + @objc func endEditing() { + view.endEditing(false) + } + + /// :nodoc: + @objc + public override func updateAppearance() { + super.updateAppearance() + + view.backgroundColor = theme.primaryBackgroundColor + + let navBarTheme = navigationController?.navigationBar.stp_theme ?? theme + doneItem?.stp_setTheme(navBarTheme) + tableView?.allowsSelection = false + + cardImageView?.tintColor = theme.accentColor + activityIndicator?.tintColor = theme.accentColor + + paymentCell?.theme = theme + cardHeaderView?.theme = theme + addressHeaderView?.theme = theme + for cell in addressViewModel.addressCells { + cell.theme = theme + } + setNeedsStatusBarAppearanceUpdate() + } + + /// :nodoc: + @objc + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewDidAppearStartTime = Date() + analyticsLogger.logFormShown(paymentMethodType: .card) + + stp_beginObservingKeyboardAndInsettingScrollView( + tableView, + onChange: nil + ) + firstEmptyField()?.becomeFirstResponder() + } + + func firstEmptyField() -> UIResponder? { + + if paymentCell?.isEmpty != nil { + return paymentCell! + } + + for cell in addressViewModel.addressCells { + if cell.contents?.count ?? 0 == 0 { + return cell + } + } + return nil + } + + /// :nodoc: + @objc + public override func handleCancelTapped(_ sender: Any?) { + delegate?.addCardViewControllerDidCancel(self) + } + + @objc func nextPressed(_ sender: Any?) { + analyticsLogger.logDoneButtonTapped(paymentMethodType: .card, shownStartDate: viewDidAppearStartTime) + loading = true + guard let cardParams = paymentCell?.paymentField?.paymentMethodParams.card else { + return + } + // Create and return a Payment Method + let billingDetails = STPPaymentMethodBillingDetails() + if configuration?.requiredBillingAddressFields == .postalCode { + let address = STPAddress() + address.postalCode = paymentCell?.paymentField?.postalCode + billingDetails.address = STPPaymentMethodAddress(address: address) + } else { + billingDetails.address = STPPaymentMethodAddress(address: addressViewModel.address) + billingDetails.email = addressViewModel.address.email + billingDetails.name = addressViewModel.address.name + billingDetails.phone = addressViewModel.address.phone + } + let paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil + ) + apiClient.createPaymentMethod(with: paymentMethodParams) { + paymentMethod, + createPaymentMethodError in + if let createPaymentMethodError = createPaymentMethodError { + self.handleError(createPaymentMethodError) + } else { + if let paymentMethod = paymentMethod { + self.delegate?.addCardViewController( + self, + didCreatePaymentMethod: paymentMethod + ) { + attachToCustomerError in + stpDispatchToMainThreadIfNecessary({ + if let attachToCustomerError = attachToCustomerError { + self.handleError(attachToCustomerError) + } else { + self.loading = false + } + }) + } + } + } + } + } + + func handleError(_ error: Error) { + loading = false + firstEmptyField()?.becomeFirstResponder() + + let alertController = UIAlertController( + title: error.localizedDescription, + message: (error as NSError).localizedFailureReason, + preferredStyle: .alert + ) + + alertController.addAction( + UIAlertAction( + title: String.Localized.ok, + style: .cancel, + handler: nil + ) + ) + + present(alertController, animated: true) + } + + func updateDoneButton() { + stp_navigationItemProxy?.rightBarButtonItem?.isEnabled = + (paymentCell?.paymentField?.isValid ?? false && addressViewModel.isValid) + || alwaysEnableDoneButton + } + + func updateInputAccessoryVisiblity() { + // The inputAccessoryToolbar switches from the paymentCell to the first address field. + // It should only be shown when there *is* an address field. This compensates for the lack + // of a 'Return' key on the number pad used for paymentCell entry + let hasAddressCells = (addressViewModel.addressCells.count) > 0 + paymentCell?.inputAccessoryView = hasAddressCells ? inputAccessoryToolbar : nil + } + + /// Only send form interacted analytic once per time this screen is shown + func sendFormInteractedAnalyticIfNecessary() { + if !didSendFormInteractedAnalytic { + didSendFormInteractedAnalytic = true + analyticsLogger.logFormInteracted(paymentMethodType: .card) + } + } + + /// Only send card number completed analytic once per time the card number changes from invalid to valid + var shouldSendCardNumberCompletedAnalytic = true + func sendCardNumberCompletedAnalyticIfNecessary(cardNumber: String?) { + let isCardNumberValid = STPCardValidator.validationState(forNumber: cardNumber, validatingCardBrand: true) == .valid + if isCardNumberValid, shouldSendCardNumberCompletedAnalytic { + analyticsLogger.logCardNumberCompleted() + shouldSendCardNumberCompletedAnalytic = false + } else if !isCardNumberValid { + // Reset shouldSendCardNumberCompletedAnalytic when the card number is invalid, so that it gets sent when the card number becomes valid. + shouldSendCardNumberCompletedAnalytic = true + } + } + + // MARK: - STPPaymentCardTextField + @objc + public func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { + sendFormInteractedAnalyticIfNecessary() + sendCardNumberCompletedAnalyticIfNecessary(cardNumber: textField.cardNumber) + inputAccessoryToolbar?.stp_setEnabled(textField.isValid) + updateDoneButton() + } + + @objc func paymentFieldNextTapped() { + _ = addressViewModel.addressCells.stp_boundSafeObject(at: 0)?.becomeFirstResponder() + } + + @objc + public func paymentCardTextFieldWillEndEditing(forReturn textField: STPPaymentCardTextField) { + paymentFieldNextTapped() + } + + @objc + public func paymentCardTextFieldDidBeginEditingCVC(_ textField: STPPaymentCardTextField) { + let isAmex = STPCardValidator.brand(forNumber: textField.cardNumber ?? "") == .amex + var newImage: UIImage? + var animationTransition: UIView.AnimationOptions + + if isAmex { + newImage = STPLegacyImageLibrary.largeCardAmexCVCImage() + animationTransition = .transitionCrossDissolve + } else { + newImage = STPLegacyImageLibrary.largeCardBackImage() + animationTransition = .transitionFlipFromRight + } + + if let cardImageView = cardImageView { + UIView.transition( + with: cardImageView, + duration: 0.2, + options: animationTransition, + animations: { + self.cardImageView?.image = newImage + } + ) + } + } + + @objc + public func paymentCardTextFieldDidEndEditingCVC(_ textField: STPPaymentCardTextField) { + let isAmex = STPCardValidator.brand(forNumber: textField.cardNumber ?? "") == .amex + let animationTransition: UIView.AnimationOptions = + isAmex ? .transitionCrossDissolve : .transitionFlipFromLeft + + if let cardImageView = cardImageView { + UIView.transition( + with: cardImageView, + duration: 0.2, + options: animationTransition, + animations: { + self.cardImageView?.image = STPLegacyImageLibrary.largeCardFrontImage() + } + ) + } + } + + @objc + public func paymentCardTextFieldDidBeginEditing(_ textField: STPPaymentCardTextField) { + if #available(macCatalyst 14.0, *) { + cardScanner?.stop() + } + } + + // MARK: - STPAddressViewModelDelegate + func addressViewModel(_ addressViewModel: STPAddressViewModel, addedCellAt index: Int) { + let indexPath = IndexPath( + row: index, + section: STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue + ) + tableView?.insertRows(at: [indexPath], with: .automatic) + updateInputAccessoryVisiblity() + } + + func addressViewModel(_ addressViewModel: STPAddressViewModel, removedCellAt index: Int) { + let indexPath = IndexPath( + row: Int(index), + section: STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue + ) + tableView?.deleteRows(at: [indexPath], with: .automatic) + updateInputAccessoryVisiblity() + } + + func addressViewModelDidChange(_ addressViewModel: STPAddressViewModel) { + updateDoneButton() + } + + func addressViewModelWillUpdate(_ addressViewModel: STPAddressViewModel) { + tableView?.beginUpdates() + } + + func addressViewModelDidUpdate(_ addressViewModel: STPAddressViewModel) { + tableView?.endUpdates() + } + + // MARK: - UITableView + /// :nodoc: + @objc + public func numberOfSections(in tableView: UITableView) -> Int { + return 3 + } + + /// :nodoc: + @objc + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == STPPaymentCardSection.stpPaymentCardNumberSection.rawValue { + return 1 + } else if section == STPPaymentCardSection.stpPaymentCardScannerSection.rawValue { + return isScanning ? 1 : 0 + } else if section == STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue { + return addressViewModel.addressCells.count + } + return 0 + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + var cell: UITableViewCell? + switch indexPath.section { + case STPPaymentCardSection.stpPaymentCardNumberSection.rawValue: + cell = paymentCell + case STPPaymentCardSection.stpPaymentCardScannerSection.rawValue: + if #available(macCatalyst 14.0, *) { + cell = scannerCell + } else { + return UITableViewCell() + } + case STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue: + cell = addressViewModel.addressCells.stp_boundSafeObject(at: indexPath.row) + default: + return UITableViewCell() // won't be called; exists to make the static analyzer happy + } + cell?.backgroundColor = theme.secondaryBackgroundColor + cell?.contentView.backgroundColor = UIColor.clear + return cell! + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let topRow = indexPath.row == 0 + let bottomRow = + self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 == indexPath.row + cell.stp_setBorderColor(theme.tertiaryBackgroundColor) + cell.stp_setTopBorderHidden(!topRow) + cell.stp_setBottomBorderHidden(!bottomRow) + cell.stp_setFakeSeparatorColor(theme.quaternaryBackgroundColor) + cell.stp_setFakeSeparatorLeftInset(15.0) + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + heightForFooterInSection section: Int + ) + -> CGFloat + { + if self.tableView(tableView, numberOfRowsInSection: section) == 0 { + return 0.01 + } + return 27.0 + } + + /// :nodoc: + @objc + public override func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) -> CGFloat { + let fittingSize = CGSize( + width: view.bounds.size.width, + height: CGFloat.greatestFiniteMagnitude + ) + let numberOfRows = self.tableView(tableView, numberOfRowsInSection: section) + if section == STPPaymentCardSection.stpPaymentCardNumberSection.rawValue { + return cardHeaderView?.sizeThatFits(fittingSize).height ?? 0.0 + } else if section == STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue + && numberOfRows != 0 + { + return addressHeaderView?.sizeThatFits(fittingSize).height ?? 0.0 + } else if section == STPPaymentCardSection.stpPaymentCardScannerSection.rawValue { + return 0.01 + } else if numberOfRows != 0 { + return tableView.sectionHeaderHeight + } + return 0.01 + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + viewForHeaderInSection section: Int + ) + -> UIView? + { + if self.tableView(tableView, numberOfRowsInSection: section) == 0 { + return UIView() + } else { + if section == STPPaymentCardSection.stpPaymentCardNumberSection.rawValue { + return cardHeaderView + } else if section == STPPaymentCardSection.stpPaymentCardBillingAddressSection.rawValue + { + return addressHeaderView + } + } + return nil + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + viewForFooterInSection section: Int + ) + -> UIView? + { + return UIView() + } + + @objc func useShippingAddress(_ sender: UIButton) { + tableView?.beginUpdates() + addressViewModel.address = shippingAddress ?? STPAddress() + hasUsedShippingAddress = true + firstEmptyField()?.becomeFirstResponder() + UIView.animate( + withDuration: 0.2, + animations: { + self.addressHeaderView?.buttonHidden = true + } + ) + tableView?.endUpdates() + } + + // MARK: - STPCardScanner + /// :nodoc: + @objc + public override func viewWillTransition( + to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator + ) { + super.viewWillTransition(to: size, with: coordinator) + let orientation = UIDevice.current.orientation + if orientation.isPortrait || orientation.isLandscape { + if #available(macCatalyst 14.0, *) { + cardScanner?.deviceOrientation = orientation + } + } + if isScanning { + let indexPath = IndexPath( + row: 0, + section: STPPaymentCardSection.stpPaymentCardScannerSection.rawValue + ) + DispatchQueue.main.async(execute: { + self.tableView?.scrollToRow(at: indexPath, at: .middle, animated: true) + }) + } + } + + static let cardScannerKSTPCardScanAnimationTime: TimeInterval = 0.04 + + @available(macCatalyst 14.0, *) + func cardScanner( + _ scanner: STPCardScanner, + didFinishWith cardParams: STPPaymentMethodCardParams?, + error: Error? + ) { + if let error = error { + handleError(error) + } + if let cardParams = cardParams { + view.isUserInteractionEnabled = false + paymentCell?.paymentField?.inputView = UIView() as? UIInputView + var i = 0 + scannerCompleteAnimationTimer = Timer.scheduledTimer( + withTimeInterval: STPAddCardViewController.cardScannerKSTPCardScanAnimationTime, + repeats: true, + block: { timer in + i += 1 + let newParams = STPPaymentMethodCardParams() + guard let number = cardParams.number else { + timer.invalidate() + self.view.isUserInteractionEnabled = false + return + } + if i < number.count { + newParams.number = String( + number[...number.index(number.startIndex, offsetBy: i)] + ) + } else { + newParams.number = number + } + self.paymentCell?.paymentField?.paymentMethodParams = STPPaymentMethodParams( + card: newParams, + billingDetails: nil, + metadata: nil + ) + if i > number.count { + self.paymentCell?.paymentField?.paymentMethodParams = + STPPaymentMethodParams( + card: cardParams, + billingDetails: nil, + metadata: nil + ) + self.isScanning = false + self.paymentCell?.paymentField?.inputView = nil + // Force the inputView to reload by asking the text field to resign/become first responder: + _ = self.paymentCell?.paymentField?.resignFirstResponder() + _ = self.paymentCell?.paymentField?.becomeFirstResponder() + timer.invalidate() + self.view.isUserInteractionEnabled = true + } + } + ) + } else { + isScanning = false + } + } + +} + +/// An `STPAddCardViewControllerDelegate` is notified when an `STPAddCardViewController` +/// successfully creates a card token or is cancelled. It has internal error-handling +/// logic, so there's no error case to deal with. +@objc public protocol STPAddCardViewControllerDelegate: NSObjectProtocol { + /// Called when the user cancels adding a card. You should dismiss (or pop) the + /// view controller at this point. + /// - Parameter addCardViewController: the view controller that has been cancelled + func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) + + /// This is called when the user successfully adds a card and Stripe returns a + /// Payment Method. + /// You should send the PaymentMethod to your backend to store it on a customer, and then + /// call the provided `completion` block when that call is finished. If an error + /// occurs while talking to your backend, call `completion(error)`, otherwise, + /// dismiss (or pop) the view controller. + /// - Parameters: + /// - addCardViewController: the view controller that successfully created a token + /// - paymentMethod: the Payment Method that was created. - seealso: STPPaymentMethod + /// - completion: call this callback when you're done sending the token to your backend + @objc func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + completion: @escaping STPErrorBlock + ) + + // MARK: - Deprecated + + /// This method is deprecated as of v16.0.0 (https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md#migrating-from-versions--1600). + /// To use this class, migrate your integration from Charges to PaymentIntents. See https://stripe.com/docs/payments/payment-intents/migration/charges#read + @available( + *, + deprecated, + message: + "Use addCardViewController(_:didCreatePaymentMethod:completion:) instead and migrate your integration to PaymentIntents. See https://stripe.com/docs/payments/payment-intents/migration/charges#read", + renamed: "addCardViewController(_:didCreatePaymentMethod:completion:)" + ) + @objc optional func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreateToken token: STPToken, + completion: STPErrorBlock + ) + /// This method is deprecated as of v16.0.0 (https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md#migrating-from-versions--1600). + /// To use this class, migrate your integration from Charges to PaymentIntents. See https://stripe.com/docs/payments/payment-intents/migration/charges#read + @available( + *, + deprecated, + message: + "Use addCardViewController(_:didCreatePaymentMethod:completion:) instead and migrate your integration to PaymentIntents. See https://stripe.com/docs/payments/payment-intents/migration/charges#read", + renamed: "addCardViewController(_:didCreatePaymentMethod:completion:)" + ) + @objc optional func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreateSource source: STPSource, + completion: STPErrorBlock + ) +} + +private let STPPaymentCardCellReuseIdentifier = "STPPaymentCardCellReuseIdentifier" +enum STPPaymentCardSection: Int { + case stpPaymentCardNumberSection = 0 + case stpPaymentCardScannerSection = 1 + case stpPaymentCardBillingAddressSection = 2 +} + +/// :nodoc: +@_spi(STP) extension STPAddCardViewController: STPAnalyticsProtocol { + @_spi(STP) public static let stp_analyticsIdentifier = "STPAddCardViewController" +} diff --git a/Stripe/StripeiOS/Source/STPAddress+BasicUI.swift b/Stripe/StripeiOS/Source/STPAddress+BasicUI.swift new file mode 100644 index 00000000..d99df351 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAddress+BasicUI.swift @@ -0,0 +1,212 @@ +// +// STPAddress+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore + +/// What set of billing address information you need to collect from your user. +/// +/// @note If the user is from a country that does not use zip/postal codes, +/// the user may not be asked for one regardless of this setting. +@objc +public enum STPBillingAddressFields: UInt { + /// No billing address information + case none + /// Just request the user's billing postal code + case postalCode + /// Request the user's full billing address + case full + /// Just request the user's billing name + case name + /// Just request the user's billing ZIP (synonym for STPBillingAddressFieldsZip) + @available(*, deprecated, message: "Use STPBillingAddressFields.postalCode instead") + case zip +} + +extension STPAddress { + + /// Checks if this STPAddress has the level of valid address information + /// required by the passed in setting. + /// - Parameter requiredFields: The required level of billing address information to + /// check against. + /// - Returns: YES if this address contains at least the necessary information, + /// NO otherwise. + @objc + public func containsRequiredFields(_ requiredFields: STPBillingAddressFields) -> Bool { + switch requiredFields { + case .none: + return true + case .postalCode: + return STPPostalCodeValidator.validationState( + forPostalCode: postalCode, + countryCode: country + ) == .valid + case .full: + return hasValidPostalAddress() + case .name: + return (name?.count ?? 0) > 0 + default: + fatalError() + } + } + + /// Checks if this STPAddress has any content (possibly invalid) in any of the + /// desired billing address fields. + /// Where `containsRequiredFields:` validates that this STPAddress contains valid data in + /// all of the required fields, this method checks for the existence of *any* data. + /// For example, if `desiredFields` is `STPBillingAddressFieldsZip`, this will check + /// if the postalCode is empty. + /// Note: When `desiredFields == STPBillingAddressFieldsNone`, this method always returns + /// NO. + /// @parameter desiredFields The billing address information the caller is interested in. + /// - Returns: YES if there is any data in this STPAddress that's relevant for those fields. + @objc(containsContentForBillingAddressFields:) + public func containsContent(for desiredFields: STPBillingAddressFields) -> Bool { + switch desiredFields { + case .none: + return false + case .postalCode: + return (postalCode?.count ?? 0) > 0 + case .full: + return hasPartialPostalAddress() + case .name: + return (name?.count ?? 0) > 0 + default: + fatalError() + } + } + + /// Checks if this STPAddress has the level of valid address information + /// required by the passed in setting. + /// Note: When `requiredFields == nil`, this method always returns + /// YES. + /// - Parameter requiredFields: The required shipping address information to check against. + /// - Returns: YES if this address contains at least the necessary information, + /// NO otherwise. + @objc + public func containsRequiredShippingAddressFields( + _ requiredFields: Set? + ) + -> Bool + { + guard let requiredFields = requiredFields else { + return true + } + var containsFields = true + + if requiredFields.contains(.name) { + containsFields = containsFields && (name?.count ?? 0) > 0 + } + if requiredFields.contains(.emailAddress) { + containsFields = + containsFields && STPEmailAddressValidator.stringIsValidEmailAddress(email) + } + if requiredFields.contains(.phoneNumber) { + containsFields = + containsFields + && STPPhoneNumberValidator.stringIsValidPhoneNumber( + phone ?? "", + forCountryCode: country + ) + } + if requiredFields.contains(.postalAddress) { + containsFields = containsFields && hasValidPostalAddress() + } + return containsFields + } + + /// Checks if this STPAddress has any content (possibly invalid) in any of the + /// desired shipping address fields. + /// Where `containsRequiredShippingAddressFields:` validates that this STPAddress + /// contains valid data in all of the required fields, this method checks for the + /// existence of *any* data. + /// Note: When `desiredFields == nil`, this method always returns + /// NO. + /// @parameter desiredFields The shipping address information the caller is interested in. + /// - Returns: YES if there is any data in this STPAddress that's relevant for those fields. + @objc + public func containsContent( + forShippingAddressFields desiredFields: Set? + ) + -> Bool + { + guard let desiredFields = desiredFields else { + return false + } + return (desiredFields.contains(.name) && (name?.count ?? 0) > 0) + || (desiredFields.contains(.emailAddress) && (email?.count ?? 0) > 0) + || (desiredFields.contains(.phoneNumber) && (phone?.count ?? 0) > 0) + || (desiredFields.contains(.postalAddress) && hasPartialPostalAddress()) + } + + /// Converts an STPBillingAddressFields enum value into the closest equivalent + /// representation of PKContactField options + /// - Parameter billingAddressFields: Stripe billing address fields enum value to convert. + /// - Returns: The closest representation of the billing address requirement as + /// a PKContactField value. + @objc(applePayContactFieldsFromBillingAddressFields:) + public class func applePayContactFields( + from billingAddressFields: STPBillingAddressFields + ) + -> Set + { + switch billingAddressFields { + case .none: + return Set([]) + case .postalCode, .full: + return Set([.name, .postalAddress]) + case .name: + return Set([.name]) + case .zip: + return Set() + @unknown default: + fatalError() + } + } + + /// Converts a set of STPContactField values into the closest equivalent + /// representation of PKContactField options + /// - Parameter contactFields: Stripe contact fields values to convert. + /// - Returns: The closest representation of the contact fields as + /// a PKContactField value. + @objc + public class func pkContactFields( + fromStripeContactFields contactFields: Set? + ) -> Set? { + guard let contactFields = contactFields else { + return nil + } + + var pkFields: Set = Set() + let stripeToPayKitContactMap: [STPContactField: PKContactField] = [ + STPContactField.postalAddress: PKContactField.postalAddress, + STPContactField.emailAddress: PKContactField.emailAddress, + STPContactField.phoneNumber: PKContactField.phoneNumber, + STPContactField.name: PKContactField.name, + ] + + for contactField in contactFields { + if let convertedField = stripeToPayKitContactMap[contactField] { + pkFields.insert(convertedField) + } + } + return pkFields + } + + private func hasValidPostalAddress() -> Bool { + return (line1?.count ?? 0) > 0 && (city?.count ?? 0) > 0 && (country?.count ?? 0) > 0 + && ((state?.count ?? 0) > 0 || !(country == "US")) + && (STPPostalCodeValidator.validationState( + forPostalCode: postalCode, + countryCode: country + ) == .valid) + } +} diff --git a/Stripe/StripeiOS/Source/STPAddressFieldTableViewCell.swift b/Stripe/StripeiOS/Source/STPAddressFieldTableViewCell.swift new file mode 100644 index 00000000..7ae4b4e7 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAddressFieldTableViewCell.swift @@ -0,0 +1,509 @@ +// +// STPAddressFieldTableViewCell.swift +// StripeiOS +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +enum STPAddressFieldType: Int { + case name + case line1 + case line2 + case city + case state + case zip + case country + case email + case phone +} + +protocol STPAddressFieldTableViewCellDelegate: AnyObject { + func addressFieldTableViewCellDidUpdateText(_ cell: STPAddressFieldTableViewCell) + + func addressFieldTableViewCellDidReturn(_ cell: STPAddressFieldTableViewCell) + func addressFieldTableViewCellDidEndEditing(_ cell: STPAddressFieldTableViewCell) + var addressFieldTableViewCountryCode: String? { get set } + var availableCountries: Set? { get set } +} + +class STPAddressFieldTableViewCell: UITableViewCell, UITextFieldDelegate, UIPickerViewDelegate, + UIPickerViewDataSource +{ + + init( + type: STPAddressFieldType, + contents: String?, + lastInList: Bool, + delegate: STPAddressFieldTableViewCellDelegate? + ) { + textField = { + if type == .phone { + // We have very specific US-based phone formatting that's built into STPFormTextField + let formTextField = STPFormTextField() + formTextField.preservesContentsOnPaste = false + formTextField.selectionEnabled = false + return formTextField + } else { + return STPValidatedTextField() + } + }() + + super.init(style: .default, reuseIdentifier: nil) + self.delegate = delegate + theme = STPTheme() + _contents = contents + + textField.delegate = self + textField.addTarget( + self, + action: #selector(STPAddressFieldTableViewCell.textFieldTextDidChange(textField:)), + for: .editingChanged + ) + contentView.addSubview(textField) + + let toolbar = UIToolbar() + let flexibleItem = UIBarButtonItem( + barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil + ) + let nextItem = UIBarButtonItem( + title: STPLocalizedString("Next", nil), + style: .done, + target: self, + action: #selector(nextTapped(sender:)) + ) + toolbar.items = [flexibleItem, nextItem] + inputAccessoryToolbar = toolbar + + var countryCode = NSLocale.autoupdatingCurrent.regionCode + var otherCountryCodes = Array( + self.delegate?.availableCountries ?? Set(NSLocale.isoCountryCodes) + ) + if otherCountryCodes.contains(countryCode ?? "") { + // Remove the current country code to re-add it once we sort the list. + otherCountryCodes.removeAll { $0 == countryCode } + } else { + // If it isn't in the list (if we've been configured to not show that country), don't re-add it. + countryCode = nil + } + let locale = NSLocale.current as NSLocale + otherCountryCodes = + (otherCountryCodes as NSArray).sortedArray(comparator: { code1, code2 in + guard let code1 = code1 as? String, let code2 = code2 as? String else { + return .orderedDescending + } + let localeID1 = NSLocale.localeIdentifier(fromComponents: [ + NSLocale.Key.countryCode.rawValue: code1 + ]) + let localeID2 = NSLocale.localeIdentifier(fromComponents: [ + NSLocale.Key.countryCode.rawValue: code2 + ]) + if let name1 = locale.displayName(forKey: .identifier, value: localeID1), + let name2 = locale.displayName(forKey: .identifier, value: localeID2) + { + return name1.compare(name2) + } else { + return .orderedDescending + } + }) as? [String] ?? [] + if let countryCode = countryCode { + countryCodes = ["", countryCode] + otherCountryCodes + } else { + countryCodes = [""] + otherCountryCodes + } + let pickerView = UIPickerView() + pickerView.dataSource = self + pickerView.delegate = self + countryPickerView = pickerView + + self.lastInList = lastInList + self.type = type + self.textField.text = contents + + var ourCountryCode = self.delegate?.addressFieldTableViewCountryCode + + if ourCountryCode == nil { + ourCountryCode = countryCode + } + delegateCountryCodeDidChange(countryCode: ourCountryCode ?? "") + updateAppearance() + + self.textField.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate( + [ + textField.leadingAnchor.constraint( + equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, + constant: 15 + ), + textField.trailingAnchor.constraint( + equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, + constant: -15 + ), + textField.topAnchor.constraint( + equalTo: contentView.safeAreaLayoutGuide.topAnchor, + constant: 1 + ), + contentView.safeAreaLayoutGuide.bottomAnchor.constraint( + greaterThanOrEqualTo: textField.bottomAnchor + ), + textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 43), + inputAccessoryToolbar?.heightAnchor.constraint(equalToConstant: 44), + ].compactMap { $0 } + ) + } + + var type: STPAddressFieldType = .name + var caption: String? { + get { + self.textField.placeholder + } + set { + self.textField.placeholder = newValue + } + } + + private(set) var textField: STPValidatedTextField + private var _contents: String? + var contents: String? { + get { + // iOS 11 QuickType completions from textContentType have a space at the end. + // This *keeps* that space in the `textField`, but removes leading/trailing spaces from + // the logical contents of this field, so they're ignored for validation and persisting + return _contents?.trimmingCharacters(in: CharacterSet.whitespaces) + } + set { + _contents = newValue + if self.type == .country { + self.updateTextFieldsAndCaptions() + } else { + self.textField.text = contents + } + if self.textField.isFirstResponder { + self.textField.validText = self.potentiallyValidContents + } else { + self.textField.validText = self.validContents + } + self.delegate?.addressFieldTableViewCellDidUpdateText(self) + } + } + + var theme: STPTheme = .defaultTheme { + didSet { + updateAppearance() + } + } + + var lastInList: Bool = false { + didSet { + updateTextFieldsAndCaptions() + } + } + + private var inputAccessoryToolbar: UIToolbar? + private var countryPickerView: UIPickerView? + private var countryCodes: [AnyHashable]? + private weak var delegate: STPAddressFieldTableViewCellDelegate? + private var ourCountryCode: String? + + func updateTextFieldsAndCaptions() { + textField.placeholder = placeholder(for: type) + + if !lastInList { + textField.returnKeyType = .next + } else { + textField.returnKeyType = .default + } + switch type { + case .name: + textField.keyboardType = .default + textField.textContentType = .name + case .line1: + textField.keyboardType = .numbersAndPunctuation + textField.textContentType = .streetAddressLine1 + case .line2: + textField.keyboardType = .numbersAndPunctuation + textField.textContentType = .streetAddressLine2 + case .city: + textField.keyboardType = .default + textField.textContentType = .addressCity + case .state: + textField.keyboardType = .default + textField.textContentType = .addressState + case .zip: + textField.keyboardType = .numbersAndPunctuation + textField.textContentType = .postalCode + case .country: + textField.keyboardType = .default + // Don't set textContentType for Country, because we don't want iOS to skip the UIPickerView for input + textField.inputView = countryPickerView + // If we're being set directly to a country we don't allow, add it to the allowed list + let countryCodes = self.countryCodes ?? [] + if let contents = contents, + !countryCodes.contains(contents) && NSLocale.isoCountryCodes.contains(contents) + { + self.countryCodes = countryCodes + [contents] + } + let index = countryCodes.firstIndex(of: contents ?? "") ?? NSNotFound + if index == NSNotFound { + textField.text = "" + } else { + countryPickerView?.selectRow(index, inComponent: 0, animated: false) + if let countryPickerView = countryPickerView { + textField.text = pickerView( + countryPickerView, + titleForRow: index, + forComponent: 0 + ) + } + } + textField.validText = validContents + case .phone: + self.textField.keyboardType = .numbersAndPunctuation + self.textField.textContentType = .telephoneNumber + let behavior: STPFormTextFieldAutoFormattingBehavior = + (self.countryCodeIsUnitedStates ? .phoneNumbers : .none) + (self.textField as? STPFormTextField)?.autoFormattingBehavior = behavior + case .email: + self.textField.keyboardType = .emailAddress + self.textField.textContentType = .emailAddress + } + + if !self.lastInList { + self.textField.inputAccessoryView = self.inputAccessoryToolbar + } else { + self.textField.inputAccessoryView = nil + } + self.textField.accessibilityLabel = self.textField.placeholder + self.textField.accessibilityIdentifier = self.accessibilityIdentifierForAddressField( + type: self.type + ) + } + + func accessibilityIdentifierForAddressField(type: STPAddressFieldType) -> String { + switch type { + case .name: + return "ShippingAddressFieldTypeNameIdentifier" + case .line1: + return "ShippingAddressFieldTypeLine1Identifier" + case .line2: + return "ShippingAddressFieldTypeLine2Identifier" + case .city: + return "ShippingAddressFieldTypeCityIdentifier" + case .state: + return "ShippingAddressFieldTypeStateIdentifier" + case .zip: + return "ShippingAddressFieldTypeZipIdentifier" + case .country: + return "ShippingAddressFieldTypeCountryIdentifier" + case .email: + return "ShippingAddressFieldTypeEmailIdentifier" + case .phone: + return "ShippingAddressFieldTypePhoneIdentifier" + } + } + + func stateFieldCaption(forCountryCode countryCode: String?) -> String { + return StripeSharedStrings.localizedStateString(for: countryCode) + } + + func placeholder(for addressFieldType: STPAddressFieldType) -> String { + switch addressFieldType { + case .name: + return String.Localized.name + case .line1: + return String.Localized.address + case .line2: + return STPLocalizedString( + "Apt.", + "Caption for Apartment/Address line 2 field on address form" + ) + case .city: + return String.Localized.city + case .state: + return stateFieldCaption(forCountryCode: self.ourCountryCode) + case .zip: + return StripeSharedStrings.localizedPostalCodeString(for: self.ourCountryCode) + case .country: + return String.Localized.country + case .email: + return String.Localized.email + case .phone: + return String.Localized.phone + } + } + + func delegateCountryCodeDidChange(countryCode: String) { + if self.type == .country { + self.contents = countryCode + } + + self.ourCountryCode = countryCode + self.updateTextFieldsAndCaptions() + self.setNeedsLayout() + } + + func updateAppearance() { + self.backgroundColor = self.theme.secondaryBackgroundColor + self.contentView.backgroundColor = .clear + self.textField.placeholderColor = theme.tertiaryForegroundColor + self.textField.defaultColor = theme.primaryForegroundColor + self.textField.errorColor = self.theme.errorColor + self.textField.font = self.theme.font + self.setNeedsLayout() + } + + var countryCodeIsUnitedStates: Bool { + self.ourCountryCode == "US" + } + + public override func becomeFirstResponder() -> Bool { + return self.textField.becomeFirstResponder() + } + + @objc func nextTapped(sender: NSObject) { + delegate?.addressFieldTableViewCellDidReturn(self) + } + + @objc + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.delegate?.addressFieldTableViewCellDidReturn(self) + return false + } + + @objc + public func textFieldDidEndEditing(_ textField: UITextField) { + (textField as? STPFormTextField)?.validText = validContents + self.delegate?.addressFieldTableViewCellDidEndEditing(self) + } + + public override func accessibilityElementCount() -> Int { + return 1 + } + + public override func accessibilityElement(at index: Int) -> Any? { + return textField + } + + public override func index(ofAccessibilityElement element: Any) -> Int { + return 0 + } + + @objc func textFieldTextDidChange(textField: STPValidatedTextField) { + if self.type != .country { + _contents = textField.text + if textField.isFirstResponder { + textField.validText = self.potentiallyValidContents + } else { + textField.validText = self.validContents + } + } + self.delegate?.addressFieldTableViewCellDidUpdateText(self) + } + + // pragma mark - UITextFieldDelegate + + var validContents: Bool { + switch self.type { + case .name, .line1, .city, .state, .country: + return self.contents?.count ?? 0 > 0 + case .line2: + return true + case .zip: + return STPPostalCodeValidator.validationState( + forPostalCode: self.contents, + countryCode: self.ourCountryCode + ) == .valid + case .email: + return STPEmailAddressValidator.stringIsValidEmailAddress(self.contents) + case .phone: + return STPPhoneNumberValidator.stringIsValidPhoneNumber( + self.contents ?? "", + forCountryCode: self.ourCountryCode + ) + } + } + + var potentiallyValidContents: Bool { + switch self.type { + case .name, .line1, .city, .state, .country, .line2, .phone: + return true + case .zip: + let validationState = STPPostalCodeValidator.validationState( + forPostalCode: self.contents, + countryCode: self.ourCountryCode + ) + return validationState == .valid || validationState == .incomplete + case .email: + return STPEmailAddressValidator.stringIsValidPartialEmailAddress(self.contents) + } + } + + public func pickerView( + _ pickerView: UIPickerView, + didSelectRow row: Int, + inComponent component: Int + ) { + guard let countryCode = self.countryCodes?[row] as? String + else { + return + } + self.ourCountryCode = countryCode + self.contents = self.ourCountryCode + textField.text = self.pickerView(pickerView, titleForRow: row, forComponent: component) + // UIControlEvent not fired for programmatic changes + self.textFieldTextDidChange(textField: textField) + self.delegate?.addressFieldTableViewCountryCode = self.ourCountryCode ?? "" + + } + + public func pickerView( + _ pickerView: UIPickerView, + titleForRow row: Int, + forComponent component: Int + ) -> String? { + guard let countryCode = self.countryCodes?[row] as? String else { + return nil + } + let identifier = Locale.identifier(fromComponents: [ + NSLocale.Key.countryCode.rawValue: countryCode + ]) + return (NSLocale.autoupdatingCurrent as NSLocale).displayName( + forKey: NSLocale.Key(rawValue: NSLocale.Key.identifier.rawValue), + value: identifier + ) + } + + public func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + public func pickerView( + _ pickerView: UIPickerView, + numberOfRowsInComponent component: Int + ) + -> Int + { + self.countryCodes?.count ?? 0 + } + + required convenience init?( + coder aDecoder: NSCoder + ) { + assertionFailure("Use initWithType: instead.") + self.init( + type: .name, + contents: nil, + lastInList: false, + delegate: nil + ) + } + +} diff --git a/Stripe/StripeiOS/Source/STPAddressViewModel.swift b/Stripe/StripeiOS/Source/STPAddressViewModel.swift new file mode 100644 index 00000000..aa3fa68b --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAddressViewModel.swift @@ -0,0 +1,434 @@ +// +// STPAddressViewModel.swift +// StripeiOS +// +// Created by Jack Flintermann on 4/21/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Contacts +import CoreLocation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +import UIKit + +protocol STPAddressViewModelDelegate: AnyObject { + func addressViewModelDidChange(_ addressViewModel: STPAddressViewModel) + func addressViewModel(_ addressViewModel: STPAddressViewModel, addedCellAt index: Int) + func addressViewModel(_ addressViewModel: STPAddressViewModel, removedCellAt index: Int) + func addressViewModelWillUpdate(_ addressViewModel: STPAddressViewModel) + func addressViewModelDidUpdate(_ addressViewModel: STPAddressViewModel) +} + +class STPAddressViewModel: STPAddressFieldTableViewCellDelegate { + private(set) var addressCells: [STPAddressFieldTableViewCell] = [] + weak var delegate: STPAddressViewModelDelegate? + + var addressFieldTableViewCountryCode: String? = Locale.autoupdatingCurrent.regionCode { + didSet { + updatePostalCodeCellIfNecessary() + if let addressFieldTableViewCountryCode = addressFieldTableViewCountryCode { + for cell in addressCells { + cell.delegateCountryCodeDidChange(countryCode: addressFieldTableViewCountryCode) + } + } + } + } + + var address: STPAddress { + get { + let address = STPAddress() + for cell in addressCells { + + switch cell.type { + case .name: + address.name = cell.contents + case .line1: + address.line1 = cell.contents + case .line2: + address.line2 = cell.contents + case .city: + address.city = cell.contents + case .state: + address.state = cell.contents + case .zip: + address.postalCode = cell.contents + case .country: + address.country = cell.contents + case .email: + address.email = cell.contents + case .phone: + address.phone = cell.contents + } + } + // Prefer to use the contents of STPAddressFieldTypeCountry, but fallback to + // `addressFieldTableViewCountryCode` if nil (important for STPBillingAddressFieldsPostalCode) + address.country = address.country ?? addressFieldTableViewCountryCode + return address + } + set(address) { + if let country = address.country { + addressFieldTableViewCountryCode = country + } + + for cell in addressCells { + switch cell.type { + case .name: + cell.contents = address.name + case .line1: + cell.contents = address.line1 + case .line2: + cell.contents = address.line2 + case .city: + cell.contents = address.city + case .state: + cell.contents = address.state + case .zip: + cell.contents = address.postalCode + case .country: + cell.contents = address.country + case .email: + cell.contents = address.email + case .phone: + cell.contents = address.phone + } + } + } + } + + // The default value of availableCountries is nil, which will allow all known countries. + var availableCountries: Set? + + var isValid: Bool { + if isBillingAddress { + // The AddressViewModel is only for address fields. + // Determining whether the postal code is present is up to the + // STPCardTextFieldViewModel. + if requiredBillingAddressFields == .postalCode { + return true + } else { + return address.containsRequiredFields(requiredBillingAddressFields) + } + + } else { + if let requiredShippingAddressFields = requiredShippingAddressFields { + return address.containsRequiredShippingAddressFields(requiredShippingAddressFields) + } + return false + } + } + + // The default value of availableCountries is nil, which will allow all known countries. + init( + requiredBillingFields requiredBillingAddressFields: STPBillingAddressFields, + availableCountries: Set? = nil + ) { + isBillingAddress = true + self.availableCountries = availableCountries + self.requiredBillingAddressFields = requiredBillingAddressFields + switch requiredBillingAddressFields { + case .none: + addressCells = [] + case .zip, .postalCode: + addressCells = [] // Postal code cell will be added later if necessary + case .full: + addressCells = [ + STPAddressFieldTableViewCell( + type: .name, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .line1, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .line2, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .country, + contents: addressFieldTableViewCountryCode, + lastInList: false, + delegate: self + ), + // Postal code cell will be added here later if necessary + STPAddressFieldTableViewCell( + type: .city, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .state, + contents: "", + lastInList: true, + delegate: self + ), + ] + case .name: + addressCells = [ + STPAddressFieldTableViewCell( + type: .name, + contents: "", + lastInList: true, + delegate: self + ), + ] + default: + fatalError() + } + commonInit() + } + + init( + requiredShippingFields requiredShippingAddressFields: Set, + availableCountries: Set? = nil + ) { + isBillingAddress = false + self.availableCountries = availableCountries + self.requiredShippingAddressFields = requiredShippingAddressFields + var cells: [STPAddressFieldTableViewCell] = [] + + if requiredShippingAddressFields.contains(STPContactField.name) { + cells.append( + STPAddressFieldTableViewCell( + type: .name, + contents: "", + lastInList: false, + delegate: self + ) + ) + } + if requiredShippingAddressFields.contains(.emailAddress) { + cells.append( + STPAddressFieldTableViewCell( + type: .email, + contents: "", + lastInList: false, + delegate: self + ) + ) + } + if requiredShippingAddressFields.contains(STPContactField.postalAddress) { + var postalCells = [ + STPAddressFieldTableViewCell( + type: .name, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .line1, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .line2, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .country, + contents: addressFieldTableViewCountryCode, + lastInList: false, + delegate: self + ), + // Postal code cell will be added here later if necessary + STPAddressFieldTableViewCell( + type: .city, + contents: "", + lastInList: false, + delegate: self + ), + STPAddressFieldTableViewCell( + type: .state, + contents: "", + lastInList: false, + delegate: self + ), + ] + if requiredShippingAddressFields.contains(.name) { + postalCells.remove(at: 0) + } + cells.append(contentsOf: postalCells.compactMap { $0 }) + } + if requiredShippingAddressFields.contains(.phoneNumber) { + cells.append( + STPAddressFieldTableViewCell( + type: .phone, + contents: "", + lastInList: false, + delegate: self + ) + ) + } + if let lastCell = cells.last { + lastCell.lastInList = true + } + addressCells = cells + commonInit() + } + + private func cell(at index: Int) -> STPAddressFieldTableViewCell? { + guard index > 0, + index < addressCells.count + else { + return nil + } + return addressCells[index] + } + + private var isBillingAddress = false + private var requiredBillingAddressFields: STPBillingAddressFields = .none + private var requiredShippingAddressFields: Set? + private var showingPostalCodeCell = false + private var geocodeInProgress = false + + private func commonInit() { + if let countryCode = Locale.autoupdatingCurrent.regionCode { + addressFieldTableViewCountryCode = countryCode + } + updatePostalCodeCellIfNecessary() + } + + private func updatePostalCodeCellIfNecessary() { + delegate?.addressViewModelWillUpdate(self) + let shouldBeShowingPostalCode = STPPostalCodeValidator.postalCodeIsRequired( + forCountryCode: addressFieldTableViewCountryCode + ) + + if shouldBeShowingPostalCode && !showingPostalCodeCell { + if containsStateAndPostalFields() { + // Add before city + let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .city }) ?? 0 + + var mutableAddressCells = addressCells + mutableAddressCells.insert( + STPAddressFieldTableViewCell( + type: .zip, + contents: "", + lastInList: false, + delegate: self + ), + at: zipFieldIndex + ) + addressCells = mutableAddressCells + delegate?.addressViewModel(self, addedCellAt: zipFieldIndex) + delegate?.addressViewModelDidChange(self) + } + } else if !shouldBeShowingPostalCode && showingPostalCodeCell { + if containsStateAndPostalFields() { + if let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .zip }) { + + var mutableAddressCells = addressCells + mutableAddressCells.remove(at: zipFieldIndex) + addressCells = mutableAddressCells + delegate?.addressViewModel(self, removedCellAt: zipFieldIndex) + delegate?.addressViewModelDidChange(self) + } + } + } + showingPostalCodeCell = shouldBeShowingPostalCode + delegate?.addressViewModelDidUpdate(self) + } + + private func containsStateAndPostalFields() -> Bool { + if isBillingAddress { + return requiredBillingAddressFields == .full + } else { + return requiredShippingAddressFields?.contains(.postalAddress) ?? false + } + } + + func updateCityAndState(fromZipCodeCell zipCell: STPAddressFieldTableViewCell?) { + + let zipCode = zipCell?.contents + + if geocodeInProgress || zipCode == nil || !(zipCell?.textField.validText ?? false) + || !(addressFieldTableViewCountryCode == "US") + { + return + } + + var cityCell: STPAddressFieldTableViewCell? + var stateCell: STPAddressFieldTableViewCell? + for cell in addressCells { + if cell.type == .city { + cityCell = cell + } else if cell.type == .state { + stateCell = cell + } + } + + if (cityCell == nil && stateCell == nil) + || ((cityCell?.contents?.count ?? 0) > 0 || (stateCell?.contents?.count ?? 0) > 0) + { + // Don't auto fill if either have text already + // Or if neither are non-nil + return + } else { + geocodeInProgress = true + let geocoder = CLGeocoder() + + let onCompletion: CLGeocodeCompletionHandler = { placemarks, error in + stpDispatchToMainThreadIfNecessary({ + if (placemarks?.count ?? 0) > 0 && error == nil { + let placemark = placemarks?.first + if (cityCell?.contents?.count ?? 0) == 0 + && (stateCell?.contents?.count ?? 0) == 0 + && (zipCell?.contents == zipCode) + { + // Check contents again to make sure they're still empty + // And that zipcode hasn't changed to something else + cityCell?.contents = placemark?.locality + stateCell?.contents = placemark?.administrativeArea + } + } + self.geocodeInProgress = false + }) + } + + let address = CNMutablePostalAddress() + address.postalCode = zipCode ?? "" + address.isoCountryCode = addressFieldTableViewCountryCode ?? "" + + geocoder.geocodePostalAddress( + address, + completionHandler: onCompletion + ) + } + } + + private func cell(after cell: STPAddressFieldTableViewCell?) -> STPAddressFieldTableViewCell? { + guard let cell = cell, + let cellIndex = addressCells.firstIndex(of: cell), + cellIndex + 1 < addressCells.count + else { + return nil + } + return addressCells[cellIndex + 1] + } + + func addressFieldTableViewCellDidUpdateText(_ cell: STPAddressFieldTableViewCell) { + delegate?.addressViewModelDidChange(self) + } + + func addressFieldTableViewCellDidReturn(_ cell: STPAddressFieldTableViewCell) { + _ = self.cell(after: cell)?.becomeFirstResponder() + } + + func addressFieldTableViewCellDidEndEditing(_ cell: STPAddressFieldTableViewCell) { + if cell.type == .zip { + updateCityAndState(fromZipCodeCell: cell) + } + } + +} diff --git a/Stripe/StripeiOS/Source/STPAnalyticsClient+BasicUI.swift b/Stripe/StripeiOS/Source/STPAnalyticsClient+BasicUI.swift new file mode 100644 index 00000000..809037ab --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAnalyticsClient+BasicUI.swift @@ -0,0 +1,160 @@ +// +// STPAnalyticsClient+BasicUI.swift +// StripeiOS +// +// Created by Yuki Tokuhiro on 1/24/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments + +extension STPPaymentContext { + final class AnalyticsLogger { + let analyticsClient = STPAnalyticsClient.sharedClient + var apiClient: STPAPIClient = .shared + var product: String + lazy var commonParameters: [String: Any] = { + [ + "product": product, + ] + }() + + init(product: T.Type) { + self.product = product.stp_analyticsIdentifier + } + + // MARK: - Loading + + func logLoadStarted() { + log(event: .biLoadStarted, params: [:]) + } + + func logLoadSucceeded(loadStartDate: Date, defaultPaymentOption: STPPaymentOption?) { + let event: STPAnalyticEvent = .biLoadSucceeded + let duration = Date().timeIntervalSince(loadStartDate) + let defaultPaymentMethod: String = { + guard let defaultPaymentOption else { + return "none" + } + switch defaultPaymentOption { + case is STPApplePayPaymentOption: + return "apple_pay" + case let defaultPaymentMethod as STPPaymentMethod: + return defaultPaymentMethod.type.identifier + default: + assertionFailure() + return "unknown" + } + }() + let params: [String: Any] = [ + "duration": duration, + "selected_lpm": defaultPaymentMethod, + ] + log(event: event, params: params) + } + + func logLoadFailed(loadStartDate: Date, error: Error) { + let event: STPAnalyticEvent = .biLoadFailed + let duration = Date().timeIntervalSince(loadStartDate) + var params: [String: Any] = [ + "duration": duration, + ] + params.mergeAssertingOnOverwrites(error.serializeForV1Analytics()) + log(event: event, params: params) + } + + // MARK: - Payment + + func logPayment(status: STPPaymentStatus, loadStartDate: Date?, paymentOption: STPPaymentOption, error: Error?) { + let didSucceed: Bool + switch status { + case .userCancellation: + // Don't send analytic for cancels + return + case .success: + didSucceed = true + case .error: + didSucceed = false + @unknown default: + return + } + + let event: STPAnalyticEvent + let paymentMethodType: String + switch paymentOption { + case let paymentMethod as STPPaymentMethod: + paymentMethodType = paymentMethod.type.identifier + event = didSucceed ? .biPaymentCompleteSavedPMSuccess : .biPaymentCompleteSavedPMFailure + case let params as STPPaymentMethodParams: + paymentMethodType = params.type.identifier + event = didSucceed ? .biPaymentCompleteNewPMSuccess : .biPaymentCompleteNewPMFailure + case is STPApplePayPaymentOption: + paymentMethodType = "apple_pay" + event = didSucceed ? .biPaymentCompleteApplePaySuccess : .biPaymentCompleteApplePayFailure + default: + assertionFailure("Unknown payment option!") + return + } + + var params: [String: Any] = ["selected_lpm": paymentMethodType] + if let error { + params.mergeAssertingOnOverwrites(error.serializeForV1Analytics()) + } + if let loadStartDate { + params["duration"] = Date().timeIntervalSince(loadStartDate) + } + + log(event: event, params: params) + } + + // MARK: - UI + + func logPaymentOptionsScreenAppeared() { + log(event: .biOptionsShown, params: [:]) + } + + func logFormShown(paymentMethodType: STPPaymentMethodType) { + let event = STPAnalyticEvent.biFormShown + let params = ["selected_lpm": paymentMethodType] + log(event: event, params: params) + } + + /// - Parameter shownStartDate: The date when the form was first shown. This should never be nil. + func logDoneButtonTapped(paymentMethodType: STPPaymentMethodType, shownStartDate: Date?) { + let event = STPAnalyticEvent.biDoneButtonTapped + + var params: [String: Any] = [ + "selected_lpm": paymentMethodType, + ] + if let shownStartDate { + let duration = Date().timeIntervalSince(shownStartDate) + params["duration"] = duration + } else if NSClassFromString("XCTest") == nil { + assertionFailure("Shown start date should never be nil!") + } + + log(event: event, params: params) + } + + func logFormInteracted(paymentMethodType: STPPaymentMethodType) { + log(event: .biFormInteracted, params: [ + "selected_lpm": paymentMethodType, + ]) + } + + func logCardNumberCompleted() { + log(event: .biCardNumberCompleted, params: [:]) + } + + // MARK: - Helpers + + private func log(event: STPAnalyticEvent, params: [String: Any]) { + let analytic = GenericAnalytic(event: event, params: params.merging(commonParameters, uniquingKeysWith: { new, _ in + assertionFailure("Constructing analytics parameters with duplicate keys") + return new + })) + analyticsClient.log(analytic: analytic, apiClient: apiClient) + } + } +} diff --git a/Stripe/StripeiOS/Source/STPAnalyticsClient+Payments.swift b/Stripe/StripeiOS/Source/STPAnalyticsClient+Payments.swift new file mode 100644 index 00000000..6706b95d --- /dev/null +++ b/Stripe/StripeiOS/Source/STPAnalyticsClient+Payments.swift @@ -0,0 +1,27 @@ +// +// STPAnalyticsClient+Payments.swift +// StripeiOS +// +// Created by David Estes on 1/24/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +/// An analytic specific to payments that serializes payment-specific +/// information into its params. +@_spi(STP) public protocol PaymentAnalytic: Analytic { + var additionalParams: [String: Any] { get } +} + +@_spi(STP) extension PaymentAnalytic { + public var params: [String: Any] { + var params = additionalParams + + params["apple_pay_enabled"] = NSNumber(value: StripeAPI.deviceSupportsApplePay()) + params["ocr_type"] = PaymentsSDKVariant.ocrTypeString + params["pay_var"] = PaymentsSDKVariant.variant + return params + } +} diff --git a/Stripe/StripeiOS/Source/STPApplePayContextDelegate.swift b/Stripe/StripeiOS/Source/STPApplePayContextDelegate.swift new file mode 100644 index 00000000..0814a5de --- /dev/null +++ b/Stripe/StripeiOS/Source/STPApplePayContextDelegate.swift @@ -0,0 +1,98 @@ +// +// STPApplePayContextDelegate.swift +// StripeiOS +// +// Created by David Estes on 9/15/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeApplePay +@_spi(STP) import StripeCore + +/// Implement the required methods of this delegate to supply a PaymentIntent to STPApplePayContext and be notified of the completion of the Apple Pay payment. +/// You may also implement the optional delegate methods to handle shipping methods and shipping address changes e.g. to verify you can ship to the address, or update the payment amount. +@objc public protocol STPApplePayContextDelegate: _stpinternal_STPApplePayContextDelegateBase { + /// Called after the customer has authorized Apple Pay. Implement this method to call the completion block with the client secret of a PaymentIntent or SetupIntent. + /// - Parameters: + /// - paymentMethod: The PaymentMethod that represents the customer's Apple Pay payment method. + /// If you create the PaymentIntent with confirmation_method=manual, pass `paymentMethod.stripeId` as the payment_method and confirm=true. Otherwise, you can ignore this parameter. + /// - paymentInformation: The underlying PKPayment created by Apple Pay. + /// If you create the PaymentIntent with confirmation_method=manual, you can collect shipping information using its `shippingContact` and `shippingMethod` properties. + /// - completion: Call this with the PaymentIntent or SetupIntent client secret, or the error that occurred creating the PaymentIntent or SetupIntent. + @objc(applePayContext:didCreatePaymentMethod:paymentInformation:completion:) + func applePayContext( + _ context: STPApplePayContext, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + paymentInformation: PKPayment, + completion: @escaping STPIntentClientSecretCompletionBlock + ) + + /// Called after the Apple Pay sheet is dismissed with the result of the payment. + /// Your implementation could stop a spinner and display a receipt view or error to the customer, for example. + /// - Parameters: + /// - status: The status of the payment + /// - error: The error that occurred, if any. + @objc(applePayContext:didCompleteWithStatus:error:) + func applePayContext( + _ context: STPApplePayContext, + didCompleteWith status: STPPaymentStatus, + error: Error? + ) +} + +/// A helper class used to bridge StripeApplePay.framework with the legacy Stripe.framework objects. +@objc(STPApplePayContextLegacyHelper) +class STPApplePayContextLegacyHelper: NSObject { + @objc class func performDidCreatePaymentMethod( + _ storage: _stpinternal_ApplePayContextDidCreatePaymentMethodStorage + ) { + let delegate = storage.delegate as! STPApplePayContextDelegate + // Convert the PaymentMethod to an STPPaymentMethod: + guard + let stpPaymentMethod = STPPaymentMethod.decodedObject( + fromAPIResponse: storage.paymentMethod.allResponseFields + ) + else { + assertionFailure("Failed to convert PaymentMethod to STPPaymentMethod") + return + } + delegate.applePayContext( + storage.context, + didCreatePaymentMethod: stpPaymentMethod, + paymentInformation: storage.paymentInformation, + completion: storage.completion + ) + } + + @objc class func performDidComplete(_ storage: _stpinternal_ApplePayContextDidCompleteStorage) { + let delegate = storage.delegate as! STPApplePayContextDelegate + let stpStatus = STPPaymentStatus(applePayStatus: storage.status) + + // If this is a modern API error, convert it down to a legacy STPError. + // This is to avoid changing the API experience for users. + // We can re-evaluate this as we release more of the modern API. + if let modernError = storage.error as? StripeError { + storage.error = NSError.stp_error(from: modernError) + } + + delegate.applePayContext(storage.context, didCompleteWith: stpStatus, error: storage.error) + } + +} + +extension STPPaymentStatus { + init( + applePayStatus: STPApplePayContext.PaymentStatus + ) { + switch applePayStatus { + case .success: + self = .success + case .error: + self = .error + case .userCancellation: + self = .userCancellation + } + } +} diff --git a/Stripe/StripeiOS/Source/STPApplePayPaymentOption.swift b/Stripe/StripeiOS/Source/STPApplePayPaymentOption.swift new file mode 100644 index 00000000..14edba1f --- /dev/null +++ b/Stripe/StripeiOS/Source/STPApplePayPaymentOption.swift @@ -0,0 +1,51 @@ +// +// STPApplePayPaymentOption.swift +// StripeiOS +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +import UIKit + +/// An empty class representing that the user wishes to pay via Apple Pay. This can +/// be checked on an `STPPaymentContext`, e.g: +/// ``` +/// if paymentContext.selectedPaymentOption is STPApplePayPaymentOption { +/// // Don't ask the user for their card number; they want to pay with apple pay. +/// } +/// ``` +@objc public class STPApplePayPaymentOption: NSObject, STPPaymentOption { + // MARK: - STPPaymentOption + @objc public var image: UIImage { + return STPImageLibrary.applePayCardImage() + } + + @objc public var templateImage: UIImage { + // No template for Apple Pay + return STPImageLibrary.applePayCardImage() + } + + @objc public var label: String { + return String.Localized.apple_pay + } + + @objc public var isReusable: Bool { + return true + } + + // MARK: - Equality + /// :nodoc: + @objc + public override func isEqual(_ object: Any?) -> Bool { + return object is STPApplePayPaymentOption + } + + /// :nodoc: + @objc public override var hash: Int { + return NSStringFromClass(STPApplePayPaymentOption.self).hash + } +} diff --git a/Stripe/StripeiOS/Source/STPBackendAPIAdapter.swift b/Stripe/StripeiOS/Source/STPBackendAPIAdapter.swift new file mode 100644 index 00000000..2cd6ed58 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPBackendAPIAdapter.swift @@ -0,0 +1,86 @@ +// +// STPBackendAPIAdapter.swift +// StripeiOS +// +// Created by Jack Flintermann on 1/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +import UIKit + +/// A "bridge" from our pre-built UI (`STPPaymentContext`, `STPPaymentOptionsViewController`) +/// to your backend to fetch Customer-related information needed to power those views. +/// Typically, you will not need to implement this protocol yourself. You +/// should instead use `STPCustomerContext`, which implements +/// and manages retrieving and updating a Stripe customer for you. +/// - seealso: STPCustomerContext.h +/// If you would prefer retrieving and updating your Stripe customer object via +/// your own backend instead of using `STPCustomerContext`, you should make your +/// application's API client conform to this interface. +@objc public protocol STPBackendAPIAdapter: NSObjectProtocol { + /// Retrieve the customer to be displayed inside a payment context. + /// If you are not using STPCustomerContext: + /// On your backend, retrieve the Stripe customer associated with your currently + /// logged-in user ( https://stripe.com/docs/api#retrieve_customer ), and return + /// the raw JSON response from the Stripe API. Back in your iOS app, after you've + /// called this API, deserialize your API response into an `STPCustomer` object + /// (you can use the `STPCustomerDeserializer` class to do this). + /// - seealso: STPCard + /// - Parameter completion: call this callback when you're done fetching and parsing the above information from your backend. For example, `completion(customer, nil)` (if your call succeeds) or `completion(nil, error)` if an error is returned. + func retrieveCustomer(_ completion: STPCustomerCompletionBlock?) + /// Retrieves a list of Payment Methods attached to a customer. + /// If you are implementing your own : + /// Call the list method ( https://stripe.com/docs/api/payment_methods/list ) + /// with the Stripe customer. If this API call succeeds, call `completion(paymentMethods)` + /// with the list of PaymentMethods. Otherwise, call `completion(error)` with the error + /// that occurred. + /// - Parameter completion: Call this callback with the list of Payment Methods attached to the + /// customer. For example, `completion(paymentMethods)` (if your call succeeds) or + /// `completion(error)` if an error is returned. + func listPaymentMethodsForCustomer(completion: STPPaymentMethodsCompletionBlock?) + /// Adds a Payment Method to a customer. + /// If you are implementing your own : + /// On your backend, retrieve the Stripe customer associated with your logged-in user. + /// Then, call the Attach method on the Payment Method with that customer's ID + /// ( https://stripe.com/docs/api/payment_methods/attach ). If this API call succeeds, + /// call `completion(nil)`. Otherwise, call `completion(error)` with the error that + /// occurred. + /// - Parameters: + /// - paymentMethod: A valid Payment Method + /// - completion: Call this callback when you're done adding the payment method + /// to the customer on your backend. For example, `completion(nil)` (if your call succeeds) + /// or `completion(error)` if an error is returned. + func attachPaymentMethod(toCustomer paymentMethod: STPPaymentMethod, completion: STPErrorBlock?) + + /// Deletes the given Payment Method from the customer. + /// If you are implementing your own : + /// Call the Detach method ( https://stripe.com/docs/api/payment_methods/detach ) + /// on the Payment Method. If this API call succeeds, call `completion(nil)`. + /// Otherwise, call `completion(error)` with the error that occurred. + /// - Parameters: + /// - paymentMethod: The Payment Method to delete from the customer + /// - completion: Call this callback when you're done deleting the Payment Method + /// from the customer on your backend. For example, `completion(nil)` (if your call + /// succeeds) or `completion(error)` if an error is returned. + @objc optional func detachPaymentMethod( + fromCustomer paymentMethod: STPPaymentMethod, + completion: STPErrorBlock? + ) + /// Sets the given shipping address on the customer. + /// If you are implementing your own : + /// On your backend, retrieve the Stripe customer associated with your logged-in user. + /// Then, call the Customer Update method ( https://stripe.com/docs/api#update_customer ) + /// specifying shipping to be the given shipping address. If this API call succeeds, + /// call `completion(nil)`. Otherwise, call `completion(error)` with the error that occurred. + /// - Parameters: + /// - shipping: The shipping address to set on the customer + /// - completion: call this callback when you're done updating the customer on + /// your backend. For example, `completion(nil)` (if your call succeeds) or + /// `completion(error)` if an error is returned. + /// - seealso: https://stripe.com/docs/api#update_customer + @objc optional func updateCustomer( + withShippingAddress shipping: STPAddress, + completion: STPErrorBlock? + ) +} diff --git a/Stripe/StripeiOS/Source/STPBankSelectionTableViewCell.swift b/Stripe/StripeiOS/Source/STPBankSelectionTableViewCell.swift new file mode 100644 index 00000000..6d7a730d --- /dev/null +++ b/Stripe/StripeiOS/Source/STPBankSelectionTableViewCell.swift @@ -0,0 +1,124 @@ +// +// STPBankSelectionTableViewCell.swift +// StripeiOS +// +// Created by David Estes on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import UIKit + +class STPBankSelectionTableViewCell: UITableViewCell { + func configure( + withBank bankBrand: STPFPXBankBrand, + theme: STPTheme, + selected: Bool, + offline: Bool, + enabled: Bool + ) { + bank = bankBrand + self.theme = theme + + backgroundColor = theme.secondaryBackgroundColor + + // Left icon + leftIcon?.image = STPLegacyImageLibrary.fpxBrandImage(for: bank) + leftIcon?.tintColor = primaryColorForPaymentOption(withSelected: selected, enabled: enabled) + + // Title label + titleLabel?.font = theme.font + titleLabel?.text = STPFPXBank.stringFrom(bank) + if offline { + let format = STPLocalizedString( + "%@ - Offline", + "Bank name when bank is offline for maintenance." + ) + titleLabel?.text = String(format: format, STPFPXBank.stringFrom(bank) ?? "") + } + titleLabel?.textColor = primaryColorForPaymentOption( + withSelected: isSelected, + enabled: enabled + ) + + // Loading indicator + activityIndicator?.tintColor = theme.accentColor + if selected { + activityIndicator?.startAnimating() + } else { + activityIndicator?.stopAnimating() + } + + setNeedsLayout() + } + + private var bank: STPFPXBankBrand! + private var theme: STPTheme = .defaultTheme + private var leftIcon: UIImageView? + private var titleLabel: UILabel? + private var activityIndicator: UIActivityIndicatorView? + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + // Left icon + let leftIcon = UIImageView() + self.leftIcon = leftIcon + contentView.addSubview(leftIcon) + + // Title label + let titleLabel = UILabel() + self.titleLabel = titleLabel + contentView.addSubview(titleLabel) + + // Loading indicator + let activityIndicator = UIActivityIndicatorView(style: .medium) + self.activityIndicator = activityIndicator + self.activityIndicator?.hidesWhenStopped = true + contentView.addSubview(activityIndicator) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let midY = bounds.midY + let padding: CGFloat = 15.0 + let iconWidth: CGFloat = 26.0 + + // Left icon + leftIcon?.sizeToFit() + leftIcon?.center = CGPoint(x: padding + (iconWidth / 2.0), y: midY) + + // Activity indicator + activityIndicator?.center = CGPoint( + x: bounds.width - padding - (activityIndicator?.bounds.midX ?? 0.0), + y: midY + ) + + // Title label + var labelFrame = bounds + // not every icon is `iconWidth` wide, but give them all the same amount of space: + labelFrame.origin.x = padding + iconWidth + padding + labelFrame.size.width = + (activityIndicator?.frame.minX ?? 0.0) - padding - labelFrame.origin.x + titleLabel?.frame = labelFrame + } + + func primaryColorForPaymentOption(withSelected selected: Bool, enabled: Bool) -> UIColor { + if selected { + return theme.accentColor + } else { + return + (enabled + ? theme.primaryForegroundColor + : theme.primaryForegroundColor.withAlphaComponent(0.6)) + } + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPBankSelectionViewController.swift b/Stripe/StripeiOS/Source/STPBankSelectionViewController.swift new file mode 100644 index 00000000..b6fb8cdd --- /dev/null +++ b/Stripe/StripeiOS/Source/STPBankSelectionViewController.swift @@ -0,0 +1,300 @@ +// +// STPBankSelectionViewController.swift +// StripeiOS +// +// Created by David Estes on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import PassKit +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +import UIKit + +/// The payment methods supported by STPBankSelectionViewController. +@objc public enum STPBankSelectionMethod: Int { + /// FPX (Malaysia) + case FPX + /// An unknown payment method + case unknown +} + +/// This view controller displays a list of banks of the specified type, allowing the user to select one to pay from. +/// Once a bank is selected, it will return a PaymentMethodParams object, which you can use to confirm a PaymentIntent +/// or inspect to obtain details about the selected bank. +public class STPBankSelectionViewController: STPCoreTableViewController, UITableViewDataSource, + UITableViewDelegate +{ + /// A convenience initializer; equivalent to calling `init( bankMethod:bankMethod configuration:STPPaymentConfiguration.shared theme:STPTheme.defaultTheme`. + @objc + public convenience init( + bankMethod: STPBankSelectionMethod + ) { + self.init( + bankMethod: bankMethod, + configuration: STPPaymentConfiguration.shared, + theme: STPTheme.defaultTheme + ) + } + + @objc public convenience required init( + theme: STPTheme? + ) { + self.init( + bankMethod: .FPX, + configuration: STPPaymentConfiguration.shared, + theme: theme ?? .defaultTheme + ) + } + + /// Initializes a new `STPBankSelectionViewController` with the provided configuration and theme. Don't forget to set the `delegate` property after initialization. + /// - Parameters: + /// - bankMethod: The user will be presented with a list of banks for this payment method. STPBankSelectionMethodFPX is currently the only supported payment method. + /// - configuration: The configuration to use. This determines the Stripe publishable key to use when querying metadata about the banks. - seealso: STPPaymentConfiguration + /// - theme: The theme to use to inform the view controller's visual appearance. - seealso: STPTheme + @objc + public init( + bankMethod: STPBankSelectionMethod, + configuration: STPPaymentConfiguration, + theme: STPTheme + ) { + super.init(theme: theme) + STPAnalyticsClient.sharedClient.addClass( + toProductUsageIfNecessary: STPBankSelectionViewController.self + ) + assert(bankMethod == .FPX, "STPBankSelectionViewController currently only supports FPX.") + self.bankMethod = bankMethod + self.configuration = configuration + selectedBank = .unknown + apiClient = STPAPIClient.shared + if bankMethod == .FPX { + _refreshFPXStatus() + NotificationCenter.default.addObserver( + self, + selector: #selector(_refreshFPXStatus), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + title = String.Localized.bank_account + } + + /// The view controller's delegate. This must be set before showing the view controller in order for it to work properly. - seealso: STPBankSelectionViewControllerDelegate + @objc public weak var delegate: STPBankSelectionViewControllerDelegate? + + /// The API Client to use to make requests. + /// Defaults to `STPAPIClient.shared` + public var apiClient: STPAPIClient = .shared + + private var bankMethod: STPBankSelectionMethod = .unknown + private var selectedBank: STPFPXBankBrand = .unknown + private var configuration: STPPaymentConfiguration? + private weak var imageView: UIImageView? + private var headerView: STPSectionHeaderView? + private var loading = false + private var bankStatus: STPFPXBankStatusResponse? + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func _refreshFPXStatus() { + apiClient.retrieveFPXBankStatus(withCompletion: { bankStatusResponse, error in + if error == nil && bankStatusResponse != nil { + if let bankStatusResponse = bankStatusResponse { + self._update(withBankStatus: bankStatusResponse) + } + } + }) + } + + @objc override func createAndSetupViews() { + super.createAndSetupViews() + + tableView?.register( + STPBankSelectionTableViewCell.self, + forCellReuseIdentifier: STPBankSelectionCellReuseIdentifier + ) + + tableView?.dataSource = self + tableView?.delegate = self + tableView?.reloadData() + } + + @objc override func updateAppearance() { + super.updateAppearance() + + tableView?.reloadData() + } + + @objc override func useSystemBackButton() -> Bool { + return true + } + + func _update(withBankStatus bankStatusResponse: STPFPXBankStatusResponse) { + bankStatus = bankStatusResponse + + tableView?.beginUpdates() + if let indexPathsForVisibleRows = tableView?.indexPathsForVisibleRows { + tableView?.reloadRows(at: indexPathsForVisibleRows, with: .none) + } + tableView?.endUpdates() + } + + // MARK: - UITableView + + /// :nodoc: + @objc + public func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + /// :nodoc: + @objc + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return STPFPXBankBrand.unknown.rawValue + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + let cell = + tableView.dequeueReusableCell( + withIdentifier: STPBankSelectionCellReuseIdentifier, + for: indexPath + ) + as? STPBankSelectionTableViewCell + let bankBrand = STPFPXBankBrand(rawValue: indexPath.row) + let selected = selectedBank == bankBrand + var offline: Bool? + if let bankBrand = bankBrand { + offline = bankStatus != nil && !(bankStatus?.bankBrandIsOnline(bankBrand) ?? false) + } + if let bankBrand = bankBrand { + cell?.configure( + withBank: bankBrand, + theme: theme, + selected: selected, + offline: offline ?? false, + enabled: !loading + ) + } + return cell! + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let topRow = indexPath.row == 0 + let bottomRow = + self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 == indexPath.row + cell.stp_setBorderColor(theme.tertiaryBackgroundColor) + cell.stp_setTopBorderHidden(!topRow) + cell.stp_setBottomBorderHidden(!bottomRow) + cell.stp_setFakeSeparatorColor(theme.quaternaryBackgroundColor) + cell.stp_setFakeSeparatorLeftInset(15.0) + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + heightForFooterInSection section: Int + ) + -> CGFloat + { + return 27.0 + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + shouldHighlightRowAt indexPath: IndexPath + ) + -> Bool + { + return !loading + } + + /// :nodoc: + @objc + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if loading { + return // Don't allow user interaction if we're currently setting up a payment method + } + loading = true + tableView.deselectRow(at: indexPath, animated: true) + let bankIndex = indexPath.row + selectedBank = STPFPXBankBrand(rawValue: bankIndex) ?? .unknown + tableView.reloadSections( + NSIndexSet(index: indexPath.section) as IndexSet, + with: .none + ) + + let fpx = STPPaymentMethodFPXParams() + fpx.bank = STPFPXBankBrand(rawValue: bankIndex) ?? .unknown + // Create and return a Payment Method Params object + let paymentMethodParams = STPPaymentMethodParams( + fpx: fpx, + billingDetails: nil, + metadata: nil + ) + if delegate?.responds( + to: #selector( + STPBankSelectionViewControllerDelegate.bankSelectionViewController( + _: + didCreatePaymentMethodParams: + )) + ) ?? false { + delegate?.bankSelectionViewController( + self, + didCreatePaymentMethodParams: paymentMethodParams + ) + } + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } + + required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + fatalError("init(nibName:bundle:) has not been implemented") + } +} + +/// An `STPBankSelectionViewControllerDelegate` is notified when a user selects a bank. +@objc public protocol STPBankSelectionViewControllerDelegate: NSObjectProtocol { + /// This is called when the user selects a bank. + /// You can use the returned PaymentMethodParams to confirm a PaymentIntent, or inspect + /// it to obtain details about the selected bank. + /// Once you're done, you'll want to dismiss (or pop) the view controller. + /// - Parameters: + /// - bankViewController: the view controller that created the PaymentMethodParams + /// - paymentMethodParams: the PaymentMethodParams that was created. - seealso: STPPaymentMethodParams + @objc(bankSelectionViewController:didCreatePaymentMethodParams:) + func bankSelectionViewController( + _ bankViewController: STPBankSelectionViewController, + didCreatePaymentMethodParams paymentMethodParams: STPPaymentMethodParams + ) +} + +private let STPBankSelectionCellReuseIdentifier = "STPBankSelectionCellReuseIdentifier" + +/// :nodoc: +@_spi(STP) extension STPBankSelectionViewController: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier = "STPBankSelectionViewController" +} diff --git a/Stripe/StripeiOS/Source/STPBasicUIAnalyticsSerializer.swift b/Stripe/StripeiOS/Source/STPBasicUIAnalyticsSerializer.swift new file mode 100644 index 00000000..a0f9aaa1 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPBasicUIAnalyticsSerializer.swift @@ -0,0 +1,84 @@ +// +// STPAnalyticsClient+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments + +@objc(STPBasicUIAnalyticsSerializer) +class STPBasicUIAnalyticsSerializer: NSObject, STPAnalyticsSerializer { + static func serializeConfiguration( + _ configuration: NSObject + ) -> [String: + String] + { + var dictionary: [String: String] = [:] + dictionary["publishable_key"] = STPAPIClient.shared.publishableKey ?? "unknown" + + guard let configuration = configuration as? STPPaymentConfiguration else { + return dictionary + } + + if configuration.applePayEnabled && !configuration.fpxEnabled { + dictionary["additional_payment_methods"] = "default" + } else if !configuration.applePayEnabled && !configuration.fpxEnabled { + dictionary["additional_payment_methods"] = "none" + } else if !configuration.applePayEnabled && configuration.fpxEnabled { + dictionary["additional_payment_methods"] = "fpx" + } else if configuration.applePayEnabled && configuration.fpxEnabled { + dictionary["additional_payment_methods"] = "applepay,fpx" + } + + switch configuration.requiredBillingAddressFields { + case .none: + dictionary["required_billing_address_fields"] = "none" + case .postalCode: + dictionary["required_billing_address_fields"] = "zip" + case .full: + dictionary["required_billing_address_fields"] = "full" + case .name: + dictionary["required_billing_address_fields"] = "name" + default: + fatalError() + } + + var shippingFields: [String] = [] + if let shippingAddressFields = configuration.requiredShippingAddressFields { + if shippingAddressFields.contains(.name) { + shippingFields.append("name") + } + if shippingAddressFields.contains(.emailAddress) { + shippingFields.append("email") + } + if shippingAddressFields.contains(.postalAddress) { + shippingFields.append("address") + } + if shippingAddressFields.contains(.phoneNumber) { + shippingFields.append("phone") + } + } + + if shippingFields.isEmpty { + shippingFields.append("none") + } + dictionary["required_shipping_address_fields"] = shippingFields.joined(separator: "_") + + switch configuration.shippingType { + case .shipping: + dictionary["shipping_type"] = "shipping" + case .delivery: + dictionary["shipping_type"] = "delivery" + @unknown default: + break + } + + dictionary["company_name"] = configuration.companyName + dictionary["apple_merchant_identifier"] = configuration.appleMerchantIdentifier ?? "unknown" + return dictionary + } +} diff --git a/Stripe/StripeiOS/Source/STPBlocks.swift b/Stripe/StripeiOS/Source/STPBlocks.swift new file mode 100644 index 00000000..e750cc71 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPBlocks.swift @@ -0,0 +1,44 @@ +// +// STPBlocks.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit + +/// A callback to be run with a response from the Stripe API containing information about the online status of FPX banks. +/// - Parameters: +/// - bankStatusResponse: The response from Stripe containing the status of the various banks. Will be nil if an error occurs. - seealso: STPFPXBankStatusResponse +/// - error: The error returned from the response, or nil if none occurs. +typealias STPFPXBankStatusCompletionBlock = (STPFPXBankStatusResponse?, Error?) -> Void + +/// These values control the labels used in the shipping info collection form. +@objc public enum STPShippingType: Int { + /// Shipping the purchase to the provided address using a third-party + /// shipping company. + case shipping + /// Delivering the purchase by the seller. + case delivery +} + +/// An enum representing the status of a shipping address validation. +@objc public enum STPShippingStatus: Int { + /// The shipping address is valid. + case valid + /// The shipping address is invalid. + case invalid +} + +/// A callback to be run with a validation result and shipping methods for a +/// shipping address. +/// - Parameters: +/// - status: An enum representing whether the shipping address is valid. +/// - shippingValidationError: If the shipping address is invalid, an error describing the issue with the address. If no error is given and the address is invalid, the default error message will be used. +/// - shippingMethods: The shipping methods available for the address. +/// - selectedShippingMethod: The default selected shipping method for the address. +public typealias STPShippingMethodsCompletionBlock = ( + STPShippingStatus, Error?, [PKShippingMethod]?, PKShippingMethod? +) -> Void diff --git a/Stripe/StripeiOS/Source/STPCameraView.swift b/Stripe/StripeiOS/Source/STPCameraView.swift new file mode 100644 index 00000000..17fa4247 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCameraView.swift @@ -0,0 +1,77 @@ +// +// STPCameraView.swift +// StripeiOS +// +// Created by David Estes on 8/17/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import AVFoundation +import UIKit + +@available(macCatalyst 14.0, *) +class STPCameraView: UIView { + private var flashLayer: CALayer? + + var captureSession: AVCaptureSession? { + get { + return (videoPreviewLayer.session)! + } + set(captureSession) { + videoPreviewLayer.session = captureSession + } + } + + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + func playSnapshotAnimation() { + CATransaction.begin() + CATransaction.setValue( + kCFBooleanTrue, + forKey: kCATransactionDisableActions + ) + flashLayer?.frame = CGRect( + x: 0, + y: 0, + width: layer.bounds.size.width, + height: layer.bounds.size.height + ) + flashLayer?.opacity = 1.0 + CATransaction.commit() + DispatchQueue.main.async(execute: { + let fadeAnim = CABasicAnimation(keyPath: "opacity") + fadeAnim.fromValue = NSNumber(value: 1.0) + fadeAnim.toValue = NSNumber(value: 0.0) + fadeAnim.duration = 1.0 + self.flashLayer?.add(fadeAnim, forKey: "opacity") + self.flashLayer?.opacity = 0.0 + }) + } + + override init( + frame: CGRect + ) { + super.init(frame: frame) + flashLayer = CALayer() + if let flashLayer = flashLayer { + layer.addSublayer(flashLayer) + } + flashLayer?.masksToBounds = true + flashLayer?.backgroundColor = UIColor.black.cgColor + flashLayer?.opacity = 0.0 + layer.masksToBounds = true + videoPreviewLayer.videoGravity = .resizeAspectFill + } + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPCard+BasicUI.swift b/Stripe/StripeiOS/Source/STPCard+BasicUI.swift new file mode 100644 index 00000000..2b90f892 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCard+BasicUI.swift @@ -0,0 +1,30 @@ +// +// STPCard+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import UIKit + +extension STPCard: STPPaymentOption { + // MARK: - STPPaymentOption + @objc public var image: UIImage { + return STPImageLibrary.cardBrandImage(for: brand) + } + + @objc public var templateImage: UIImage { + return STPImageLibrary.templatedBrandImage(for: brand) + } + + @objc public var label: String { + let brand = STPCard.string(from: self.brand) + return "\(brand) \(last4 )" + } + + @objc public var isReusable: Bool { + return true + } +} diff --git a/Stripe/StripeiOS/Source/STPCardScanner.swift b/Stripe/StripeiOS/Source/STPCardScanner.swift new file mode 100644 index 00000000..c55e104b --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCardScanner.swift @@ -0,0 +1,488 @@ +// +// STPCardScanner.swift +// StripeiOS +// +// Created by David Estes on 8/17/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import AVFoundation +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +import UIKit +import Vision + +enum STPCardScannerError: Int { + /// Camera not available. + case cameraNotAvailable +} + +@available(macCatalyst 14.0, *) +@objc protocol STPCardScannerDelegate: NSObjectProtocol { + @objc(cardScanner:didFinishWithCardParams:error:) func cardScanner( + _ scanner: STPCardScanner, + didFinishWith cardParams: + STPPaymentMethodCardParams?, + error: Error?) +} + +@available(macCatalyst 14.0, *) +@objc(STPCardScanner_legacy) +class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { + // iOS will kill the app if it tries to request the camera without an NSCameraUsageDescription + static let cardScanningAvailableCameraHasUsageDescription = { + return + (Bundle.main.infoDictionary?["NSCameraUsageDescription"] != nil + || Bundle.main.localizedInfoDictionary?["NSCameraUsageDescription"] != nil) + }() + + static var cardScanningAvailable: Bool { + // Always allow in tests: + if NSClassFromString("XCTest") != nil { + return true + } + return cardScanningAvailableCameraHasUsageDescription + } + + weak var cameraView: STPCameraView? + + var feedbackGenerator: UINotificationFeedbackGenerator? + + @objc public var deviceOrientation: UIDeviceOrientation { + get { + return stp_deviceOrientation + } + set(newDeviceOrientation) { + stp_deviceOrientation = newDeviceOrientation + + // This is an optimization for portrait mode: The card will be centered in the screen, + // so we can ignore the top and bottom. We'll use the whole frame in landscape. + let kSTPCardScanningScreenCenter = CGRect( + x: 0, y: CGFloat(0.3), width: 1, height: CGFloat(0.4)) + + // iOS camera image data is returned in LandcapeLeft orientation by default. We'll flip it as needed: + switch newDeviceOrientation { + case .portraitUpsideDown: + videoOrientation = .portraitUpsideDown + textOrientation = .left + regionOfInterest = kSTPCardScanningScreenCenter + case .landscapeLeft: + videoOrientation = .landscapeRight + textOrientation = .up + regionOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1) + case .landscapeRight: + videoOrientation = .landscapeLeft + textOrientation = .down + regionOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1) + case .portrait, .faceUp, .faceDown, .unknown: + fallthrough + default: + videoOrientation = .portrait + textOrientation = .right + regionOfInterest = kSTPCardScanningScreenCenter + } + cameraView?.videoPreviewLayer.connection?.videoOrientation = videoOrientation + } + } + + override init() { + } + + init(delegate: STPCardScannerDelegate?) { + super.init() + self.delegate = delegate + captureSessionQueue = DispatchQueue(label: "com.stripe.CardScanning.CaptureSessionQueue") + deviceOrientation = UIDevice.current.orientation + } + + func start() { + if isScanning { + return + } + STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: STPCardScanner.self) + startTime = Date() + + isScanning = true + timeoutTime = nil + feedbackGenerator = UINotificationFeedbackGenerator() + feedbackGenerator?.prepare() + + captureSessionQueue?.async(execute: { + #if targetEnvironment(simulator) + // Camera not supported on Simulator + self.stopWithError(STPCardScanner.stp_cardScanningError()) + return + #else + self.detectedNumbers = NSCountedSet() + self.detectedExpirations = NSCountedSet() + self.setupCamera() + DispatchQueue.main.async(execute: { + self.cameraView?.captureSession = self.captureSession + self.cameraView?.videoPreviewLayer.connection?.videoOrientation = + self.videoOrientation + }) + #endif + }) + } + + func stop() { + stopWithError(nil) + } + + private weak var delegate: STPCardScannerDelegate? + private var captureDevice: AVCaptureDevice? + private var captureSession: AVCaptureSession? + private var captureSessionQueue: DispatchQueue? + private var videoDataOutput: AVCaptureVideoDataOutput? + private var videoDataOutputQueue: DispatchQueue? + private var textRequest: VNRecognizeTextRequest? + private var isScanning = false + + private var timeoutTime: Date? + private var didTimeout: Bool { + if let timeoutTime = timeoutTime { + return timeoutTime <= Date() + } + return false + } + + private var stp_deviceOrientation: UIDeviceOrientation! + private var videoOrientation: AVCaptureVideoOrientation! + private var textOrientation: CGImagePropertyOrientation! + private var regionOfInterest = CGRect.zero + private var detectedNumbers = NSCountedSet() + private var detectedExpirations = NSCountedSet() + private var startTime: Date? + + // MARK: Public + + class func stp_cardScanningError() -> Error { + let userInfo = [ + NSLocalizedDescriptionKey: String.Localized.allow_camera_access, + STPError.errorMessageKey: "The camera couldn't be used.", + ] + return NSError( + domain: STPCardScannerErrorDomain, + code: STPCardScannerError.cameraNotAvailable.rawValue, + userInfo: userInfo) + } + + deinit { + if isScanning { + captureDevice?.unlockForConfiguration() + captureSession?.stopRunning() + } + } + + func stopWithError(_ error: Error?) { + if isScanning { + finish(with: nil, error: error) + } + } + + // MARK: Setup + func setupCamera() { + weak var weakSelf = self + textRequest = VNRecognizeTextRequest(completionHandler: { request, error in + let strongSelf = weakSelf + if !(strongSelf?.isScanning ?? false) { + return + } + if error != nil { + strongSelf?.stopWithError(STPCardScanner.stp_cardScanningError()) + return + } + strongSelf?.processVNRequest(request) + }) + + let captureDevice = AVCaptureDevice.default( + .builtInWideAngleCamera, for: .video, position: .back) + self.captureDevice = captureDevice + + captureSession = AVCaptureSession() + captureSession?.sessionPreset = .hd1920x1080 + + var deviceInput: AVCaptureDeviceInput? + do { + if let captureDevice = captureDevice { + deviceInput = try AVCaptureDeviceInput(device: captureDevice) + } + } catch { + stopWithError(STPCardScanner.stp_cardScanningError()) + return + } + + if let deviceInput = deviceInput { + if captureSession?.canAddInput(deviceInput) ?? false { + captureSession?.addInput(deviceInput) + } else { + stopWithError(STPCardScanner.stp_cardScanningError()) + return + } + } + + videoDataOutputQueue = DispatchQueue(label: "com.stripe.CardScanning.VideoDataOutputQueue") + videoDataOutput = AVCaptureVideoDataOutput() + videoDataOutput?.alwaysDiscardsLateVideoFrames = true + videoDataOutput?.setSampleBufferDelegate(self, queue: videoDataOutputQueue) + + // This is the recommended pixel buffer format for Vision: + videoDataOutput?.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: + kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + ] + + if let videoDataOutput = videoDataOutput { + if captureSession?.canAddOutput(videoDataOutput) ?? false { + captureSession?.addOutput(videoDataOutput) + } else { + stopWithError(STPCardScanner.stp_cardScanningError()) + return + } + } + + // This improves recognition quality, but means the VideoDataOutput buffers won't match what we're seeing on screen. + videoDataOutput?.connection(with: .video)?.preferredVideoStabilizationMode = .auto + + captureSession?.startRunning() + + do { + try self.captureDevice?.lockForConfiguration() + self.captureDevice?.autoFocusRangeRestriction = .near + } catch { + } + } + + // MARK: Processing + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + if !isScanning { + return + } + let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) + if pixelBuffer == nil { + return + } + textRequest?.recognitionLevel = .accurate + textRequest?.usesLanguageCorrection = false + textRequest?.regionOfInterest = regionOfInterest + var handler: VNImageRequestHandler? + if let pixelBuffer = pixelBuffer { + handler = VNImageRequestHandler( + cvPixelBuffer: pixelBuffer, orientation: textOrientation, options: [:]) + } + do { + try handler?.perform([textRequest].compactMap { $0 }) + } catch { + } + } + + func processVNRequest(_ request: VNRequest) { + var allNumbers: [String] = [] + for observation in request.results ?? [] { + guard let observation = observation as? VNRecognizedTextObservation else { + continue + } + let candidates = observation.topCandidates(5) + let topCandidate = candidates.first?.string + if STPCardValidator.sanitizedNumericString(for: topCandidate ?? "").count >= 4 { + allNumbers.append(topCandidate ?? "") + } + for recognizedText in candidates { + let possibleNumber = STPCardValidator.sanitizedNumericString( + for: recognizedText.string) + if possibleNumber.count < 4 { + continue // This probably isn't something we're interested in, so don't bother processing it. + } + + // First strategy: We check if Vision sent us a number in a group on its own. If that fails, we'll try + // to catch it later when we iterate over all the numbers. + if STPCardValidator.validationState( + forNumber: possibleNumber, validatingCardBrand: true) + == .valid + { + addDetectedNumber(possibleNumber) + } else if possibleNumber.count >= 4 && possibleNumber.count <= 6 + && STPStringUtils.stringMayContainExpirationDate(recognizedText.string) + { + // Try to parse anything that looks like an expiration date. + let expirationString = STPStringUtils.expirationDateString( + from: recognizedText.string) + let sanitizedExpiration = STPCardValidator.sanitizedNumericString( + for: expirationString ?? "") + let month = (sanitizedExpiration as NSString).substring(to: 2) + let year = (sanitizedExpiration as NSString).substring(from: 2) + + // Ignore expiration dates 10+ years in the future, as they're likely to be incorrect recognitions + let calendar = Calendar(identifier: .gregorian) + let presentYear = calendar.component(.year, from: Date()) + let maxYear = (presentYear % 100) + 10 + + if STPCardValidator.validationState(forExpirationYear: year, inMonth: month) + == .valid + && Int(year) ?? 0 < maxYear + { + addDetectedExpiration(sanitizedExpiration) + } + } + } + } + // Second strategy: We look for consecutive groups of 4/4/4/4 or 4/6/5 + // Vision is sending us groups like ["1234 565", "1234 1"], so we'll normalize these into groups with spaces: + let allGroups = allNumbers.joined(separator: " ").components(separatedBy: " ") + if allGroups.count < 3 { + return + } + for i in 0..<(allGroups.count - 3) { + let string1 = allGroups[i] + let string2 = allGroups[i + 1] + let string3 = allGroups[i + 2] + var string4 = "" + if i + 3 < allGroups.count { + string4 = allGroups[i + 3] + } + // Then we'll go through each group and build a potential match: + let potentialCardString = "\(string1)\(string2)\(string3)\(string4)" + let potentialAmexString = "\(string1)\(string2)\(string3)" + + // Then we'll add valid matches. It's okay if we add a number a second time after doing so above, as the success of that first pass means it's more likely to be a good match. + if STPCardValidator.validationState( + forNumber: potentialCardString, validatingCardBrand: true) + == .valid + { + addDetectedNumber(potentialCardString) + } else if STPCardValidator.validationState( + forNumber: potentialAmexString, validatingCardBrand: true) == .valid + { + addDetectedNumber(potentialAmexString) + } + } + } + + func addDetectedNumber(_ number: String) { + detectedNumbers.add(number) + + // Set a timeout: If we don't get enough scans in the next 0.6 seconds, we'll use the best option we have. + if timeoutTime == nil { + self.timeoutTime = Date().addingTimeInterval(kSTPCardScanningTimeout) + weak var weakSelf = self + DispatchQueue.main.async(execute: { + let strongSelf = weakSelf + strongSelf?.cameraView?.playSnapshotAnimation() + strongSelf?.feedbackGenerator?.notificationOccurred(.success) + }) + // Just in case we don't get any frames, add another call to `finishIfReady` after timeoutTime to check + videoDataOutputQueue?.asyncAfter( + deadline: DispatchTime.now() + kSTPCardScanningTimeout, + execute: { + let strongSelf = weakSelf + if strongSelf?.isScanning ?? false { + strongSelf?.finishIfReady() + } + }) + } + + if (detectedNumbers.count(for: number)) >= kSTPCardScanningMinimumValidScans { + finishIfReady() + } + } + + func addDetectedExpiration(_ expiration: String) { + detectedExpirations.add(expiration) + if (detectedExpirations.count(for: expiration)) >= kSTPCardScanningMinimumValidScans { + finishIfReady() + } + } + + // MARK: Completion + func finishIfReady() { + if !isScanning { + return + } + let detectedNumbers = self.detectedNumbers + let detectedExpirations = self.detectedExpirations + + let topNumber = (detectedNumbers.allObjects as NSArray).sortedArray(comparator: { + obj1, obj2 in + let c1 = detectedNumbers.count(for: obj1) + let c2 = detectedNumbers.count(for: obj2) + if c1 < c2 { + return .orderedAscending + } else if c1 > c2 { + return .orderedDescending + } else { + return .orderedSame + } + }).last + let topExpiration = (detectedExpirations.allObjects as NSArray).sortedArray(comparator: { + obj1, obj2 in + let c1 = detectedExpirations.count(for: obj1) + let c2 = detectedExpirations.count(for: obj2) + if c1 < c2 { + return .orderedAscending + } else if c1 > c2 { + return .orderedDescending + } else { + return .orderedSame + } + }).last + + var didSeeEnoughScans = false + if let topNumber = topNumber, let topExpiration = topExpiration { + didSeeEnoughScans = detectedNumbers.count(for: topNumber) >= kSTPCardScanningMinimumValidScans && + detectedExpirations.count(for: topExpiration) >= kSTPCardScanningMinimumValidScans + } + if didTimeout || didSeeEnoughScans { + let params = STPPaymentMethodCardParams() + params.number = topNumber as? String + if let topExpiration = topExpiration { + params.expMonth = NSNumber( + value: Int((topExpiration as! NSString).substring(to: 2)) ?? 0) + params.expYear = NSNumber( + value: Int((topExpiration as! NSString).substring(from: 2)) ?? 0) + } + finish(with: params, error: nil) + } + } + + func finish(with params: STPPaymentMethodCardParams?, error: Error?) { + var duration: TimeInterval? + if let startTime = startTime { + duration = Date().timeIntervalSince(startTime) + } + isScanning = false + captureDevice?.unlockForConfiguration() + captureSession?.stopRunning() + + DispatchQueue.main.async(execute: { + if params == nil { + STPAnalyticsClient.sharedClient.logCardScanCancelled(withDuration: duration ?? 0.0) + } else { + STPAnalyticsClient.sharedClient.logCardScanSucceeded(withDuration: duration ?? 0.0) + } + self.feedbackGenerator = nil + + self.cameraView?.captureSession = nil + self.delegate?.cardScanner(self, didFinishWith: params, error: error) + }) + } + + // MARK: Orientation +} + +// The number of successful scans required for both card number and expiration date before returning a result. +private let kSTPCardScanningMinimumValidScans = 2 +// Once one successful scan is found, we'll stop scanning after this many seconds. +private let kSTPCardScanningTimeout: TimeInterval = 0.6 +let STPCardScannerErrorDomain = "STPCardScannerErrorDomain" + +/// :nodoc: +@available(macCatalyst 14.0, *) +extension STPCardScanner: STPAnalyticsProtocol { + static var stp_analyticsIdentifier = "STPCardScanner" +} diff --git a/Stripe/StripeiOS/Source/STPCardScannerTableViewCell.swift b/Stripe/StripeiOS/Source/STPCardScannerTableViewCell.swift new file mode 100644 index 00000000..7692e148 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCardScannerTableViewCell.swift @@ -0,0 +1,67 @@ +// +// STPCardScannerTableViewCell.swift +// StripeiOS +// +// Created by David Estes on 8/17/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import UIKit + +@available(macCatalyst 14.0, *) +class STPCardScannerTableViewCell: UITableViewCell { + private(set) weak var cameraView: STPCameraView? + + private var _theme: STPTheme? + var theme: STPTheme? { + get { + _theme + } + set(theme) { + _theme = theme + updateAppearance() + } + } + + let cardSizeRatio: CGFloat = 2.125 / 3.370 // ID-1 card size (in inches) + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + let cameraView = STPCameraView(frame: bounds) + contentView.addSubview(cameraView) + self.cameraView = cameraView + theme = STPTheme.defaultTheme + self.cameraView?.translatesAutoresizingMaskIntoConstraints = false + contentView.addConstraints( + [ + cameraView.heightAnchor.constraint( + equalTo: cameraView.widthAnchor, + multiplier: cardSizeRatio + ), + cameraView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0), + cameraView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), + cameraView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0), + cameraView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), + ]) + updateAppearance() + } + + override func layoutSubviews() { + + super.layoutSubviews() + } + + @objc func updateAppearance() { + // The first few frames of the camera view will be black, so our background should be black too. + cameraView?.backgroundColor = UIColor.black + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPCardValidationState.swift b/Stripe/StripeiOS/Source/STPCardValidationState.swift new file mode 100644 index 00000000..48e1f46b --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCardValidationState.swift @@ -0,0 +1,9 @@ +// +// STPCardValidationState.swift +// StripeiOS +// +// Created by Jack Flintermann on 8/7/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +import Foundation diff --git a/Stripe/StripeiOS/Source/STPCoreScrollViewController.swift b/Stripe/StripeiOS/Source/STPCoreScrollViewController.swift new file mode 100644 index 00000000..de3b7bbf --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCoreScrollViewController.swift @@ -0,0 +1,62 @@ +// +// STPCoreScrollViewController.swift +// StripeiOS +// +// Created by Brian Dorfman on 1/6/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeUICore +import UIKit + +/// This is the base class for all Stripe scroll view controllers. It is intended +/// for use only by Stripe classes, you should not subclass it yourself in your app. +public class STPCoreScrollViewController: STPCoreViewController { + /// This returns the scroll view being managed by the view controller + @objc public lazy var scrollView: UIScrollView = { + createScrollView() + }() + + /// This method is used by the base implementation to create the object + /// backing the `scrollView` property. Subclasses can override to change the + /// type of the scroll view (eg UITableView or UICollectionView instead of + /// UIScrollView). + + func createScrollView() -> UIScrollView { + return UIScrollView() + } + + override func createAndSetupViews() { + super.createAndSetupViews() + view.addSubview(scrollView) + } + + /// :nodoc: + @objc + public override func viewDidLoad() { + super.viewDidLoad() + + scrollView.contentInsetAdjustmentBehavior = .automatic + } + + /// :nodoc: + @objc + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + scrollView.frame = view.bounds + } + + @objc override func updateAppearance() { + super.updateAppearance() + + scrollView.backgroundColor = theme.primaryBackgroundColor + scrollView.tintColor = theme.accentColor + + if theme.primaryBackgroundColor.isBright { + scrollView.indicatorStyle = .black + } else { + scrollView.indicatorStyle = .white + } + } +} diff --git a/Stripe/StripeiOS/Source/STPCoreTableViewController.swift b/Stripe/StripeiOS/Source/STPCoreTableViewController.swift new file mode 100644 index 00000000..5d7105e3 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCoreTableViewController.swift @@ -0,0 +1,54 @@ +// +// STPCoreTableViewController.swift +// StripeiOS +// +// Created by Brian Dorfman on 1/6/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +import UIKit + +/// This is the base class for all Stripe scroll view controllers. It is intended +/// for use only by Stripe classes, you should not subclass it yourself in your app. +/// It inherits from STPCoreScrollViewController and changes the type of the +/// created scroll view to UITableView, as well as other shared table view logic. +public class STPCoreTableViewController: STPCoreScrollViewController { + /// This points to the same object as `STPCoreScrollViewController`'s `scrollView` + /// property but with the type cast to `UITableView` + + @objc public var tableView: UITableView? { + return (scrollView as? UITableView) + } + + override func createScrollView() -> UIScrollView { + let tableView = UITableView(frame: CGRect.zero, style: .grouped) + tableView.sectionHeaderHeight = 30 + + return tableView + } + + /// :nodoc: + @objc + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView?.reloadData() + } + + @objc override func updateAppearance() { + super.updateAppearance() + tableView?.separatorStyle = .none // handle this with fake separator views for flexibility + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) + -> CGFloat + { + return 0.01 + } +} diff --git a/Stripe/StripeiOS/Source/STPCoreViewController.swift b/Stripe/StripeiOS/Source/STPCoreViewController.swift new file mode 100644 index 00000000..105b544d --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCoreViewController.swift @@ -0,0 +1,162 @@ +// +// STPCoreViewController.swift +// StripeiOS +// +// Created by Brian Dorfman on 1/6/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeUICore +import UIKit + +/// This is the base class for all Stripe view controllers. It is intended for use +/// only by Stripe classes, you should not subclass it yourself in your app. +/// It theming, back/cancel button management, and other shared logic for +/// Stripe view controllers. +public class STPCoreViewController: UIViewController { + /// A convenience initializer; equivalent to calling `init(theme: STPTheme.defaultTheme)`. + @objc + public convenience init() { + self.init(theme: STPTheme.defaultTheme) + } + + /// Initializes a new view controller with the specified theme + /// - Parameter theme: The theme to use to inform the view controller's visual appearance. - seealso: STPTheme + @objc public required init( + theme: STPTheme? + ) { + super.init(nibName: nil, bundle: nil) + commonInit(with: theme) + } + + /// Passes through to the default UIViewController behavior for this initializer, + /// and then also sets the default theme as in `init` + @objc public required override init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + commonInit(with: STPTheme.defaultTheme) + } + + /// Passes through to the default UIViewController behavior for this initializer, + /// and then also sets the default theme as in `init` + @objc public required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + commonInit(with: STPTheme.defaultTheme) + } + + private var _theme: STPTheme = STPTheme.defaultTheme + @objc var theme: STPTheme { + get { + _theme + } + set(theme) { + _theme = theme + updateAppearance() + } + } + @objc var cancelItem: UIBarButtonItem? + + /// All designated initializers funnel through this method to do their setup + /// - Parameter theme: Initial theme for this view controller + func commonInit(with theme: STPTheme?) { + if let theme = theme { + _theme = theme + } else { + _theme = .defaultTheme + } + + if !useSystemBackButton() { + cancelItem = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(STPAddCardViewController.handleCancelTapped(_:)) + ) + cancelItem?.accessibilityIdentifier = "CoreViewControllerCancelIdentifier" + + stp_navigationItemProxy?.leftBarButtonItem = cancelItem + } + + NotificationCenter.default.addObserver( + self, + selector: #selector(STPAddCardViewController.updateAppearance), + name: UIContentSizeCategory.didChangeNotification, + object: nil + ) + } + + /// Called in viewDidLoad after doing base implementation, before + /// calling updateAppearance + func createAndSetupViews() { + // do nothing + } + + // These viewDidX() methods have significant code done + // in the base class and super must be called if they are overidden + /// :nodoc: + @objc + public override func viewDidLoad() { + super.viewDidLoad() + + createAndSetupViews() + updateAppearance() + } + /// :nodoc: + @objc + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateAppearance() + } + /// :nodoc: + @objc + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + view.endEditing(true) + } + + /// Update views based on current STPTheme + @objc func updateAppearance() { + let navBarTheme = navigationController?.navigationBar.stp_theme ?? theme + navigationItem.leftBarButtonItem?.stp_setTheme(navBarTheme) + navigationItem.rightBarButtonItem?.stp_setTheme(navBarTheme) + cancelItem?.stp_setTheme(navBarTheme) + + view.backgroundColor = theme.primaryBackgroundColor + + setNeedsStatusBarAppearanceUpdate() + } + + /// :nodoc: + @objc public override var preferredStatusBarStyle: UIStatusBarStyle { + let navBarTheme = navigationController?.navigationBar.stp_theme ?? theme + return navBarTheme.secondaryBackgroundColor.isBright + ? .default + : .lightContent + } + + /// Called by the automatically-managed back/cancel button + /// By default pops the top item off the navigation stack, or if we are the + /// root of the navigation controller, dimisses presentation + /// - Parameter sender: Sender of the target action, if applicable. + @objc func handleCancelTapped(_ sender: Any?) { + if stp_isAtRootOfNavigationController() { + // if we're the root of the navigation controller, we've been presented modally. + presentingViewController?.dismiss(animated: true) + } else { + // otherwise, we've been pushed onto the stack. + navigationController?.popViewController(animated: true) + } + } + + /// If you override this and return YES, then your CoreVC implementation will not + /// create and set up a cancel and instead just use the default + /// UIViewController back button behavior. + /// You won't receive calls to `handleCancelTapped` if this is YES. + /// Defaults to NO. + func useSystemBackButton() -> Bool { + return false + } +} diff --git a/Stripe/StripeiOS/Source/STPCustomerContext.swift b/Stripe/StripeiOS/Source/STPCustomerContext.swift new file mode 100644 index 00000000..13820fa2 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPCustomerContext.swift @@ -0,0 +1,419 @@ +// +// STPCustomerContext.swift +// StripeiOS +// +// Created by Ben Guo on 5/2/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI + +/// An `STPCustomerContext` retrieves and updates a Stripe customer and their attached +/// payment methods using an ephemeral key, a short-lived API key scoped to a specific +/// customer object. If your current user logs out of your app and a new user logs in, +/// be sure to either create a new instance of `STPCustomerContext` or clear the current +/// instance's cache. On your backend, be sure to create and return a +/// new ephemeral key for the Customer object associated with the new user. +open class STPCustomerContext: NSObject, STPBackendAPIAdapter { + /// Initializes a new `STPCustomerContext` with the specified key provider. + /// Upon initialization, a CustomerContext will fetch a new ephemeral key from + /// your backend and use it to prefetch the customer object specified in the key. + /// Subsequent customer and payment method retrievals (e.g. by `STPPaymentContext`) + /// will return the prefetched customer / attached payment methods immediately if + /// its age does not exceed 60 seconds. + /// - Parameter keyProvider: The key provider the customer context will use. + /// - Returns: the newly-instantiated customer context. + @objc(initWithKeyProvider:) + public convenience init( + keyProvider: STPCustomerEphemeralKeyProvider + ) { + self.init(keyProvider: keyProvider, apiClient: STPAPIClient.shared) + } + + /// Initializes a new `STPCustomerContext` with the specified key provider. + /// Upon initialization, a CustomerContext will fetch a new ephemeral key from + /// your backend and use it to prefetch the customer object specified in the key. + /// Subsequent customer and payment method retrievals (e.g. by `STPPaymentContext`) + /// will return the prefetched customer / attached payment methods immediately if + /// its age does not exceed 60 seconds. + /// - Parameters: + /// - keyProvider: The key provider the customer context will use. + /// - apiClient: The API Client to use to make requests. + /// - Returns: the newly-instantiated customer context. + @objc(initWithKeyProvider:apiClient:) + public convenience init( + keyProvider: STPCustomerEphemeralKeyProvider?, + apiClient: STPAPIClient + ) { + let keyManager = STPEphemeralKeyManager( + keyProvider: keyProvider, + apiVersion: STPAPIClient.apiVersion, + performsEagerFetching: true + ) + self.init(keyManager: keyManager, apiClient: apiClient) + } + + /// `STPCustomerContext` will cache its customer object and associated payment methods + /// for up to 60 seconds. If your current user logs out of your app and a new user logs + /// in, be sure to either call this method or create a new instance of `STPCustomerContext`. + /// On your backend, be sure to create and return a new ephemeral key for the + /// customer object associated with the new user. + @objc + public func clearCache() { + clearCachedCustomer() + clearCachedPaymentMethods() + } + + private var _includeApplePayPaymentMethods = false + /// By default, `STPCustomerContext` will filter Apple Pay when it retrieves + /// Payment Methods. Apple Pay payment methods should generally not be re-used and + /// shouldn't be offered to customers as a new payment method (Apple Pay payment + /// methods may only be re-used for subscriptions). + /// If you are using `STPCustomerContext` to back your own UI and would like to + /// disable Apple Pay filtering, set this property to YES. + /// Note: If you are using `STPPaymentContext`, you should not change this property. + @objc public var includeApplePayPaymentMethods: Bool { + get { + _includeApplePayPaymentMethods + } + set(includeApplePayMethods) { + _includeApplePayPaymentMethods = includeApplePayMethods + customer?.updateSources(filteringApplePay: !includeApplePayMethods) + } + } + + private var _customer: STPCustomer? + private var customer: STPCustomer? { + get { + _customer + } + set(customer) { + _customer = customer + customerRetrievedDate = (customer) != nil ? Date() : nil + } + } + @objc internal var customerRetrievedDate: Date? + + private var _paymentMethods: [STPPaymentMethod]? + private var paymentMethods: [STPPaymentMethod]? { + get { + if !includeApplePayPaymentMethods { + var paymentMethodsExcludingApplePay: [STPPaymentMethod]? = [] + for paymentMethod in _paymentMethods ?? [] { + let isApplePay = + paymentMethod.type == .card && paymentMethod.card?.wallet?.type == .applePay + if !isApplePay { + paymentMethodsExcludingApplePay?.append(paymentMethod) + } + } + return paymentMethodsExcludingApplePay ?? [] + } else { + return _paymentMethods ?? [] + } + } + set(paymentMethods) { + _paymentMethods = paymentMethods + paymentMethodsRetrievedDate = paymentMethods != nil ? Date() : nil + } + } + @objc internal var paymentMethodsRetrievedDate: Date? + private var keyManager: STPEphemeralKeyManagerProtocol + private var apiClient: STPAPIClient + + init( + keyManager: STPEphemeralKeyManagerProtocol, + apiClient: STPAPIClient + ) { + STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: STPCustomerContext.self) + self.keyManager = keyManager + self.apiClient = apiClient + _includeApplePayPaymentMethods = false + super.init() + retrieveCustomer(nil) + listPaymentMethodsForCustomer(completion: nil) + } + + func clearCachedCustomer() { + customer = nil + } + + func clearCachedPaymentMethods() { + paymentMethods = nil + } + + func shouldUseCachedCustomer() -> Bool { + if customer == nil || customerRetrievedDate == nil { + return false + } + let now = Date() + if let customerRetrievedDate = customerRetrievedDate { + return now.timeIntervalSince(customerRetrievedDate) < CachedCustomerMaxAge + } + return false + } + + func shouldUseCachedPaymentMethods() -> Bool { + if paymentMethods == nil || paymentMethodsRetrievedDate == nil { + return false + } + let now = Date() + if let paymentMethodsRetrievedDate = paymentMethodsRetrievedDate { + return now.timeIntervalSince(paymentMethodsRetrievedDate) < CachedCustomerMaxAge + } + return false + } + + // MARK: - STPBackendAPIAdapter + @objc + public func retrieveCustomer(_ completion: STPCustomerCompletionBlock? = nil) { + if shouldUseCachedCustomer() { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(self.customer, nil) + }) + } + return + } + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(nil, retrieveKeyError) + }) + } + return + } + self.apiClient.retrieveCustomer(using: ephemeralKey) { customer, error in + if let customer = customer { + customer.updateSources(filteringApplePay: !self.includeApplePayPaymentMethods) + self.customer = customer + } + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(self.customer, error) + }) + } + } + }) + } + + @objc + public func updateCustomer( + withShippingAddress shipping: STPAddress, + completion: STPErrorBlock? + ) { + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(retrieveKeyError) + }) + } + return + } + var params: [String: Any] = [:] + params["shipping"] = STPAddress.shippingInfoForCharge( + with: shipping, + shippingMethod: nil + ) + self.apiClient.updateCustomer( + withParameters: params, + using: ephemeralKey + ) { customer, error in + if let customer = customer { + customer.updateSources(filteringApplePay: !self.includeApplePayPaymentMethods) + self.customer = customer + } + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(error) + }) + } + } + }) + } + + /// A convenience method for attaching the PaymentMethod to the current Customer + @objc + public func attachPaymentMethodToCustomer( + paymentMethodId: String, + completion: STPErrorBlock? + ) { + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(retrieveKeyError) + }) + } + return + } + + self.apiClient.attachPaymentMethod( + paymentMethodId, + toCustomerUsing: ephemeralKey + ) { error in + self.clearCachedPaymentMethods() + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(error) + }) + } + } + }) + } + + @objc + public func attachPaymentMethod( + toCustomer paymentMethod: STPPaymentMethod, + completion: STPErrorBlock? + ) { + attachPaymentMethodToCustomer( + paymentMethodId: paymentMethod.stripeId, + completion: completion + ) + } + + /// A convenience method for detaching the PaymentMethod to the current Customer + @objc + public func detachPaymentMethodFromCustomer( + paymentMethodId: String, + completion: STPErrorBlock? + ) { + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(retrieveKeyError) + }) + } + return + } + + self.apiClient.detachPaymentMethod( + paymentMethodId, + fromCustomerUsing: ephemeralKey + ) { error in + self.clearCachedPaymentMethods() + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(error) + }) + } + } + }) + + } + + @objc + public func detachPaymentMethod( + fromCustomer paymentMethod: STPPaymentMethod, + completion: STPErrorBlock? + ) { + detachPaymentMethodFromCustomer( + paymentMethodId: paymentMethod.stripeId, + completion: completion + ) + } + + @objc + public func listPaymentMethodsForCustomer(completion: STPPaymentMethodsCompletionBlock? = nil) { + if shouldUseCachedPaymentMethods() { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(self.paymentMethods, nil) + }) + } + return + } + + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(nil, retrieveKeyError) + }) + } + return + } + + self.apiClient.listPaymentMethodsForCustomer(using: ephemeralKey) { + paymentMethods, + error in + if paymentMethods != nil { + self.paymentMethods = paymentMethods + } + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(self.paymentMethods, error) + }) + } + } + }) + } + + func saveLastSelectedPaymentMethodID( + forCustomer paymentMethodID: String?, + completion: STPErrorBlock? + ) { + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(retrieveKeyError) + }) + } + return + } + + var customerToDefaultPaymentMethodID = + (UserDefaults.standard.dictionary(forKey: kLastSelectedPaymentMethodDefaultsKey)) + as? [String: String] ?? [:] + if let customerID = ephemeralKey.customerID { + customerToDefaultPaymentMethodID[customerID] = paymentMethodID + UserDefaults.standard.set( + customerToDefaultPaymentMethodID, + forKey: kLastSelectedPaymentMethodDefaultsKey + ) + } + + if let completion = completion { + stpDispatchToMainThreadIfNecessary({ + completion(nil) + }) + } + }) + } + + func retrieveLastSelectedPaymentMethodIDForCustomer( + completion: @escaping (String?, Error?) -> Void + ) { + keyManager.getOrCreateKey({ ephemeralKey, retrieveKeyError in + guard let ephemeralKey = ephemeralKey, retrieveKeyError == nil else { + stpDispatchToMainThreadIfNecessary({ + completion(nil, retrieveKeyError) + }) + return + } + + let customerToDefaultPaymentMethodID = + (UserDefaults.standard.dictionary(forKey: kLastSelectedPaymentMethodDefaultsKey)) + as? [String: String] ?? [:] + stpDispatchToMainThreadIfNecessary({ + completion(customerToDefaultPaymentMethodID[ephemeralKey.customerID ?? ""], nil) + }) + }) + } +} + +/// Stores the key we use in NSUserDefaults to save a dictionary of Customer id to their last selected payment method ID +private let kLastSelectedPaymentMethodDefaultsKey = + UserDefaults.StripeKeys.customerToLastSelectedPaymentMethod.rawValue +private let CachedCustomerMaxAge: TimeInterval = 60 + +/// :nodoc: +@_spi(STP) extension STPCustomerContext: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier = "STPCustomerContext" +} diff --git a/Stripe/StripeiOS/Source/STPEphemeralKey.swift b/Stripe/StripeiOS/Source/STPEphemeralKey.swift new file mode 100644 index 00000000..6ddf10b6 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPEphemeralKey.swift @@ -0,0 +1,104 @@ +// +// STPEphemeralKey.swift +// StripeiOS +// +// Created by Ben Guo on 5/4/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePayments + +class STPEphemeralKey: NSObject, STPAPIResponseDecodable { + private(set) var stripeID: String + private(set) var created: Date + private(set) var livemode = false + private(set) var secret: String + private(set) var expires: Date + private(set) var customerID: String? + private(set) var issuingCardID: String? + + /// You cannot directly instantiate an `STPEphemeralKey`. You should instead use + /// `decodedObjectFromAPIResponse:` to create a key from a JSON response. + required init( + stripeID: String, + created: Date, + secret: String, + expires: Date + ) { + self.stripeID = stripeID + self.created = created + self.secret = secret + self.expires = expires + super.init() + } + + private(set) var allResponseFields: [AnyHashable: Any] = [:] + + class func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard let response = response else { + return nil + } + let dict = response.stp_dictionaryByRemovingNulls() + + // required fields + guard + let stripeId = dict.stp_string(forKey: "id"), + let created = dict.stp_date(forKey: "created"), + let secret = dict.stp_string(forKey: "secret"), + let expires = dict.stp_date(forKey: "expires"), + let associatedObjects = dict.stp_array(forKey: "associated_objects"), + dict["livemode"] != nil + else { + return nil + } + + var customerID: String? + var issuingCardID: String? + for obj in associatedObjects { + if let obj = obj as? [AnyHashable: Any] { + let type = obj.stp_string(forKey: "type") + if type == "customer" { + customerID = obj.stp_string(forKey: "id") + } + if type == "issuing.card" { + issuingCardID = obj.stp_string(forKey: "id") + } + } + } + if customerID == nil && issuingCardID == nil { + return nil + } + let key = self.init(stripeID: stripeId, created: created, secret: secret, expires: expires) + key.customerID = customerID + key.issuingCardID = issuingCardID + key.stripeID = stripeId + key.livemode = dict.stp_bool(forKey: "livemode", or: true) + key.created = created + key.secret = secret + key.expires = expires + key.allResponseFields = response + return key + } + + override var hash: Int { + return stripeID.hash + } + + override func isEqual(_ object: Any?) -> Bool { + if self === (object as? STPEphemeralKey) { + return true + } + if object == nil || !(object is STPEphemeralKey) { + return false + } + if let object = object as? STPEphemeralKey { + return isEqual(to: object) + } + return false + } + + func isEqual(to other: STPEphemeralKey) -> Bool { + return stripeID == other.stripeID + } +} diff --git a/Stripe/StripeiOS/Source/STPEphemeralKeyManager.swift b/Stripe/StripeiOS/Source/STPEphemeralKeyManager.swift new file mode 100644 index 00000000..b9955c98 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPEphemeralKeyManager.swift @@ -0,0 +1,179 @@ +// +// STPEphemeralKeyManager.swift +// StripeiOS +// +// Created by Ben Guo on 5/9/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +import UIKit + +protocol STPEphemeralKeyManagerProtocol { + /// If the retriever's stored ephemeral key has not expired, it will be + /// returned immediately to the given callback. If the stored key is expiring, a + /// new key will be requested from the key provider, and returned to the callback. + /// If the retriever is unable to provide an unexpired key, an error will be returned. + /// - Parameter completion: The callback to be run with the returned key, or an error. + func getOrCreateKey(_ completion: @escaping STPEphemeralKeyCompletionBlock) +} + +typealias STPEphemeralKeyCompletionBlock = (STPEphemeralKey?, Error?) -> Void +class STPEphemeralKeyManager: NSObject, STPEphemeralKeyManagerProtocol { + private var _expirationInterval: TimeInterval = 0.0 + /// If the current ephemeral key expires in less than this time interval, a call + /// to `getOrCreateKey` will request a new key from the manager's key provider. + /// The maximum allowed value is one hour – higher values will be clamped. + var expirationInterval: TimeInterval { + get { + _expirationInterval + } + set(expirationInterval) { + _expirationInterval = TimeInterval(min(expirationInterval, 60 * 60)) + } + } + /// If this value is YES, the manager will eagerly refresh its key on app foregrounding. + private(set) var performsEagerFetching = false + + /// Initializes a new `STPEphemeralKeyManager` with the specified key provider. + /// - Parameters: + /// - keyProvider: The key provider the manager will use. + /// - apiVersion: The Stripe API version the manager will use. + /// - performsEagerFetching: If the manager should eagerly refresh its key on app foregrounding. + /// - Returns: the newly-initiated `STPEphemeralKeyManager`. + @objc init( + keyProvider: Any?, + apiVersion: String, + performsEagerFetching: Bool + ) { + super.init() + assert( + keyProvider is STPCustomerEphemeralKeyProvider + || keyProvider is STPIssuingCardEphemeralKeyProvider, + "Your STPEphemeralKeyProvider must either implement `STPCustomerEphemeralKeyProvider` or `STPIssuingCardEphemeralKeyProvider`." + ) + expirationInterval = DefaultExpirationInterval + self.keyProvider = keyProvider + self.apiVersion = apiVersion + self.performsEagerFetching = performsEagerFetching + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillForegroundNotification), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + @objc dynamic func getOrCreateKey(_ completion: @escaping STPEphemeralKeyCompletionBlock) { + if currentKeyIsUnexpired() { + completion(ephemeralKey, nil) + } else { + if let createKeyPromise = createKeyPromise { + // coalesce repeated calls into one request + createKeyPromise.onSuccess({ key in + completion(key, nil) + }).onFailure({ error in + completion(nil, error) + }) + } else { + createKeyPromise = STPPromise.init().onSuccess({ key in + self.ephemeralKey = key + completion(key, nil) + }).onFailure({ error in + completion(nil, error) + }) + _createKey() + } + } + } + + @objc internal var ephemeralKey: STPEphemeralKey? + private var apiVersion: String? + private var keyProvider: Any? + @objc internal var lastEagerKeyRefresh: Date? + private var createKeyPromise: STPPromise? + + deinit { + NotificationCenter.default.removeObserver( + self, + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + func currentKeyIsUnexpired() -> Bool { + return ephemeralKey != nil + && (ephemeralKey?.expires.timeIntervalSinceNow ?? 0.0) > expirationInterval + } + + func shouldPerformEagerRefresh() -> Bool { + return performsEagerFetching + && (lastEagerKeyRefresh == nil + || (lastEagerKeyRefresh?.timeIntervalSinceNow ?? 0.0) > MinEagerRefreshInterval) + } + + @objc func handleWillForegroundNotification() { + // To make sure we don't end up hitting the ephemeral keys endpoint on every + // foreground (e.g. if there's an issue decoding the ephemeral key), throttle + // eager refreshes to once per hour. + if !currentKeyIsUnexpired() && shouldPerformEagerRefresh() { + lastEagerKeyRefresh = Date() + getOrCreateKey({ _, _ in + // getOrCreateKey sets the self.ephemeralKey. Nothing left to do for us here + }) + } + } + + func _createKey() { + let jsonCompletion = + { (jsonResponse: [AnyHashable: Any]?, error: Error?) in + let key = STPEphemeralKey.decodedObject(fromAPIResponse: jsonResponse) + if let key = key { + self.createKeyPromise?.succeed(key) + } else { + // the API request failed + if let error = error { + self.createKeyPromise?.fail(error) + } else { + // the ephemeral key could not be decoded + self.createKeyPromise?.fail(NSError.stp_ephemeralKeyDecodingError()) + if self.keyProvider is STPCustomerEphemeralKeyProvider { + assert( + false, + "Could not parse the ephemeral key response following protocol STPCustomerEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/basic#prepare-your-api" + ) + } else if self.keyProvider is STPIssuingCardEphemeralKeyProvider { + assert( + false, + "Could not parse the ephemeral key response following protocol STPIssuingCardEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/basic#prepare-your-api" + ) + } + assert( + false, + "Could not parse the ephemeral key response. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/basic#prepare-your-api" + ) + } + } + self.createKeyPromise = nil + } as STPJSONResponseCompletionBlock + + if keyProvider is STPCustomerEphemeralKeyProvider { + weak var provider = keyProvider as? STPCustomerEphemeralKeyProvider + provider?.createCustomerKey( + withAPIVersion: apiVersion ?? "", + completion: jsonCompletion + ) + } else if keyProvider is STPIssuingCardEphemeralKeyProvider { + weak var provider = keyProvider as? STPIssuingCardEphemeralKeyProvider + provider?.createIssuingCardKey( + withAPIVersion: apiVersion ?? "", + completion: jsonCompletion + ) + } + } +} + +private let DefaultExpirationInterval: TimeInterval = 60 +private let MinEagerRefreshInterval: TimeInterval = 60 * 60 diff --git a/Stripe/StripeiOS/Source/STPEphemeralKeyProvider.swift b/Stripe/StripeiOS/Source/STPEphemeralKeyProvider.swift new file mode 100644 index 00000000..9bac4bb2 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPEphemeralKeyProvider.swift @@ -0,0 +1,74 @@ +// +// STPEphemeralKeyProvider.swift +// StripeiOS +// +// Created by Ben Guo on 5/9/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation + +/// You should make your application's API client conform to this interface. +/// It provides a way for Stripe utility classes to request a new ephemeral key from +/// your backend, which it will use to retrieve and update Stripe API objects. +@objc public protocol STPCustomerEphemeralKeyProvider: NSObjectProtocol { + /// Creates a new ephemeral key for retrieving and updating a Stripe customer. + /// On your backend, you should create a new ephemeral key for the Stripe customer + /// associated with your user, and return the raw JSON response from the Stripe API. + /// For an example Ruby implementation of this API, refer to our example backend: + /// https://github.com/stripe/example-mobile-backend/blob/v18.1.0/web.rb + /// Back in your iOS app, once you have a response from this API, call the provided + /// completion block with the JSON response, or an error if one occurred. + /// - Parameters: + /// - apiVersion: The Stripe API version to use when creating a key. + /// You should pass this parameter to your backend, and use it to set the API version + /// in your key creation request. Passing this version parameter ensures that the + /// Stripe SDK can always parse the ephemeral key response from your server. + /// - completion: Call this callback when you're done fetching a new ephemeral + /// key from your backend. For example, `completion(json, nil)` (if your call succeeds) + /// or `completion(nil, error)` if an error is returned. + @objc(createCustomerKeyWithAPIVersion:completion:) func createCustomerKey( + withAPIVersion apiVersion: String, + completion: @escaping STPJSONResponseCompletionBlock + ) +} + +/// You should make your application's API client conform to this interface. +/// It provides a way for Stripe utility classes to request a new ephemeral key from +/// your backend, which it will use to retrieve and update Stripe API objects. +@objc public protocol STPIssuingCardEphemeralKeyProvider: NSObjectProtocol { + /// Creates a new ephemeral key for retrieving and updating a Stripe Issuing Card. + /// On your backend, you should create a new ephemeral key for your logged-in user's + /// primary Issuing Card, and return the raw JSON response from the Stripe API. + /// For an example Ruby implementation of this API, refer to our example backend: + /// https://github.com/stripe/example-mobile-backend/blob/v18.1.0/web.rb + /// Back in your iOS app, once you have a response from this API, call the provided + /// completion block with the JSON response, or an error if one occurred. + /// - Parameters: + /// - apiVersion: The Stripe API version to use when creating a key. + /// You should pass this parameter to your backend, and use it to set the API version + /// in your key creation request. Passing this version parameter ensures that the + /// Stripe SDK can always parse the ephemeral key response from your server. + /// - completion: Call this callback when you're done fetching a new ephemeral + /// key from your backend. For example, `completion(json, nil)` (if your call succeeds) + /// or `completion(nil, error)` if an error is returned. + @objc(createIssuingCardKeyWithAPIVersion:completion:) func createIssuingCardKey( + withAPIVersion apiVersion: String, + completion: @escaping STPJSONResponseCompletionBlock + ) +} + +/// You should make your application's API client conform to this interface. +/// It provides a way for Stripe utility classes to request a new ephemeral key from +/// your backend, which it will use to retrieve and update Stripe API objects. +/// @deprecated use `STPCustomerEphemeralKeyProvider` or `STPIssuingCardEphemeralKeyProvider` +/// depending on the type of key that will@objc be fetched. + +@available( + *, + deprecated, + message: + "use `STPCustomerEphemeralKeyProvider` or `STPIssuingCardEphemeralKeyProvider` depending on the type of key that will be fetched." +) +@objc public protocol STPEphemeralKeyProvider: STPCustomerEphemeralKeyProvider { +} diff --git a/Stripe/StripeiOS/Source/STPFPXBankStatusResponse.swift b/Stripe/StripeiOS/Source/STPFPXBankStatusResponse.swift new file mode 100644 index 00000000..73a2ff66 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPFPXBankStatusResponse.swift @@ -0,0 +1,44 @@ +// +// STPFPXBankStatusResponse.swift +// StripeiOS +// +// Created by David Estes on 10/21/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePayments + +class STPFPXBankStatusResponse: NSObject, STPAPIResponseDecodable { + func bankBrandIsOnline(_ bankBrand: STPFPXBankBrand) -> Bool { + let bankCode = STPFPXBank.bankCodeFrom(bankBrand, false) + let bankStatus = bankList?[bankCode ?? ""] + if bankCode != nil && bankStatus != nil { + return bankStatus?.boolValue ?? false + } + // This status endpoint isn't reliable. If we don't know this bank's status, default to online. + // The worst that will happen here is that the user ends up at their bank's "Down For Maintenance" page when checking out. + return true + } + + private var bankList: [String: NSNumber]? + private(set) var allResponseFields: [AnyHashable: Any] = [:] + + required internal override init() { + super.init() + } + + class func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard let response = response else { + return nil + } + let dict = response.stp_dictionaryByRemovingNulls() + + let statusResponse = self.init() + statusResponse.bankList = + dict.stp_dictionary(forKey: "parsed_bank_status") as? [String: NSNumber] + statusResponse.allResponseFields = dict + + return statusResponse + } +} diff --git a/Stripe/StripeiOS/Source/STPFakeAddPaymentPassViewController.swift b/Stripe/StripeiOS/Source/STPFakeAddPaymentPassViewController.swift new file mode 100644 index 00000000..34f553ac --- /dev/null +++ b/Stripe/StripeiOS/Source/STPFakeAddPaymentPassViewController.swift @@ -0,0 +1,279 @@ +// +// STPFakeAddPaymentPassViewController.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/28/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import PassKit +@_spi(STP) import StripeCore +import UIKit + +/// This class is a piece of fake UI that is intended to mimic `PKAddPaymentPassViewController`. That class is restricted to apps with a special entitlement from Apple, and as such can be difficult to build and test against. This class implements the same public API as `PKAddPaymentPassViewController`, and can be used to develop against the Stripe API in *testmode only*. (Obviously it will not actually place cards into the user's Apple Pay wallet either.) When it's time to go to production, you may simply replace all references to `STPFakeAddPaymentPassViewController` in your app with `PKAddPaymentPassViewController` and it will continue to function. For more information on developing against this API, please see https://stripe.com/docs/issuing/cards/digital-wallets . +public class STPFakeAddPaymentPassViewController: UIViewController { + /// @see PKAddPaymentPassViewController + @objc + public class func canAddPaymentPass() -> Bool { + return true + } + + /// @see PKAddPaymentPassViewController + @objc(initWithRequestConfiguration:delegate:) + public required init?( + requestConfiguration configuration: PKAddPaymentPassRequestConfiguration, + delegate: PKAddPaymentPassViewControllerDelegate? + ) { + super.init(nibName: nil, bundle: nil) + assert(delegate != nil, "Invalid parameter not satisfying: delegate != nil") + state = .initial + self.delegate = delegate + self.configuration = configuration + if configuration.primaryAccountSuffix == nil && configuration.cardholderName == nil { + assert( + false, + "Your PKAddPaymentPassRequestConfiguration must provide either a cardholderName or a primaryAccountSuffix." + ) + } + } + + /// @see PKAddPaymentPassViewController + @objc public weak var delegate: PKAddPaymentPassViewControllerDelegate? + private var configuration: PKAddPaymentPassRequestConfiguration? + + private var _state: STPFakeAddPaymentPassViewControllerState = .initial + private var state: STPFakeAddPaymentPassViewControllerState { + get { + _state + } + set(state) { + _state = state + let cancelItem = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(cancel(_:)) + ) + let nextButton = UIButton(type: .system) + nextButton.addTarget(self, action: #selector(next(_:)), for: .touchUpInside) + let indicatorView = UIActivityIndicatorView(style: .medium) + indicatorView.startAnimating() + let loadingItem = UIBarButtonItem(customView: indicatorView) + nextButton.setTitle(STPNonLocalizedString("Next"), for: .normal) + let nextItem = UIBarButtonItem(customView: nextButton) + let doneItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(done(_:)) + ) + + switch state { + case .initial: + contentLabel?.text = STPNonLocalizedString( + "This class simulates the delegate methods that PKAddPaymentPassViewController will call in your app. Press next to continue." + ) + navigationItem.leftBarButtonItem = cancelItem + navigationItem.rightBarButtonItem = nextItem + case .loading: + contentLabel?.text = STPNonLocalizedString("Fetching encrypted card details...") + cancelItem.isEnabled = false + navigationItem.leftBarButtonItem = cancelItem + navigationItem.rightBarButtonItem = loadingItem + case .error: + contentLabel?.text = STPNonLocalizedString("Error: " + (errorText ?? "")) + doneItem.isEnabled = false + navigationItem.leftBarButtonItem = cancelItem + navigationItem.rightBarButtonItem = doneItem + case .success: + contentLabel?.text = STPNonLocalizedString( + "Success! In production, your card would now have been added to your Apple Pay wallet. Your app's success callback will be triggered when the user presses 'Done'." + ) + cancelItem.isEnabled = false + navigationItem.leftBarButtonItem = cancelItem + navigationItem.rightBarButtonItem = doneItem + } + } + } + private var contentLabel: UILabel? + private var errorText: String? + + /// :nodoc: + @objc public convenience override init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + self.init(requestConfiguration: PKAddPaymentPassRequestConfiguration(), delegate: nil)! + } + + /// :nodoc: + @objc public required convenience init?( + coder aDecoder: NSCoder + ) { + self.init(requestConfiguration: PKAddPaymentPassRequestConfiguration(), delegate: nil) + } + + /// :nodoc: + @objc + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.white + + let navBar = UINavigationBar() + view.addSubview(navBar) + navBar.isTranslucent = false + navBar.backgroundColor = UIColor.white + navBar.items = [navigationItem] + navBar.translatesAutoresizingMaskIntoConstraints = false + navBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true + navBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true + navBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true + + let contentLabel = UILabel(frame: CGRect.zero) + self.contentLabel = contentLabel + contentLabel.textAlignment = .center + contentLabel.textColor = UIColor.black + contentLabel.numberOfLines = 0 + contentLabel.font = UIFont.systemFont(ofSize: 18) + contentLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(contentLabel) + contentLabel.topAnchor.constraint(equalTo: navBar.bottomAnchor).isActive = true + contentLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10.0).isActive = true + contentLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10.0).isActive = + true + contentLabel.heightAnchor.constraint(equalToConstant: 150).isActive = true + + var pairs: [AnyHashable] = [] + if configuration?.cardholderName != nil { + pairs.append(["Name", configuration?.cardholderName]) + } + if configuration?.primaryAccountSuffix != nil { + pairs.append([ + "Card Number", + "···· \(configuration?.primaryAccountSuffix ?? "")", + ]) + } + var rows: [AnyHashable] = [] + for pair in pairs { + guard let pair = pair as? [AnyHashable] else { + continue + } + let left = UILabel() + left.text = pair[0] as? String + left.textAlignment = .left + left.font = UIFont.boldSystemFont(ofSize: 16) + let right = UILabel() + right.text = pair[1] as? String + right.textAlignment = .left + right.textColor = UIColor.lightGray + let row = UIStackView(arrangedSubviews: [left, right]) + row.axis = .horizontal + row.distribution = .fillEqually + row.alignment = .fill + row.translatesAutoresizingMaskIntoConstraints = false + rows.append(row) + } + var pairsTable: UIStackView? + if let rows = rows as? [UIView] { + pairsTable = UIStackView(arrangedSubviews: rows) + } + pairsTable?.isLayoutMarginsRelativeArrangement = true + pairsTable?.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + pairsTable?.axis = .vertical + pairsTable?.translatesAutoresizingMaskIntoConstraints = false + if let pairsTable = pairsTable { + view.addSubview(pairsTable) + } + + pairsTable?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true + pairsTable?.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true + pairsTable?.topAnchor.constraint(equalTo: contentLabel.bottomAnchor).isActive = true + pairsTable?.heightAnchor.constraint(equalToConstant: CGFloat((rows.count * 50))).isActive = + true + state = .initial + } + + @objc func cancel(_ sender: Any?) { + let castedVC = unsafeBitCast(self, to: PKAddPaymentPassViewController.self) + delegate?.addPaymentPassViewController( + castedVC, + didFinishAdding: nil, + error: NSError( + domain: PKPassKitErrorDomain, + code: PKAddPaymentPassError.userCancelled.rawValue, + userInfo: nil + ) + ) + } + + @objc func next(_ sender: Any?) { + state = .loading + let certificates = [ + "cert1".data(using: .utf8), + "cert2".data(using: .utf8), + ] + let nonce = "nonce".data(using: .utf8) + let nonceSignature = "nonceSignature".data(using: .utf8) + + DispatchQueue.main.asyncAfter( + deadline: DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) + / Double(NSEC_PER_SEC), + execute: { + if self.state == .loading { + self.errorText = + "You exceeded the timeout of 10 seconds to call the request completion handler. Please check your PKAddPaymentPassViewControllerDelegate implementation, and make sure you are calling the `completionHandler` in `addPaymentPassViewController:generateRequestWithCertificateChain:nonce:nonceSignature:completionHandler`." + self.state = .error + } + } + ) + if let nonce = nonce, let nonceSignature = nonceSignature { + let castedVC = unsafeBitCast(self, to: PKAddPaymentPassViewController.self) + delegate?.addPaymentPassViewController( + castedVC, + generateRequestWithCertificateChain: certificates.compactMap { $0 }, + nonce: nonce, + nonceSignature: nonceSignature, + completionHandler: { request in + if self.state == .loading { + var contents: String? + if request.encryptedPassData != nil { + if let encryptedPassData1 = request.encryptedPassData { + contents = String(data: encryptedPassData1, encoding: .utf8) + } + } + if request.stp_error != nil { + var error = + (request.stp_error as NSError?)?.userInfo[STPError.errorMessageKey] + as? String + if error == nil { + error = + (request.stp_error as NSError?)?.userInfo[ + NSLocalizedDescriptionKey + ] as? String + } + self.errorText = error + self.state = .error + } else if contents == "TESTMODE_CONTENTS" { + self.state = .success + } else { + self.errorText = + "Your server response contained the wrong encrypted card details. Please ensure that you are not modifying the response from the Stripe API in any way, and that your request is in testmode." + self.state = .error + } + } + } + ) + } + } + + @objc func done(_ sender: Any?) { + let pass = PKPaymentPass() + let castedVC = unsafeBitCast(self, to: PKAddPaymentPassViewController.self) + delegate?.addPaymentPassViewController(castedVC, didFinishAdding: pass, error: nil) + } +} + +enum STPFakeAddPaymentPassViewControllerState: Int { + case initial + case loading + case error + case success +} diff --git a/Stripe/StripeiOS/Source/STPImageLibrary.swift b/Stripe/StripeiOS/Source/STPImageLibrary.swift new file mode 100644 index 00000000..dac95afb --- /dev/null +++ b/Stripe/StripeiOS/Source/STPImageLibrary.swift @@ -0,0 +1,94 @@ +// +// STPImageLibrary.swift +// StripeiOS +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +/// This class lets you access card icons used by the Stripe SDK. All icons are 32 x 20 points. +@objc class STPLegacyImageLibrary: NSObject { + + /// This returns the appropriate icon for the specified bank brand. + @objc(brandImageForFPXBankBrand:) public class func fpxBrandImage( + for brand: STPFPXBankBrand + ) + -> UIImage + { + let imageName = "stp_bank_fpx_\(STPFPXBank.identifierFrom(brand) ?? "")" + let image = self.safeImageNamed( + imageName, + templateIfAvailable: false + ) + return image + } + + /// An icon representing FPX. + @objc + public class func fpxLogo() -> UIImage { + return self.safeImageNamed("stp_fpx_logo", templateIfAvailable: false) + } + + /// A large branding image for FPX. + @objc + public class func largeFpxLogo() -> UIImage { + return self.safeImageNamed("stp_fpx_big_logo", templateIfAvailable: false) + } + + @objc class func addIcon() -> UIImage { + return self.safeImageNamed("stp_icon_add", templateIfAvailable: true) + } + + @objc class func checkmarkIcon() -> UIImage { + return self.safeImageNamed("stp_icon_checkmark", templateIfAvailable: true) + } + + @objc class func largeCardFrontImage() -> UIImage { + return self.safeImageNamed("stp_card_form_front", templateIfAvailable: true) + } + + @objc class func largeCardBackImage() -> UIImage { + return self.safeImageNamed("stp_card_form_back", templateIfAvailable: true) + } + + @objc class func largeCardAmexCVCImage() -> UIImage { + return self.safeImageNamed("stp_card_form_amex_cvc", templateIfAvailable: true) + } + + @objc class func largeShippingImage() -> UIImage { + return self.safeImageNamed("stp_shipping_form", templateIfAvailable: true) + } + + class func image( + withTintColor color: UIColor, + for image: UIImage + ) -> UIImage { + var newImage: UIImage? + UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) + color.set() + let templateImage = image.withRenderingMode(.alwaysTemplate) + templateImage.draw( + in: CGRect( + x: 0, + y: 0, + width: templateImage.size.width, + height: templateImage.size.height + ) + ) + newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return newImage ?? image + } +} + +// MARK: - ImageMaker + +// :nodoc: +@_spi(STP) extension STPLegacyImageLibrary: ImageMaker { + @_spi(STP) public typealias BundleLocator = StripeBundleLocator +} diff --git a/Stripe/StripeiOS/Source/STPIntentActionLinkAuthenticateAccount.swift b/Stripe/StripeiOS/Source/STPIntentActionLinkAuthenticateAccount.swift new file mode 100644 index 00000000..be029dbe --- /dev/null +++ b/Stripe/StripeiOS/Source/STPIntentActionLinkAuthenticateAccount.swift @@ -0,0 +1,33 @@ +// +// STPIntentActionLinkAuthenticateAccount.swift +// StripeiOS +// +// Created by Cameron Sabol on 7/6/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import UIKit + +class STPIntentActionLinkAuthenticateAccount: NSObject { + + let allResponseFields: [AnyHashable: Any] + + required init( + _ allResponseFields: [AnyHashable: Any] + ) { + self.allResponseFields = allResponseFields + super.init() + } + +} + +/// :nodoc: +extension STPIntentActionLinkAuthenticateAccount: STPAPIResponseDecodable { + class func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard let response = response else { + return nil + } + + return STPIntentActionLinkAuthenticateAccount(response) as? Self + } +} diff --git a/Stripe/StripeiOS/Source/STPLocalizedString.swift b/Stripe/StripeiOS/Source/STPLocalizedString.swift new file mode 100644 index 00000000..76afd007 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPLocalizedString.swift @@ -0,0 +1,16 @@ +// +// STPLocalizedString.swift +// StripeiOS +// +// Created by David Estes on 10/19/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore + +@inline(__always) func STPLocalizedString(_ key: String, _ comment: String?) -> String { + return STPLocalizationUtils.localizedStripeString( + forKey: key, + bundleLocator: StripeBundleLocator.self + ) +} diff --git a/Stripe/StripeiOS/Source/STPPaymentActivityIndicatorView.swift b/Stripe/StripeiOS/Source/STPPaymentActivityIndicatorView.swift new file mode 100644 index 00000000..68c03b6e --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentActivityIndicatorView.swift @@ -0,0 +1,135 @@ +// +// STPPaymentActivityIndicatorView.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +/// This class can be used wherever you'd use a `UIActivityIndicatorView` and is intended to have a similar API. It renders as a spinning circle with a gap in it, similar to what you see in the App Store app or in the Apple Pay dialog when making a purchase. To change its color, set the `tintColor` property. +public class STPPaymentActivityIndicatorView: UIView { + /// Tell the view to start or stop spinning. If `hidesWhenStopped` is true, it will fade in/out if animated is true. + @objc + public func setAnimating( + _ animating: Bool, + animated: Bool + ) { + if animating == _animating { + return + } + _animating = animating + if animating { + if hidesWhenStopped { + UIView.animate( + withDuration: animated ? 0.2 : 0, + animations: { + self.alpha = 1.0 + } + ) + } + var currentRotation = Double(0) + if let currentLayer = layer.presentation() { + currentRotation = Double( + truncating: (currentLayer.value(forKeyPath: "transform.rotation.z") as! NSNumber) + ) + } + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = NSNumber(value: Float(currentRotation)) + let toValue = NSNumber(value: currentRotation + 2 * Double.pi) + animation.toValue = toValue + animation.duration = 1.0 + animation.repeatCount = Float.infinity + layer.add(animation, forKey: "rotation") + } else { + if hidesWhenStopped { + UIView.animate( + withDuration: animated ? 0.2 : 0, + animations: { + self.alpha = 0.0 + } + ) + } + } + } + + private var _animating = false + /// Whether or not the view is animating. + @objc public var animating: Bool { + get { + _animating + } + set(animating) { + setAnimating(animating, animated: false) + } + } + + private var _hidesWhenStopped = true + /// If true, the view will hide when it is not spinning. Default is true. + @objc public var hidesWhenStopped: Bool { + get { + _hidesWhenStopped + } + set(hidesWhenStopped) { + _hidesWhenStopped = hidesWhenStopped + if !animating && hidesWhenStopped { + alpha = 0 + } else { + alpha = 1 + } + } + } + private weak var indicatorLayer: CAShapeLayer? + + /// :nodoc: + @objc override init( + frame: CGRect + ) { + var initialFrame = frame + if initialFrame.isEmpty { + initialFrame = CGRect(x: frame.origin.x, y: frame.origin.y, width: 40, height: 40) + } + super.init(frame: initialFrame) + backgroundColor = UIColor.clear + let layer = CAShapeLayer() + layer.backgroundColor = UIColor.clear.cgColor + layer.fillColor = UIColor.clear.cgColor + layer.strokeColor = tintColor.cgColor + layer.strokeStart = 0 + layer.lineCap = .round + layer.strokeEnd = 0.75 + layer.lineWidth = 2.0 + indicatorLayer = layer + self.layer.addSublayer(layer) + alpha = 0 + } + + /// :nodoc: + @objc public override var tintColor: UIColor! { + get { + return super.tintColor + } + set(tintColor) { + super.tintColor = tintColor + indicatorLayer?.strokeColor = tintColor.cgColor + } + } + + /// :nodoc: + @objc + public override func layoutSubviews() { + super.layoutSubviews() + var bounds = self.bounds + bounds.size.width = CGFloat(min(bounds.size.width, bounds.size.height)) + bounds.size.height = bounds.size.width + let path = UIBezierPath(ovalIn: bounds) + indicatorLayer?.path = path.cgPath + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentCardTextFieldCell.swift b/Stripe/StripeiOS/Source/STPPaymentCardTextFieldCell.swift new file mode 100644 index 00000000..40112d5d --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentCardTextFieldCell.swift @@ -0,0 +1,91 @@ +// +// STPPaymentCardTextFieldCell.swift +// StripeiOS +// +// Created by Jack Flintermann on 6/16/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripePaymentsUI +import UIKit + +class STPPaymentCardTextFieldCell: UITableViewCell { + private(set) weak var paymentField: STPPaymentCardTextField? + + var theme: STPTheme = STPTheme.defaultTheme { + didSet { + updateAppearance() + } + } + + private var _inputAccessoryView: UIView? + override var inputAccessoryView: UIView? { + get { + _inputAccessoryView + } + set(inputAccessoryView) { + _inputAccessoryView = inputAccessoryView + paymentField?.inputAccessoryView = inputAccessoryView + } + } + + func isEmpty() -> Bool { + return (paymentField?.cardNumber?.count ?? 0) == 0 + } + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + let paymentField = STPPaymentCardTextField(frame: bounds) + paymentField.postalCodeEntryEnabled = false + contentView.addSubview(paymentField) + self.paymentField = paymentField + theme = STPTheme.defaultTheme + updateAppearance() + } + + override func layoutSubviews() { + super.layoutSubviews() + paymentField?.frame = contentView.bounds + } + + @objc func updateAppearance() { + paymentField?.backgroundColor = UIColor.clear + paymentField?.placeholderColor = theme.tertiaryForegroundColor + paymentField?.borderColor = UIColor.clear + paymentField?.textColor = theme.primaryForegroundColor + paymentField?.textErrorColor = theme.errorColor + paymentField?.font = theme.font + backgroundColor = theme.secondaryBackgroundColor + } + + @objc override func becomeFirstResponder() -> Bool { + return paymentField?.becomeFirstResponder() ?? false + } + + override func accessibilityElementCount() -> Int { + return paymentField?.allFields.count ?? 0 + } + + override func accessibilityElement(at index: Int) -> Any? { + return paymentField?.allFields[index] + } + + override func index(ofAccessibilityElement element: Any) -> Int { + let fields = paymentField?.allFields + for i in 0..<(fields?.count ?? 0) { + if (element as? AnyHashable) == fields?[i] { + return i + } + } + return 0 + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentConfiguration.swift b/Stripe/StripeiOS/Source/STPPaymentConfiguration.swift new file mode 100644 index 00000000..12719a49 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentConfiguration.swift @@ -0,0 +1,244 @@ +// +// STPPaymentConfiguration.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +/// An `STPPaymentConfiguration` represents all the options you can set or change +/// around a payment. +/// You provide an `STPPaymentConfiguration` object to your `STPPaymentContext` +/// when making a charge. The configuration generally has settings that +/// will not change from payment to payment and thus is reusable, while the context +/// is specific to a single particular payment instance. +public class STPPaymentConfiguration: NSObject, NSCopying { + /// This is a convenience singleton configuration that uses the default values for + /// every property + @objc(sharedConfiguration) public static var shared = STPPaymentConfiguration() + + private var _applePayEnabled = true + /// The user is allowed to pay with Apple Pay if it's configured and available on their device. + @objc public var applePayEnabled: Bool { + get { + return appleMerchantIdentifier != nil && _applePayEnabled + && StripeAPI.deviceSupportsApplePay() + } + set { + _applePayEnabled = newValue + } + } + + /// The user is allowed to pay with FPX. + @objc public var fpxEnabled = false + + /// The billing address fields the user must fill out when prompted for their + /// payment details. These fields will all be present on the returned PaymentMethod from + /// Stripe. + /// The default value is `STPBillingAddressFieldsPostalCode`. + /// - seealso: https://stripe.com/docs/api/payment_methods/create#create_payment_method-billing_details + @objc public var requiredBillingAddressFields = STPBillingAddressFields.postalCode + /// The shipping address fields the user must fill out when prompted for their + /// shipping info. Set to nil if shipping address is not required. + /// The default value is nil. + @objc public var requiredShippingAddressFields: Set? + /// Whether the user should be prompted to verify prefilled shipping information. + /// The default value is YES. + @objc public var verifyPrefilledShippingAddress = true + /// The type of shipping for this purchase. This property sets the labels displayed + /// when the user is prompted for shipping info, and whether they should also be + /// asked to select a shipping method. + /// The default value is STPShippingTypeShipping. + @objc public var shippingType = STPShippingType.shipping + /// The set of countries supported when entering an address. This property accepts + /// a set of ISO 2-character country codes. + /// The default value is all known countries. Setting this property will limit + /// the available countries to your selected set. + @objc public var availableCountries: Set = Set(NSLocale.isoCountryCodes) + + /// The name of your company, for displaying to the user during payment flows. For + /// example, when using Apple Pay, the payment sheet's final line item will read + /// "PAY {companyName}". + /// The default value is the name of your iOS application which is derived from the + /// `kCFBundleNameKey` of `Bundle.main`. + @objc public var companyName = Bundle.stp_applicationName() ?? "" + /// The Apple Merchant Identifier to use during Apple Pay transactions. To create + /// one of these, see our guide at https://stripe.com/docs/apple-pay . You + /// must set this to a valid identifier in order to automatically enable Apple Pay. + @objc public var appleMerchantIdentifier: String? + /// Determines whether or not the user is able to delete payment options + /// This is only relevant to the `STPPaymentOptionsViewController` which, if + /// enabled, will allow the user to delete payment options by tapping the "Edit" + /// button in the navigation bar or by swiping left on a payment option and tapping + /// "Delete". Currently, the user is not allowed to delete the selected payment + /// option but this may change in the future. + /// Default value is YES but will only work if `STPPaymentOptionsViewController` is + /// initialized with a `STPCustomerContext` either through the `STPPaymentContext` + /// or directly as an init parameter. + @objc public var canDeletePaymentOptions = true + /// Determines whether STPAddCardViewController allows the user to + /// scan cards using the camera on devices running iOS 13 or later. + /// To use this feature, you must also set the `NSCameraUsageDescription` + /// value in your app's Info.plist. + /// @note This feature is currently in beta. Please file bugs at + /// https://github.com/stripe/stripe-ios/issues + /// The default value is currently NO. This will be changed in a future update. + @objc public var cardScanningEnabled = false + // MARK: - Deprecated + + /// An enum value representing which payment options you will accept from your user + /// in addition to credit cards. + @available( + *, + deprecated, + message: + "additionalPaymentOptions has been removed. Set applePayEnabled and fpxEnabled on STPPaymentConfiguration instead." + ) + @objc public var additionalPaymentOptions: Int = 0 + + private var _publishableKey: String? + /// If you used STPPaymentConfiguration.shared.publishableKey, use STPAPIClient.shared.publishableKey instead. The SDK uses STPAPIClient.shared to make API requests by default. + /// Your Stripe publishable key + /// - seealso: https://dashboard.stripe.com/account/apikeys + @available( + *, + deprecated, + message: + "If you used STPPaymentConfiguration.shared.publishableKey, use STPAPIClient.shared.publishableKey instead. If you passed a STPPaymentConfiguration instance to an SDK component, create an STPAPIClient, set publishableKey on it, and set the SDK component's APIClient property." + ) + @objc public var publishableKey: String? { + get { + if self == STPPaymentConfiguration.shared { + return STPAPIClient.shared.publishableKey + } + return _publishableKey ?? "" + } + set(publishableKey) { + if self == STPPaymentConfiguration.shared { + STPAPIClient.shared.publishableKey = publishableKey + } else { + _publishableKey = publishableKey + } + } + } + + private var _stripeAccount: String? + /// If you used STPPaymentConfiguration.shared.stripeAccount, use STPAPIClient.shared.stripeAccount instead. The SDK uses STPAPIClient.shared to make API requests by default. + /// In order to perform API requests on behalf of a connected account, e.g. to + /// create charges for a connected account, set this property to the ID of the + /// account for which this request is being made. + /// - seealso: https://stripe.com/docs/payments#connected-accounts + @available( + *, + deprecated, + message: + "If you used STPPaymentConfiguration.shared.stripeAccount, use STPAPIClient.shared.stripeAccount instead. If you passed a STPPaymentConfiguration instance to an SDK component, create an STPAPIClient, set stripeAccount on it, and set the SDK component's APIClient property." + ) + @objc public var stripeAccount: String? { + get { + if self == STPPaymentConfiguration.shared { + return STPAPIClient.shared.stripeAccount + } + return _stripeAccount ?? "" + } + set(stripeAccount) { + if self == STPPaymentConfiguration.shared { + STPAPIClient.shared.stripeAccount = stripeAccount + } else { + _stripeAccount = stripeAccount + } + } + } + + // MARK: - Description + /// :nodoc: + @objc public override var description: String { + var additionalPaymentOptionsDescription: String? + + var paymentOptions: [String] = [] + + if _applePayEnabled { + paymentOptions.append("STPPaymentOptionTypeApplePay") + } + + if fpxEnabled { + paymentOptions.append("STPPaymentOptionTypeFPX") + } + + additionalPaymentOptionsDescription = paymentOptions.joined(separator: "|") + + var requiredBillingAddressFieldsDescription: String? + + switch requiredBillingAddressFields { + case .none: + requiredBillingAddressFieldsDescription = "STPBillingAddressFieldsNone" + case .postalCode: + requiredBillingAddressFieldsDescription = "STPBillingAddressFieldsPostalCode" + case .full: + requiredBillingAddressFieldsDescription = "STPBillingAddressFieldsFull" + case .name: + requiredBillingAddressFieldsDescription = "STPBillingAddressFieldsName" + default: + break + } + + let requiredShippingAddressFieldsDescription = requiredShippingAddressFields?.map({ + $0.rawValue + }).joined(separator: "|") + + var shippingTypeDescription: String? + + switch shippingType { + case .shipping: + shippingTypeDescription = "STPShippingTypeShipping" + case .delivery: + shippingTypeDescription = "STPShippingTypeDelivery" + default: + break + } + + let props = [ + // Object + String(format: "%@: %p", NSStringFromClass(STPPaymentConfiguration.self), self), + // Basic configuration + "additionalPaymentOptions = \(additionalPaymentOptionsDescription ?? "")", + // Billing and shipping + "requiredBillingAddressFields = \(requiredBillingAddressFieldsDescription ?? "")", + "requiredShippingAddressFields = \(requiredShippingAddressFieldsDescription ?? "")", + "verifyPrefilledShippingAddress = \((verifyPrefilledShippingAddress) ? "YES" : "NO")", + "shippingType = \(shippingTypeDescription ?? "")", + "availableCountries = \(availableCountries )", + // Additional configuration + "companyName = \(companyName )", + "appleMerchantIdentifier = \(appleMerchantIdentifier ?? "")", + "canDeletePaymentOptions = \((canDeletePaymentOptions) ? "YES" : "NO")", + "cardScanningEnabled = \((cardScanningEnabled) ? "YES" : "NO")", + ] + + return "<\(props.joined(separator: "; "))>" + } + + // MARK: - NSCopying + /// :nodoc: + @objc + public func copy(with zone: NSZone? = nil) -> Any { + let copy = STPPaymentConfiguration() + copy.applePayEnabled = _applePayEnabled + copy.fpxEnabled = fpxEnabled + copy.requiredBillingAddressFields = requiredBillingAddressFields + copy.requiredShippingAddressFields = requiredShippingAddressFields + copy.verifyPrefilledShippingAddress = verifyPrefilledShippingAddress + copy.shippingType = shippingType + copy.companyName = companyName + copy.appleMerchantIdentifier = appleMerchantIdentifier + copy.canDeletePaymentOptions = canDeletePaymentOptions + copy.cardScanningEnabled = cardScanningEnabled + copy.availableCountries = availableCountries + copy._publishableKey = _publishableKey + copy._stripeAccount = _stripeAccount + return copy + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentContext.swift b/Stripe/StripeiOS/Source/STPPaymentContext.swift new file mode 100644 index 00000000..45af07a5 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentContext.swift @@ -0,0 +1,1266 @@ +// +// STPPaymentContext.swift +// StripeiOS +// +// Created by Jack Flintermann on 4/20/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +import ObjectiveC +import PassKit +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI + +/// An `STPPaymentContext` keeps track of all of the state around a payment. It will manage fetching a user's saved payment methods, tracking any information they select, and prompting them for required additional information before completing their purchase. It can be used to power your application's "payment confirmation" page with just a few lines of code. +/// `STPPaymentContext` also provides a unified interface to multiple payment methods - for example, you can write a single integration to accept both credit card payments and Apple Pay. +/// `STPPaymentContext` saves information about a user's payment methods to a Stripe customer object, and requires an `STPCustomerContext` to manage retrieving and modifying the customer. +@objc(STPPaymentContext) +public class STPPaymentContext: NSObject, STPAuthenticationContext, + STPPaymentOptionsViewControllerDelegate, STPShippingAddressViewControllerDelegate +{ + /// This is a convenience initializer; it is equivalent to calling + /// `init(customerContext:customerContext + /// configuration:STPPaymentConfiguration.shared + /// theme:STPTheme.defaultTheme`. + /// - Parameter customerContext: The customer context the payment context will use to fetch + /// and modify its Stripe customer. - seealso: STPCustomerContext.h + /// - Returns: the newly-instantiated payment context + @objc + public convenience init( + customerContext: STPCustomerContext + ) { + self.init(apiAdapter: customerContext) + } + + /// Initializes a new Payment Context with the provided customer context, configuration, + /// and theme. After this class is initialized, you should also make sure to set its + /// `delegate` and `hostViewController` properties. + /// - Parameters: + /// - customerContext: The customer context the payment context will use to fetch + /// and modify its Stripe customer. - seealso: STPCustomerContext.h + /// - configuration: The configuration for the payment context to use. This + /// lets you set your Stripe publishable API key, required billing address fields, etc. + /// - seealso: STPPaymentConfiguration.h + /// - theme: The theme describing the visual appearance of all UI + /// that the payment context automatically creates for you. - seealso: STPTheme.h + /// - Returns: the newly-instantiated payment context + @objc + public convenience init( + customerContext: STPCustomerContext, + configuration: STPPaymentConfiguration, + theme: STPTheme + ) { + self.init( + apiAdapter: customerContext, + configuration: configuration, + theme: theme + ) + } + + /// Note: Instead of providing your own backend API adapter, we recommend using + /// `STPCustomerContext`, which will manage retrieving and updating a + /// Stripe customer for you. - seealso: STPCustomerContext.h + /// This is a convenience initializer; it is equivalent to calling + /// `init(apiAdapter:apiAdapter configuration:STPPaymentConfiguration.shared theme:STPTheme.defaultTheme)`. + @objc + public convenience init( + apiAdapter: STPBackendAPIAdapter + ) { + self.init( + apiAdapter: apiAdapter, + configuration: STPPaymentConfiguration.shared, + theme: STPTheme.defaultTheme + ) + } + + /// Note: Instead of providing your own backend API adapter, we recommend using + /// `STPCustomerContext`, which will manage retrieving and updating a + /// Stripe customer for you. - seealso: STPCustomerContext.h + /// Initializes a new Payment Context with the provided API adapter and configuration. + /// After this class is initialized, you should also make sure to set its `delegate` + /// and `hostViewController` properties. + /// - Parameters: + /// - apiAdapter: The API adapter the payment context will use to fetch and + /// modify its contents. You need to make a class conforming to this protocol that + /// talks to your server. - seealso: STPBackendAPIAdapter.h + /// - configuration: The configuration for the payment context to use. This lets + /// you set your Stripe publishable API key, required billing address fields, etc. + /// - seealso: STPPaymentConfiguration.h + /// - theme: The theme describing the visual appearance of all UI that + /// the payment context automatically creates for you. - seealso: STPTheme.h + /// - Returns: the newly-instantiated payment context + @objc + public init( + apiAdapter: STPBackendAPIAdapter, + configuration: STPPaymentConfiguration, + theme: STPTheme + ) { + STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: STPPaymentContext.self) + AnalyticsHelper.shared.generateSessionID() + self.configuration = configuration + self.apiAdapter = apiAdapter + self.theme = theme + paymentCurrency = "USD" + paymentCountry = "US" + apiClient = STPAPIClient.shared + modalPresentationStyle = .fullScreen + state = STPPaymentContextState.none + super.init() + retryLoading() + } + + /// Note: Instead of providing your own backend API adapter, we recommend using + /// `STPCustomerContext`, which will manage retrieving and updating a + /// Stripe customer for you. - seealso: STPCustomerContext.h + /// The API adapter the payment context will use to fetch and modify its contents. + /// You need to make a class conforming to this protocol that talks to your server. + /// - seealso: STPBackendAPIAdapter.h + @objc public private(set) var apiAdapter: STPBackendAPIAdapter + /// The configuration for the payment context to use internally. - seealso: STPPaymentConfiguration.h + @objc public private(set) var configuration: STPPaymentConfiguration + /// The visual appearance that will be used by any views that the context generates. - seealso: STPTheme.h + @objc public private(set) var theme: STPTheme + + private var _prefilledInformation: STPUserInformation? + /// If you've already collected some information from your user, you can set it here and it'll be automatically filled out when possible/appropriate in any UI that the payment context creates. + @objc public var prefilledInformation: STPUserInformation? { + get { + _prefilledInformation + } + set(prefilledInformation) { + _prefilledInformation = prefilledInformation + if prefilledInformation?.shippingAddress != nil && shippingAddress == nil { + shippingAddress = prefilledInformation?.shippingAddress + shippingAddressNeedsVerification = true + } + } + } + + private weak var _hostViewController: UIViewController? + /// The view controller that any additional UI will be presented on. If you have a "checkout view controller" in your app, that should be used as the host view controller. + @objc public weak var hostViewController: UIViewController? { + get { + _hostViewController + } + set(hostViewController) { + assert( + _hostViewController == nil, + "You cannot change the hostViewController on an STPPaymentContext after it's already been set." + ) + _hostViewController = hostViewController + if hostViewController is UINavigationController { + originalTopViewController = + (hostViewController as? UINavigationController)?.topViewController + } + if let hostViewController = hostViewController { + artificiallyRetain(hostViewController) + } + } + } + + private weak var _delegate: STPPaymentContextDelegate? + /// This delegate will be notified when the payment context's contents change. - seealso: STPPaymentContextDelegate + @objc public weak var delegate: STPPaymentContextDelegate? { + get { + _delegate + } + set(delegate) { + _delegate = delegate + DispatchQueue.main.async(execute: { + self.delegate?.paymentContextDidChange(self) + }) + } + } + /// Whether or not the payment context is currently loading information from the network. + + @objc public var loading: Bool { + return !(loadingPromise?.completed)! + } + /// @note This is no longer recommended as of v18.3.0 - the SDK automatically saves the Stripe ID of the last selected + /// payment method using NSUserDefaults and displays it as the default pre-selected option. You can override this behavior + /// by setting this property. + /// The Stripe ID of a payment method to display as the default pre-selected option. + /// @note Set this property immediately after initializing STPPaymentContext, or call `retryLoading` afterwards. + @objc public var defaultPaymentMethod: String? + + private var _selectedPaymentOption: STPPaymentOption? + /// The user's currently selected payment option. May be nil. + @objc public private(set) var selectedPaymentOption: STPPaymentOption? { + get { + _selectedPaymentOption + } + set { + if let newValue = newValue, let paymentOptions = self.paymentOptions { + if !paymentOptions.contains(where: { (option) -> Bool in + newValue.isEqual(option) + }) { + if newValue.isReusable { + self.paymentOptions = paymentOptions + [newValue] + } + } + } + if !(_selectedPaymentOption?.isEqual(newValue) ?? false) { + _selectedPaymentOption = newValue + stpDispatchToMainThreadIfNecessary({ + self.delegate?.paymentContextDidChange(self) + }) + } + + } + } + + private var _paymentOptions: [STPPaymentOption]? + /// The available payment options the user can choose between. May be nil. + @objc public private(set) var paymentOptions: [STPPaymentOption]? { + get { + _paymentOptions + } + set { + _paymentOptions = newValue?.sorted(by: { (obj1, obj2) -> Bool in + let applePayKlass = STPApplePayPaymentOption.self + let paymentMethodKlass = STPPaymentMethod.self + if obj1.isKind(of: applePayKlass) { + return true + } else if obj2.isKind(of: applePayKlass) { + return false + } + if obj1.isKind(of: paymentMethodKlass) && obj2.isKind(of: paymentMethodKlass) { + return (obj1.label.compare(obj2.label) == .orderedAscending) + } + return false + }) + } + } + + /// The user's currently selected shipping method. May be nil. + @objc public internal(set) var selectedShippingMethod: PKShippingMethod? + + private var _shippingMethods: [PKShippingMethod]? + /// An array of STPShippingMethod objects that describe the supported shipping methods. May be nil. + @objc public private(set) var shippingMethods: [PKShippingMethod]? { + get { + _shippingMethods + } + set { + _shippingMethods = newValue + if let shippingMethods = newValue, + let selectedShippingMethod = self.selectedShippingMethod + { + if shippingMethods.count == 0 { + self.selectedShippingMethod = nil + } else if shippingMethods.contains(selectedShippingMethod) { + self.selectedShippingMethod = shippingMethods.first + } + } + } + } + + /// The user's shipping address. May be nil. + /// If you've already collected a shipping address from your user, you may + /// prefill it by setting a shippingAddress in PaymentContext's prefilledInformation. + /// When your user enters a new shipping address, PaymentContext will save it to + /// the current customer object. When PaymentContext loads, if you haven't + /// manually set a prefilled value, any shipping information saved on the customer + /// will be used to prefill the shipping address form. Note that because your + /// customer's email may not be the same as the email provided with their shipping + /// info, PaymentContext will not prefill the shipping form's email using your + /// customer's email. + /// You should not rely on the shipping information stored on the Stripe customer + /// for order fulfillment, as your user may change this information if they make + /// multiple purchases. We recommend adding shipping information when you create + /// a charge (which can also help prevent fraud), or saving it to your own + /// database. https://stripe.com/docs/api/payment_intents/create#create_payment_intent-shipping + /// Note: by default, your user will still be prompted to verify a prefilled + /// shipping address. To change this behavior, you can set + /// `verifyPrefilledShippingAddress` to NO in your `STPPaymentConfiguration`. + @objc public private(set) var shippingAddress: STPAddress? + /// The amount of money you're requesting from the user, in the smallest currency + /// unit for the selected currency. For example, to indicate $10 USD, use 1000 + /// (i.e. 1000 cents). For more information, see https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount + /// @note This value must be present and greater than zero in order for Apple Pay + /// to be automatically enabled. + /// @note You should only set either this or `paymentSummaryItems`, not both. + /// The other will be automatically calculated on demand using your `paymentCurrency`. + + @objc public var paymentAmount: Int { + get { + return paymentAmountModel.paymentAmount( + withCurrency: paymentCurrency, + shippingMethod: selectedShippingMethod + ) + } + set(paymentAmount) { + paymentAmountModel = STPPaymentContextAmountModel(amount: paymentAmount) + } + } + /// The three-letter currency code for the currency of the payment (i.e. USD, GBP, + /// JPY, etc). Defaults to "USD". + /// @note Changing this property may change the return value of `paymentAmount` + /// or `paymentSummaryItems` (whichever one you didn't directly set yourself). + @objc public var paymentCurrency: String + /// The two-letter country code for the country where the payment will be processed. + /// You should set this to the country your Stripe account is in. Defaults to "US". + /// @note Changing this property will change the `countryCode` of your Apple Pay + /// payment requests. + /// - seealso: PKPaymentRequest for more information. + @objc public var paymentCountry: String + /// If you support Apple Pay, you can optionally set the PKPaymentSummaryItems + /// you want to display here instead of using `paymentAmount`. Note that the + /// grand total (the amount of the last summary item) must be greater than zero. + /// If not set, a single summary item will be automatically generated using + /// `paymentAmount` and your configuration's `companyName`. + /// - seealso: PKPaymentRequest for more information + /// @note You should only set either this or `paymentAmount`, not both. + /// The other will be automatically calculated on demand using your `paymentCurrency.` + + @objc public var paymentSummaryItems: [PKPaymentSummaryItem] { + get { + return paymentAmountModel.paymentSummaryItems( + withCurrency: paymentCurrency, + companyName: configuration.companyName, + shippingMethod: selectedShippingMethod + ) ?? [] + } + set(paymentSummaryItems) { + paymentAmountModel = STPPaymentContextAmountModel( + paymentSummaryItems: paymentSummaryItems + ) + } + } + + /// A value that indicates whether Apple Pay Later is available for a transaction. + /// Defaults to enabled. + /// - Seealso: This property is mirrors `PKPaymentRequest.applePayLaterAvailability` +#if compiler(>=5.9) + @available(macOS 14.0, iOS 17.0, *) + @objc public var applePayLaterAvailability: PKApplePayLaterAvailability { + // Stored properties cannot be marked potentially unavailable with '@available', so do this workaround instead + get { + return _applePayLaterAvailability as! PKApplePayLaterAvailability + } + set { + _applePayLaterAvailability = newValue + + } + } + private lazy var _applePayLaterAvailability: Any? = { + if #available(macOS 14.0, iOS 17.0, *) { + return PKApplePayLaterAvailability.available + } + return nil + }() +#endif + /// The presentation style used for all view controllers presented modally by the context. + /// Since custom transition styles are not supported, you should set this to either + /// `UIModalPresentationFullScreen`, `UIModalPresentationPageSheet`, or `UIModalPresentationFormSheet`. + /// The default value is `UIModalPresentationFullScreen`. + @objc public var modalPresentationStyle: UIModalPresentationStyle = .fullScreen + /// The mode to use when displaying the title of the navigation bar in all view + /// controllers presented by the context. The default value is `automatic`, + /// which causes the title to use the same styling as the previously displayed + /// navigation item (if the view controller is pushed onto the `hostViewController`). + /// If the `prefersLargeTitles` property of the `hostViewController`'s navigation bar + /// is false, this property has no effect and the navigation item's title is always + /// displayed as a small title. + /// If the view controller is presented modally, `automatic` and + /// `never` always result in a navigation bar with a small title. + @objc public var largeTitleDisplayMode = UINavigationItem.LargeTitleDisplayMode.automatic + /// A view that will be placed as the footer of the payment options selection + /// view controller. + /// When the footer view needs to be resized, it will be sent a + /// `sizeThatFits:` call. The view should respond correctly to this method in order + /// to be sized and positioned properly. + @objc public var paymentOptionsViewControllerFooterView: UIView? + /// A view that will be placed as the footer of the add card view controller. + /// When the footer view needs to be resized, it will be sent a + /// `sizeThatFits:` call. The view should respond correctly to this method in order + /// to be sized and positioned properly. + @objc public var addCardViewControllerFooterView: UIView? + /// The API Client to use to make requests. + /// Defaults to STPAPIClient.shared + public var apiClient: STPAPIClient = .shared { + didSet { + analyticsLogger.apiClient = apiClient + } + } + internal let analyticsLogger: AnalyticsLogger = .init(product: STPPaymentContext.self) + internal var loadingStartDate: Date? + + /// If `paymentContext:didFailToLoadWithError:` is called on your delegate, you + /// can in turn call this method to try loading again (if that hasn't been called, + /// calling this will do nothing). If retrying in turn fails, `paymentContext:didFailToLoadWithError:` + /// will be called again (and you can again call this to keep retrying, etc). + @objc + public func retryLoading() { + let loadingStartDate = Date() + self.loadingStartDate = loadingStartDate + analyticsLogger.logLoadStarted() + // Clear any cached customer object and attached payment methods before refetching + if apiAdapter is STPCustomerContext { + let customerContext = apiAdapter as? STPCustomerContext + customerContext?.clearCache() + } + weak var weakSelf = self + loadingPromise = STPPromise.init().onSuccess({ tuple in + guard let strongSelf = weakSelf else { + return + } + strongSelf.analyticsLogger.logLoadSucceeded(loadStartDate: loadingStartDate, defaultPaymentOption: tuple.selectedPaymentOption) + strongSelf.paymentOptions = tuple.paymentOptions + strongSelf.selectedPaymentOption = tuple.selectedPaymentOption + }).onFailure({ error in + guard let strongSelf = weakSelf else { + return + } + strongSelf.analyticsLogger.logLoadFailed(loadStartDate: loadingStartDate, error: error) + if strongSelf.hostViewController != nil { + if strongSelf.paymentOptionsViewController != nil + && strongSelf.paymentOptionsViewController?.viewIfLoaded?.window != nil + { + if let paymentOptionsViewController1 = strongSelf.paymentOptionsViewController { + strongSelf.appropriatelyDismiss(paymentOptionsViewController1) { + strongSelf.delegate?.paymentContext( + strongSelf, + didFailToLoadWithError: error + ) + } + } + } else { + strongSelf.delegate?.paymentContext(strongSelf, didFailToLoadWithError: error) + } + } + }) + apiAdapter.retrieveCustomer({ customer, retrieveCustomerError in + stpDispatchToMainThreadIfNecessary({ + guard let strongSelf = weakSelf else { + return + } + if let retrieveCustomerError = retrieveCustomerError { + strongSelf.loadingPromise?.fail(retrieveCustomerError) + return + } + if strongSelf.shippingAddress == nil && customer?.shippingAddress != nil { + strongSelf.shippingAddress = customer?.shippingAddress + strongSelf.shippingAddressNeedsVerification = true + } + + strongSelf.apiAdapter.listPaymentMethodsForCustomer(completion: { + paymentMethods, + error in + guard let strongSelf2 = weakSelf else { + return + } + stpDispatchToMainThreadIfNecessary({ + if let error = error { + strongSelf2.loadingPromise?.fail(error) + return + } + + if self.defaultPaymentMethod == nil + && (strongSelf2.apiAdapter is STPCustomerContext) + { + // Retrieve the last selected payment method saved by STPCustomerContext + (strongSelf2.apiAdapter as? STPCustomerContext)? + .retrieveLastSelectedPaymentMethodIDForCustomer(completion: { + paymentMethodID, + _ in + guard let strongSelf3 = weakSelf else { + return + } + if let paymentMethods = paymentMethods { + let paymentTuple = STPPaymentOptionTuple( + filteredForUIWith: paymentMethods, + selectedPaymentMethod: paymentMethodID, + configuration: strongSelf3.configuration + ) + strongSelf3.loadingPromise?.succeed(paymentTuple) + } else { + strongSelf3.loadingPromise?.fail( + STPErrorCode.invalidRequestError as! Error + ) + } + }) + } else { + if let paymentMethods = paymentMethods { + let paymentTuple = STPPaymentOptionTuple( + filteredForUIWith: paymentMethods, + selectedPaymentMethod: self.defaultPaymentMethod, + configuration: strongSelf2.configuration + ) + strongSelf2.loadingPromise?.succeed(paymentTuple) + } + } + }) + }) + }) + }) + } + + /// This creates, configures, and appropriately presents an `STPPaymentOptionsViewController` + /// on top of the payment context's `hostViewController`. It'll be dismissed automatically + /// when the user is done selecting their payment method. + /// @note This method will do nothing if it is called while STPPaymentContext is + /// already showing a view controller or in the middle of requesting a payment. + @objc + public func presentPaymentOptionsViewController() { + presentPaymentOptionsViewController(withNewState: .showingRequestedViewController) + } + + /// This creates, configures, and appropriately pushes an `STPPaymentOptionsViewController` + /// onto the navigation stack of the context's `hostViewController`. It'll be popped + /// automatically when the user is done selecting their payment method. + /// @note This method will do nothing if it is called while STPPaymentContext is + /// already showing a view controller or in the middle of requesting a payment. + @objc + public func pushPaymentOptionsViewController() { + assert( + hostViewController != nil && hostViewController?.viewIfLoaded?.window != nil, + "hostViewController must not be nil on STPPaymentContext when calling pushPaymentOptionsViewController on it. Next time, set the hostViewController property first!" + ) + var navigationController: UINavigationController? + if hostViewController is UINavigationController { + navigationController = hostViewController as? UINavigationController + } else { + navigationController = hostViewController?.navigationController + } + assert( + navigationController != nil, + "The payment context's hostViewController is not a navigation controller, or is not contained in one. Either make sure it is inside a navigation controller before calling pushPaymentOptionsViewController, or call presentPaymentOptionsViewController instead." + ) + if state == STPPaymentContextState.none { + state = .showingRequestedViewController + + let paymentOptionsViewController = STPPaymentOptionsViewController(paymentContext: self) + self.paymentOptionsViewController = paymentOptionsViewController + paymentOptionsViewController.prefilledInformation = prefilledInformation + paymentOptionsViewController.defaultPaymentMethod = defaultPaymentMethod + paymentOptionsViewController.paymentOptionsViewControllerFooterView = + paymentOptionsViewControllerFooterView + paymentOptionsViewController.addCardViewControllerFooterView = + addCardViewControllerFooterView + paymentOptionsViewController.navigationItem.largeTitleDisplayMode = + largeTitleDisplayMode + + navigationController?.pushViewController( + paymentOptionsViewController, + animated: transitionAnimationsEnabled() + ) + } + } + + /// This creates, configures, and appropriately presents a view controller for + /// collecting shipping address and shipping method on top of the payment context's + /// `hostViewController`. It'll be dismissed automatically when the user is done + /// entering their shipping info. + /// @note This method will do nothing if it is called while STPPaymentContext is + /// already showing a view controller or in the middle of requesting a payment. + @objc + public func presentShippingViewController() { + presentShippingViewController(withNewState: .showingRequestedViewController) + } + + /// This creates, configures, and appropriately pushes a view controller for + /// collecting shipping address and shipping method onto the navigation stack of + /// the context's `hostViewController`. It'll be popped automatically when the + /// user is done entering their shipping info. + /// @note This method will do nothing if it is called while STPPaymentContext is + /// already showing a view controller, or in the middle of requesting a payment. + @objc + public func pushShippingViewController() { + assert( + hostViewController != nil && hostViewController?.viewIfLoaded?.window != nil, + "hostViewController must not be nil on STPPaymentContext when calling pushShippingViewController on it. Next time, set the hostViewController property first!" + ) + var navigationController: UINavigationController? + if hostViewController is UINavigationController { + navigationController = hostViewController as? UINavigationController + } else { + navigationController = hostViewController?.navigationController + } + assert( + navigationController != nil, + "The payment context's hostViewController is not a navigation controller, or is not contained in one. Either make sure it is inside a navigation controller before calling pushShippingInfoViewController, or call presentShippingInfoViewController instead." + ) + if state == STPPaymentContextState.none { + state = .showingRequestedViewController + + let addressViewController = STPShippingAddressViewController(paymentContext: self) + addressViewController.navigationItem.largeTitleDisplayMode = largeTitleDisplayMode + navigationController?.pushViewController( + addressViewController, + animated: transitionAnimationsEnabled() + ) + } + } + + /// Requests payment from the user. This may need to present some supplemental UI + /// to the user, in which case it will be presented on the payment context's + /// `hostViewController`. For instance, if they've selected Apple Pay as their + /// payment method, calling this method will show the payment sheet. If the user + /// has a card on file, this will use that without presenting any additional UI. + /// After this is called, the `paymentContext:didCreatePaymentResult:completion:` + /// and `paymentContext:didFinishWithStatus:error:` methods will be called on the + /// context's `delegate`. + /// @note This method will do nothing if it is called while STPPaymentContext is + /// already showing a view controller, or in the middle of requesting a payment. + @objc + public func requestPayment() { + weak var weakSelf = self + loadingPromise?.onSuccess({ _ in + guard let strongSelf = weakSelf else { + return + } + + if strongSelf.state != STPPaymentContextState.none { + return + } + + if strongSelf.selectedPaymentOption == nil { + strongSelf.presentPaymentOptionsViewController(withNewState: .requestingPayment) + } else if strongSelf.requestPaymentShouldPresentShippingViewController() { + strongSelf.presentShippingViewController(withNewState: .requestingPayment) + } else if let selectedPaymentOption = strongSelf.selectedPaymentOption, + (selectedPaymentOption is STPPaymentMethod || selectedPaymentOption is STPPaymentMethodParams) + { + strongSelf.state = .requestingPayment + let result = STPPaymentResult(paymentOption: strongSelf.selectedPaymentOption!) + strongSelf.delegate?.paymentContext(self, didCreatePaymentResult: result) { status, error in + // Note `selectedPaymentOption` is always an `STPPaymentMethod` for cards + strongSelf.analyticsLogger.logPayment(status: status, loadStartDate: strongSelf.loadingStartDate, paymentOption: selectedPaymentOption, error: error) + stpDispatchToMainThreadIfNecessary({ + strongSelf.didFinish(with: status, error: error) + }) + } + } else if let selectedPaymentOption = strongSelf.selectedPaymentOption, selectedPaymentOption is STPApplePayPaymentOption { + assert( + strongSelf.hostViewController != nil, + "hostViewController must not be nil on STPPaymentContext. Next time, set the hostViewController property first!" + ) + strongSelf.state = .requestingPayment + let paymentRequest = strongSelf.buildPaymentRequest() + let shippingAddressHandler: STPShippingAddressSelectionBlock = { + shippingAddress, + completion in + // Apple Pay always returns a partial address here, so we won't + // update self.shippingAddress or self.shippingMethods + if strongSelf.delegate?.responds( + to: #selector( + STPPaymentContextDelegate.paymentContext( + _: + didUpdateShippingAddress: + completion: + )) + ) + ?? false + { + strongSelf.delegate?.paymentContext?( + strongSelf, + didUpdateShippingAddress: shippingAddress + ) { status, _, shippingMethods, _ in + completion( + status, + shippingMethods ?? [], + strongSelf.paymentSummaryItems + ) + } + } else { + completion( + .valid, + strongSelf.shippingMethods ?? [], + strongSelf.paymentSummaryItems + ) + } + } + let shippingMethodHandler: STPShippingMethodSelectionBlock = { + shippingMethod, + completion in + strongSelf.selectedShippingMethod = shippingMethod + strongSelf.delegate?.paymentContextDidChange(strongSelf) + completion(self.paymentSummaryItems) + } + let paymentHandler: STPPaymentAuthorizationBlock = { payment in + strongSelf.selectedShippingMethod = payment.shippingMethod + if let shippingContact = payment.shippingContact { + strongSelf.shippingAddress = STPAddress(pkContact: shippingContact) + } + strongSelf.shippingAddressNeedsVerification = false + strongSelf.delegate?.paymentContextDidChange(strongSelf) + if strongSelf.apiAdapter is STPCustomerContext { + let customerContext = strongSelf.apiAdapter as? STPCustomerContext + if let shippingAddress1 = strongSelf.shippingAddress { + customerContext?.updateCustomer( + withShippingAddress: shippingAddress1, + completion: nil + ) + } + } + } + let applePayPaymentMethodHandler: STPApplePayPaymentMethodHandlerBlock = { + paymentMethod, + completion in + strongSelf.apiAdapter.attachPaymentMethod(toCustomer: paymentMethod) { + attachPaymentMethodError in + stpDispatchToMainThreadIfNecessary({ + if attachPaymentMethodError != nil { + completion(.error, attachPaymentMethodError) + } else { + let result = STPPaymentResult(paymentOption: paymentMethod) + strongSelf.delegate?.paymentContext( + strongSelf, + didCreatePaymentResult: result + ) { + status, + error in + // for Apple Pay, the didFinishWithStatus callback is fired later when Apple Pay VC finishes + completion(status, error) + } + } + }) + } + } + if let paymentRequest = paymentRequest { + strongSelf.applePayVC = PKPaymentAuthorizationViewController.stp_controller( + with: paymentRequest, + apiClient: strongSelf.apiClient, + onShippingAddressSelection: shippingAddressHandler, + onShippingMethodSelection: shippingMethodHandler, + onPaymentAuthorization: paymentHandler, + onPaymentMethodCreation: applePayPaymentMethodHandler, + onFinish: { status, error in + strongSelf.analyticsLogger.logPayment(status: status, loadStartDate: strongSelf.loadingStartDate, paymentOption: selectedPaymentOption, error: error) + if strongSelf.applePayVC?.presentingViewController != nil { + strongSelf.hostViewController?.dismiss( + animated: strongSelf.transitionAnimationsEnabled() + ) { + strongSelf.didFinish(with: status, error: error) + } + } else { + strongSelf.didFinish(with: status, error: error) + } + strongSelf.applePayVC = nil + } + ) + } + if let applePayVC1 = strongSelf.applePayVC { + strongSelf.hostViewController?.present( + applePayVC1, + animated: strongSelf.transitionAnimationsEnabled() + ) + } + } + }).onFailure({ error in + guard let strongSelf = weakSelf else { + return + } + strongSelf.didFinish(with: .error, error: error) + }) + } + private var loadingPromise: STPPromise? + private weak var paymentOptionsViewController: STPPaymentOptionsViewController? + private var state: STPPaymentContextState = .none + private var paymentAmountModel = STPPaymentContextAmountModel(amount: 0) + private var shippingAddressNeedsVerification = false + // If hostViewController was set to a nav controller, the original VC on top of the stack + private weak var originalTopViewController: UIViewController? + private var applePayVC: PKPaymentAuthorizationViewController? + + // Disable transition animations in tests + func transitionAnimationsEnabled() -> Bool { + return NSClassFromString("XCTest") == nil + } + + var currentValuePromise: STPPromise { + weak var weakSelf = self + return + (loadingPromise?.map({ _ in + guard let strongSelf = weakSelf, let paymentOptions = strongSelf.paymentOptions + else { + return STPPaymentOptionTuple() + } + return STPPaymentOptionTuple( + paymentOptions: paymentOptions, + selectedPaymentOption: strongSelf.selectedPaymentOption + ) + }))! + } + + func remove(_ paymentOptionToRemove: STPPaymentOption?) { + // Remove payment method from cached representation + var paymentOptions = self.paymentOptions + paymentOptions?.removeAll { $0 as AnyObject === paymentOptionToRemove as AnyObject } + self.paymentOptions = paymentOptions + + // Elect new selected payment method if needed + if let selectedPaymentOption = selectedPaymentOption, + selectedPaymentOption.isEqual(paymentOptionToRemove) + { + self.selectedPaymentOption = self.paymentOptions?.first + } + } + + // MARK: - Payment Methods + + func presentPaymentOptionsViewController(withNewState state: STPPaymentContextState) { + assert( + hostViewController != nil && hostViewController?.viewIfLoaded?.window != nil, + "hostViewController must not be nil on STPPaymentContext when calling pushPaymentOptionsViewController on it. Next time, set the hostViewController property first!" + ) + if self.state == STPPaymentContextState.none { + self.state = state + let paymentOptionsViewController = STPPaymentOptionsViewController(paymentContext: self) + self.paymentOptionsViewController = paymentOptionsViewController + paymentOptionsViewController.prefilledInformation = prefilledInformation + paymentOptionsViewController.defaultPaymentMethod = defaultPaymentMethod + paymentOptionsViewController.paymentOptionsViewControllerFooterView = + paymentOptionsViewControllerFooterView + paymentOptionsViewController.addCardViewControllerFooterView = + addCardViewControllerFooterView + paymentOptionsViewController.navigationItem.largeTitleDisplayMode = + largeTitleDisplayMode + + let navigationController = UINavigationController( + rootViewController: paymentOptionsViewController + ) + navigationController.navigationBar.stp_theme = theme + navigationController.navigationBar.prefersLargeTitles = true + navigationController.modalPresentationStyle = modalPresentationStyle + hostViewController?.present( + navigationController, + animated: transitionAnimationsEnabled() + ) + } + } + + @objc + public func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didSelect paymentOption: STPPaymentOption + ) { + selectedPaymentOption = paymentOption + } + + @objc + public func paymentOptionsViewControllerDidFinish( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + appropriatelyDismiss(paymentOptionsViewController) { + if self.state == .requestingPayment { + self.state = STPPaymentContextState.none + self.requestPayment() + } else { + self.state = STPPaymentContextState.none + } + } + } + + @objc + public func paymentOptionsViewControllerDidCancel( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + appropriatelyDismiss(paymentOptionsViewController) { + if self.state == .requestingPayment { + self.didFinish( + with: .userCancellation, + error: nil + ) + } else { + self.state = STPPaymentContextState.none + } + } + } + + @objc + public func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didFailToLoadWithError error: Error + ) { + // we'll handle this ourselves when the loading promise fails. + } + + @objc(appropriatelyDismissPaymentOptionsViewController:completion:) func appropriatelyDismiss( + _ viewController: STPPaymentOptionsViewController, + completion: @escaping STPVoidBlock + ) { + if viewController.stp_isAtRootOfNavigationController() { + // if we're the root of the navigation controller, we've been presented modally. + viewController.presentingViewController?.dismiss( + animated: transitionAnimationsEnabled() + ) { + self.paymentOptionsViewController = nil + completion() + } + } else { + // otherwise, we've been pushed onto the stack. + var destinationViewController = hostViewController + // If hostViewController is a nav controller, pop to the original VC on top of the stack. + if hostViewController is UINavigationController { + destinationViewController = originalTopViewController + } + viewController.navigationController?.stp_pop( + to: destinationViewController, + animated: transitionAnimationsEnabled() + ) { + self.paymentOptionsViewController = nil + completion() + } + } + } + + // MARK: - Shipping Info + + func presentShippingViewController(withNewState state: STPPaymentContextState) { + assert( + hostViewController != nil && hostViewController?.viewIfLoaded?.window != nil, + "hostViewController must not be nil on STPPaymentContext when calling presentShippingViewController on it. Next time, set the hostViewController property first!" + ) + + if self.state == STPPaymentContextState.none { + self.state = state + + let addressViewController = STPShippingAddressViewController(paymentContext: self) + addressViewController.navigationItem.largeTitleDisplayMode = largeTitleDisplayMode + let navigationController = UINavigationController( + rootViewController: addressViewController + ) + navigationController.navigationBar.stp_theme = theme + navigationController.navigationBar.prefersLargeTitles = true + navigationController.modalPresentationStyle = modalPresentationStyle + hostViewController?.present( + navigationController, + animated: transitionAnimationsEnabled() + ) + } + } + + @objc + public func shippingAddressViewControllerDidCancel( + _ addressViewController: STPShippingAddressViewController + ) { + appropriatelyDismiss(addressViewController) { + if self.state == .requestingPayment { + self.didFinish( + with: .userCancellation, + error: nil + ) + } else { + self.state = STPPaymentContextState.none + } + } + } + + @objc + public func shippingAddressViewController( + _ addressViewController: STPShippingAddressViewController, + didEnter address: STPAddress, + completion: @escaping STPShippingMethodsCompletionBlock + ) { + if delegate?.responds( + to: #selector( + STPPaymentContextDelegate.paymentContext(_:didUpdateShippingAddress:completion:)) + ) + ?? false + { + delegate?.paymentContext?(self, didUpdateShippingAddress: address) { + status, + shippingValidationError, + shippingMethods, + selectedMethod in + self.shippingMethods = shippingMethods + completion(status, shippingValidationError, shippingMethods, selectedMethod) + } + } else { + completion(.valid, nil, nil, nil) + } + } + + @objc + public func shippingAddressViewController( + _ addressViewController: STPShippingAddressViewController, + didFinishWith address: STPAddress, + shippingMethod method: PKShippingMethod? + ) { + shippingAddress = address + shippingAddressNeedsVerification = false + selectedShippingMethod = method + delegate?.paymentContextDidChange(self) + if apiAdapter.responds( + to: #selector(STPCustomerContext.updateCustomer(withShippingAddress:completion:)) + ) { + if let shippingAddress = shippingAddress { + apiAdapter.updateCustomer?(withShippingAddress: shippingAddress, completion: nil) + } + } + appropriatelyDismiss(addressViewController) { + if self.state == .requestingPayment { + self.state = STPPaymentContextState.none + self.requestPayment() + } else { + self.state = STPPaymentContextState.none + } + } + } + + @objc(appropriatelyDismissViewController:completion:) func appropriatelyDismiss( + _ viewController: UIViewController, + completion: @escaping STPVoidBlock + ) { + if viewController.stp_isAtRootOfNavigationController() { + // if we're the root of the navigation controller, we've been presented modally. + viewController.presentingViewController?.dismiss( + animated: transitionAnimationsEnabled() + ) { + completion() + } + } else { + // otherwise, we've been pushed onto the stack. + var destinationViewController = hostViewController + // If hostViewController is a nav controller, pop to the original VC on top of the stack. + if hostViewController is UINavigationController { + destinationViewController = originalTopViewController + } + viewController.navigationController?.stp_pop( + to: destinationViewController, + animated: transitionAnimationsEnabled() + ) { + completion() + } + } + } + + // MARK: - Request Payment + func requestPaymentShouldPresentShippingViewController() -> Bool { + let shippingAddressRequired = (configuration.requiredShippingAddressFields?.count ?? 0) > 0 + var shippingAddressIncomplete: Bool? + if let requiredShippingAddressFields1 = configuration.requiredShippingAddressFields { + shippingAddressIncomplete = + !(shippingAddress?.containsRequiredShippingAddressFields( + requiredShippingAddressFields1 + ) + ?? false) + } + let shippingMethodRequired = + configuration.shippingType == .shipping + && delegate?.responds( + to: #selector( + STPPaymentContextDelegate.paymentContext(_:didUpdateShippingAddress:completion:) + ) + ) + ?? false + && selectedShippingMethod == nil + let verificationRequired = + configuration.verifyPrefilledShippingAddress && shippingAddressNeedsVerification + // true if STPShippingVC should be presented to collect or verify a shipping address + let shouldPresentShippingAddress = + shippingAddressRequired && (shippingAddressIncomplete ?? false || verificationRequired) + // this handles a corner case where STPShippingVC should be presented because: + // - shipping address has been pre-filled + // - no verification is required, but the user still needs to enter a shipping method + let shouldPresentShippingMethods = + shippingAddressRequired && !(shippingAddressIncomplete ?? false) + && !verificationRequired + && shippingMethodRequired + return shouldPresentShippingAddress || shouldPresentShippingMethods + } + + func didFinish( + with status: STPPaymentStatus, + error: Error? + ) { + state = STPPaymentContextState.none + delegate?.paymentContext( + self, + didFinishWith: status, + error: error + ) + } + + func buildPaymentRequest() -> PKPaymentRequest? { + guard let appleMerchantIdentifier = configuration.appleMerchantIdentifier, paymentAmount > 0 + else { + return nil + } + let paymentRequest = StripeAPI.paymentRequest( + withMerchantIdentifier: appleMerchantIdentifier, + country: paymentCountry, + currency: paymentCurrency + ) + +#if compiler(>=5.9) + if #available(macOS 14.0, iOS 17.0, *) { + paymentRequest.applePayLaterAvailability = applePayLaterAvailability._convertedToSwiftValue() + } +#endif + + let summaryItems = paymentSummaryItems + paymentRequest.paymentSummaryItems = summaryItems + + let requiredFields = STPAddress.applePayContactFields( + from: configuration.requiredBillingAddressFields + ) + paymentRequest.requiredBillingContactFields = requiredFields + + var shippingRequiredFields: Set? + if let requiredShippingAddressFields1 = configuration.requiredShippingAddressFields { + shippingRequiredFields = STPAddress.pkContactFields( + fromStripeContactFields: requiredShippingAddressFields1 + ) + } + if let shippingRequiredFields = shippingRequiredFields { + paymentRequest.requiredShippingContactFields = shippingRequiredFields + } + + paymentRequest.currencyCode = paymentCurrency.uppercased() + if let selectedShippingMethod = selectedShippingMethod { + var orderedShippingMethods = shippingMethods + orderedShippingMethods?.removeAll { + $0 as AnyObject === selectedShippingMethod as AnyObject + } + orderedShippingMethods?.insert(selectedShippingMethod, at: 0) + paymentRequest.shippingMethods = orderedShippingMethods + } else { + paymentRequest.shippingMethods = shippingMethods + } + + paymentRequest.shippingType = STPPaymentContext.pkShippingType(configuration.shippingType) + + if let shippingAddress = shippingAddress { + paymentRequest.shippingContact = shippingAddress.pkContactValue() + } + return paymentRequest + } + + class func pkShippingType(_ shippingType: STPShippingType) -> PKShippingType { + switch shippingType { + case .shipping: + return .shipping + case .delivery: + return .delivery + @unknown default: + fatalError() + } + } + + func artificiallyRetain(_ host: NSObject) { + objc_setAssociatedObject( + host, + UnsafeRawPointer(&kSTPPaymentCoordinatorAssociatedObjectKey), + self, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + + // MARK: - STPAuthenticationContext + @objc + public func authenticationPresentingViewController() -> UIViewController { + return hostViewController! + } + + @objc + public func prepare(forPresentation completion: @escaping STPVoidBlock) { + if applePayVC != nil && applePayVC?.presentingViewController != nil { + hostViewController?.dismiss( + animated: transitionAnimationsEnabled() + ) { + completion() + } + } else { + completion() + } + } +} + +/// Implement `STPPaymentContextDelegate` to get notified when a payment context changes, finishes, encounters errors, etc. In practice, if your app has a "checkout screen view controller", that is a good candidate to implement this protocol. +@objc public protocol STPPaymentContextDelegate: NSObjectProtocol { + /// Called when the payment context encounters an error when fetching its initial set of data. A few ways to handle this are: + /// - If you're showing the user a checkout page, dismiss the checkout page when this is called and present the error to the user. + /// - Present the error to the user using a `UIAlertController` with two buttons: Retry and Cancel. If they cancel, dismiss your UI. If they Retry, call `retryLoading` on the payment context. + /// To make it harder to get your UI into a bad state, this won't be called until the context's `hostViewController` has finished appearing. + /// - Parameters: + /// - paymentContext: the payment context that encountered the error + /// - error: the error that was encountered + func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) + /// This is called every time the contents of the payment context change. When this is called, you should update your app's UI to reflect the current state of the payment context. For example, if you have a checkout page with a "selected payment method" row, you should update its payment method with `paymentContext.selectedPaymentOption.label`. If that checkout page has a "buy" button, you should enable/disable it depending on the result of `paymentContext.isReadyForPayment`. + /// - Parameter paymentContext: the payment context that changed + func paymentContextDidChange(_ paymentContext: STPPaymentContext) + /// Inside this method, you should make a call to your backend API to make a PaymentIntent with that Customer + payment method, and invoke the `completion` block when that is done. + /// - Parameters: + /// - paymentContext: The context that succeeded + /// - paymentResult: Information associated with the payment that you can pass to your server. You should go to your backend API with this payment result and use the PaymentIntent API to complete the payment. See https://stripe.com/docs/mobile/ios/basic#submit-payment-intents Once that's done call the `completion` block with any error that occurred (or none, if the payment succeeded). - seealso: STPPaymentResult.h + /// - completion: Call this block when you're done creating a payment intent (or subscription, etc) on your backend. If it succeeded, call `completion(STPPaymentStatusSuccess, nil)`. If it failed with an error, call `completion(STPPaymentStatusError, error)`. If the user canceled, call `completion(STPPaymentStatusUserCancellation, nil)`. + func paymentContext( + _ paymentContext: STPPaymentContext, + didCreatePaymentResult paymentResult: STPPaymentResult, + completion: @escaping STPPaymentStatusBlock + ) + /// This is invoked by an `STPPaymentContext` when it is finished. This will be called after the payment is done and all necessary UI has been dismissed. You should inspect the returned `status` and behave appropriately. For example: if it's `STPPaymentStatusSuccess`, show the user a receipt. If it's `STPPaymentStatusError`, inform the user of the error. If it's `STPPaymentStatusUserCancellation`, do nothing. + /// - Parameters: + /// - paymentContext: The payment context that finished + /// - status: The status of the payment - `STPPaymentStatusSuccess` if it succeeded, `STPPaymentStatusError` if it failed with an error (in which case the `error` parameter will be non-nil), `STPPaymentStatusUserCancellation` if the user canceled the payment. + /// - error: An error that occurred, if any. + func paymentContext( + _ paymentContext: STPPaymentContext, + didFinishWith status: STPPaymentStatus, + error: Error? + ) + + /// Inside this method, you should verify that you can ship to the given address. + /// You should call the completion block with the results of your validation + /// and the available shipping methods for the given address. If you don't implement + /// this method, the user won't be prompted to select a shipping method and all + /// addresses will be valid. If you call the completion block with nil or an + /// empty array of shipping methods, the user won't be prompted to select a + /// shipping method. + /// @note If a user updates their shipping address within the Apple Pay dialog, + /// this address will be anonymized. For example, in the US, it will only include the + /// city, state, and zip code. The payment context will have the user's complete + /// shipping address by the time `paymentContext:didFinishWithStatus:error` is + /// called. + /// - Parameters: + /// - paymentContext: The context that updated its shipping address + /// - address: The current shipping address + /// - completion: Call this block when you're done validating the shipping + /// address and calculating available shipping methods. If you call the completion + /// block with nil or an empty array of shipping methods, the user won't be prompted + /// to select a shipping method. + @objc optional func paymentContext( + _ paymentContext: STPPaymentContext, + didUpdateShippingAddress address: STPAddress, + completion: @escaping STPShippingMethodsCompletionBlock + ) +} + +/// The current state of the payment context +/// - STPPaymentContextStateNone: No view controllers are currently being shown. The payment may or may not have already been completed +/// - STPPaymentContextStateShowingRequestedViewController: The view controller that you requested the context show is being shown (via the push or present payment methods or shipping view controller methods) +/// - STPPaymentContextStateRequestingPayment: The payment context is in the middle of requesting payment. It may be showing some other UI or view controller if more information is necessary to complete the payment. +enum STPPaymentContextState: Int { + case none + case showingRequestedViewController + case requestingPayment +} + +private var kSTPPaymentCoordinatorAssociatedObjectKey = 0 + +/// :nodoc: +@_spi(STP) extension STPPaymentContext: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier = "STPPaymentContext" +} + +#if compiler(>=5.9) +@available(macOS 14.0, iOS 17.0, *) +extension PKApplePayLaterAvailability { + func _convertedToSwiftValue() -> PKPaymentRequest.ApplePayLaterAvailability { + switch self { + case .available: + return .available + case .unavailableItemIneligible: + return .unavailable(.itemIneligible) + case .unavailableRecurringTransaction: + return .unavailable(.recurringTransaction) + @unknown default: + fatalError() + } + } +} +#endif diff --git a/Stripe/StripeiOS/Source/STPPaymentContextAmountModel.swift b/Stripe/StripeiOS/Source/STPPaymentContextAmountModel.swift new file mode 100644 index 00000000..c79c13e2 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentContextAmountModel.swift @@ -0,0 +1,96 @@ +// +// STPPaymentContextAmountModel.swift +// StripeiOS +// +// Created by Brian Dorfman on 8/16/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripePayments + +/// Internal model for STPPaymentContext's `paymentAmount` and +/// `paymentSummaryItems` properties. +class STPPaymentContextAmountModel: NSObject { + private var paymentAmount = 0 + private var paymentSummaryItems: [PKPaymentSummaryItem]? + + init( + amount paymentAmount: Int + ) { + super.init() + self.paymentAmount = paymentAmount + paymentSummaryItems = nil + } + + init( + paymentSummaryItems: [PKPaymentSummaryItem]? + ) { + super.init() + paymentAmount = 0 + self.paymentSummaryItems = paymentSummaryItems + } + + func paymentAmount(withCurrency currency: String?, shippingMethod: PKShippingMethod?) -> Int { + let shippingAmount = + ((shippingMethod != nil) + ? shippingMethod?.amount.stp_amount(withCurrency: currency) : 0) ?? 0 + if paymentSummaryItems == nil { + return paymentAmount + shippingAmount + } else { + let lastItem = paymentSummaryItems?.last + return (lastItem?.amount.stp_amount(withCurrency: currency) ?? 0) + shippingAmount + } + } + + func paymentSummaryItems( + withCurrency currency: String?, + companyName: String?, + shippingMethod: PKShippingMethod? + ) -> [PKPaymentSummaryItem]? { + var shippingItem: PKPaymentSummaryItem? + if let shippingMethod = shippingMethod { + shippingItem = PKPaymentSummaryItem( + label: shippingMethod.label, + amount: shippingMethod.amount + ) + } + if paymentSummaryItems == nil { + let shippingAmount = shippingMethod?.amount.stp_amount(withCurrency: currency) ?? 0 + let total = NSDecimalNumber.stp_decimalNumber( + withAmount: paymentAmount + shippingAmount, + currency: currency + ) + let totalItem = PKPaymentSummaryItem(label: companyName ?? "", amount: total) + var items = [totalItem] + if let shippingItem = shippingItem { + items.insert(shippingItem, at: 0) + } + return items.compactMap { $0 } + } else { + if (paymentSummaryItems?.count ?? 0) > 0 && shippingItem != nil { + var items = paymentSummaryItems + let origTotalItem = items?.last + var newTotal: NSDecimalNumber? + if let amount1 = shippingItem?.amount { + newTotal = origTotalItem?.amount.adding(amount1) + } + var totalItem: PKPaymentSummaryItem? + if let newTotal = newTotal { + totalItem = PKPaymentSummaryItem( + label: origTotalItem?.label ?? "", + amount: newTotal + ) + } + items?.removeLast() + if let items = items { + return items + [shippingItem, totalItem].compactMap { $0 } + } + return nil + } else { + return paymentSummaryItems + } + } + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentIntentParams+BasicUI.swift b/Stripe/StripeiOS/Source/STPPaymentIntentParams+BasicUI.swift new file mode 100644 index 00000000..007367db --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentIntentParams+BasicUI.swift @@ -0,0 +1,22 @@ +// +// STPPaymentIntentParams+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation + +extension STPPaymentIntentParams { + /// Provide an STPPaymentResult from STPPaymentContext, and this will populate + /// the proper field (either paymentMethodId or paymentMethodParams) for your PaymentMethod. + @objc + public func configure(with paymentResult: STPPaymentResult) { + if let paymentMethod = paymentResult.paymentMethod { + paymentMethodId = paymentMethod.stripeId + } else if let params = paymentResult.paymentMethodParams { + paymentMethodParams = params + } + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentMethod+BasicUI.swift b/Stripe/StripeiOS/Source/STPPaymentMethod+BasicUI.swift new file mode 100644 index 00000000..b99758bc --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentMethod+BasicUI.swift @@ -0,0 +1,94 @@ +// +// STPPaymentMethod+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +import UIKit + +extension STPPaymentMethod: STPPaymentOption { + // MARK: - STPPaymentOption + @objc public var image: UIImage { + if type == .card, let card = card { + return STPImageLibrary.cardBrandImage(for: card.preferredDisplayBrand) + } else { + return STPImageLibrary.cardBrandImage(for: .unknown) + } + } + + @objc public var templateImage: UIImage { + if type == .card, let card = card { + return STPImageLibrary.templatedBrandImage(for: card.preferredDisplayBrand) + } else { + return STPImageLibrary.templatedBrandImage(for: .unknown) + } + } + + @objc public var label: String { + switch type { + case .card: + if let card = card { + let brand = STPCardBrandUtilities.stringFrom(card.preferredDisplayBrand) + return "\(brand ?? "") \(card.last4 ?? "")" + } else { + return STPCardBrandUtilities.stringFrom(.unknown) ?? "" + } + case .FPX: + if let fpx = fpx { + return STPFPXBank.stringFrom(STPFPXBank.brandFrom(fpx.bankIdentifierCode)) ?? "" + } else { + fallthrough + } + case .USBankAccount: + if let usBankAccount = usBankAccount { + return String( + format: String.Localized.bank_name_account_ending_in_last_4, + usBankAccount.bankName, + usBankAccount.last4 + ) + } else { + fallthrough + } + default: + return type.displayName + } + } + + @objc public var isReusable: Bool { + switch type { + case .card, .link, .USBankAccount: + return true + case .alipay, // Careful! Revisit this if/when we support recurring Alipay + .AUBECSDebit, + .bacsDebit, .SEPADebit, .iDEAL, .FPX, .cardPresent, .giropay, .EPS, .payPal, + .przelewy24, .bancontact, + .OXXO, .sofort, .grabPay, .netBanking, .UPI, .afterpayClearpay, .blik, + .weChatPay, .boleto, .klarna, .affirm, .cashApp, .paynow, .zip, .revolutPay, .amazonPay, + .alma, .mobilePay, .konbini, .promptPay, .swish, .twint, .multibanco, .sunbit, .billie, + .satispay, + .unknown: + return false + + @unknown default: + return false + } + } +} + +extension String { + var toCardBrand: STPCardBrand? { + return STPCard.brand(from: self) + } +} + +extension STPPaymentMethodCard { + var preferredDisplayBrand: STPCardBrand { + return networks?.preferred?.toCardBrand ?? displayBrand?.toCardBrand ?? brand + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentMethodParams+BasicUI.swift b/Stripe/StripeiOS/Source/STPPaymentMethodParams+BasicUI.swift new file mode 100644 index 00000000..197f9c01 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentMethodParams+BasicUI.swift @@ -0,0 +1,57 @@ +// +// STPPaymentMethodParams+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +import UIKit + +extension STPPaymentMethodParams: STPPaymentOption { + // MARK: - STPPaymentOption + @objc public var image: UIImage { + if type == .card && card != nil { + let brand = card?.preferredDisplayBrand ?? .unknown + return STPImageLibrary.cardBrandImage(for: brand) + } else { + return STPImageLibrary.cardBrandImage(for: .unknown) + } + } + + @objc public var templateImage: UIImage { + if type == .card && card != nil { + let brand = card?.preferredDisplayBrand ?? .unknown + return STPImageLibrary.templatedBrandImage(for: brand) + } else if type == .FPX { + return STPImageLibrary.bankIcon() + } else { + return STPImageLibrary.templatedBrandImage(for: .unknown) + } + } + + @objc public var isReusable: Bool { + switch type { + case .card, .link, .USBankAccount: + return true + case .alipay, .AUBECSDebit, .bacsDebit, .SEPADebit, .iDEAL, .FPX, .cardPresent, .giropay, + .grabPay, .EPS, .przelewy24, .bancontact, .netBanking, .OXXO, .payPal, .sofort, .UPI, + .afterpayClearpay, .blik, .weChatPay, .boleto, .klarna, .affirm, .cashApp, .paynow, + .zip, .revolutPay, .amazonPay, .alma, .mobilePay, .konbini, .promptPay, .swish, .twint, + .multibanco, .sunbit, .billie, .satispay, + .unknown: + return false + @unknown default: + return false + } + } +} + +extension STPPaymentMethodCardParams { + var preferredDisplayBrand: STPCardBrand { + return networks?.preferred?.toCardBrand ?? STPCardValidator.brand(forNumber: number ?? "") + } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentOption.swift b/Stripe/StripeiOS/Source/STPPaymentOption.swift new file mode 100644 index 00000000..278c3d38 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentOption.swift @@ -0,0 +1,36 @@ +// +// STPPaymentOption.swift +// StripeiOS +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +/// This protocol represents a payment method that a user can select and use to +/// pay. +/// The classes that conform to it and are supported by the UI: +/// - `STPApplePay`, which represents that the user wants to pay with +/// Apple Pay +/// - `STPPaymentMethod`. Only `STPPaymentMethod.type == STPPaymentMethodTypeCard` and +/// `STPPaymentMethod.type == STPPaymentMethodTypeFPX` are supported by `STPPaymentContext` +/// and `STPPaymentOptionsViewController` +/// - `STPPaymentMethodParams`. This should be used with non-reusable payment method, such +/// as FPX and iDEAL. Instead of reaching out to Stripe to create a PaymentMethod, you can +/// pass an STPPaymentMethodParams directly to Stripe when confirming a PaymentIntent. +/// @note card-based Sources, Cards, and FPX support this protocol for use +/// in a custom integration. +@objc public protocol STPPaymentOption: NSObjectProtocol { + /// A small (32 x 20 points) logo image representing the payment method. For + /// example, the Visa logo for a Visa card, or the Apple Pay logo. + var image: UIImage { get } + /// A small (32 x 20 points) logo image representing the payment method that can be + /// used as template for tinted icons. + var templateImage: UIImage { get } + /// A string describing the payment method, such as "Apple Pay" or "Visa 4242". + var label: String { get } + /// Describes whether this payment option may be used multiple times. If it is not reusable, + /// the payment method must be discarded after use. + var isReusable: Bool { get } +} diff --git a/Stripe/StripeiOS/Source/STPPaymentOptionTableViewCell.swift b/Stripe/StripeiOS/Source/STPPaymentOptionTableViewCell.swift new file mode 100644 index 00000000..9d6bb81c --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentOptionTableViewCell.swift @@ -0,0 +1,326 @@ +// +// STPPaymentOptionTableViewCell.swift +// StripeiOS +// +// Created by Ben Guo on 8/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +import UIKit + +class STPPaymentOptionTableViewCell: UITableViewCell { + @objc(configureForNewCardRowWithTheme:) func configureForNewCardRow(with theme: STPTheme) { + paymentOption = nil + self.theme = theme + + backgroundColor = theme.secondaryBackgroundColor + + // Left icon + leftIcon.image = STPLegacyImageLibrary.addIcon() + leftIcon.tintColor = theme.accentColor + + // Title label + titleLabel.font = theme.font + titleLabel.textColor = theme.accentColor + titleLabel.text = STPLocalizedString("Add New Card…", "Button to add a new credit card.") + + // Checkmark icon + checkmarkIcon.isHidden = true + + setNeedsLayout() + } + + @objc(configureWithPaymentOption:theme:selected:) func configure( + with paymentOption: STPPaymentOption?, + theme: STPTheme, + selected: Bool + ) { + self.paymentOption = paymentOption + self.theme = theme + + backgroundColor = theme.secondaryBackgroundColor + + // Left icon + leftIcon.image = paymentOption?.templateImage + leftIcon.tintColor = primaryColorForPaymentOption(withSelected: selected) + + // Title label + titleLabel.font = theme.font + titleLabel.attributedText = buildAttributedString(with: paymentOption, selected: selected) + + // Checkmark icon + checkmarkIcon.tintColor = theme.accentColor + checkmarkIcon.isHidden = !selected + + // Accessibility + if selected { + accessibilityTraits.insert(.selected) + } else { + accessibilityTraits.remove(.selected) + } + + setNeedsLayout() + } + + @objc(configureForFPXRowWithTheme:) func configureForFPXRow(with theme: STPTheme) { + paymentOption = nil + self.theme = theme + + backgroundColor = theme.secondaryBackgroundColor + + // Left icon + leftIcon.image = STPImageLibrary.bankIcon() + leftIcon.tintColor = primaryColorForPaymentOption(withSelected: false) + + // Title label + titleLabel.font = theme.font + titleLabel.textColor = self.theme.primaryForegroundColor + titleLabel.text = STPLocalizedString( + "Online Banking (FPX)", + "Button to pay with a Bank Account (using FPX)." + ) + + // Checkmark icon + checkmarkIcon.isHidden = true + accessoryType = .disclosureIndicator + setNeedsLayout() + } + + private var paymentOption: STPPaymentOption? + private var theme: STPTheme = .defaultTheme + private var leftIcon = UIImageView() + private var titleLabel = UILabel() + private var checkmarkIcon = UIImageView(image: STPLegacyImageLibrary.checkmarkIcon()) + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + // Left icon + leftIcon.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(leftIcon) + + // Title label + titleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(titleLabel) + + // Checkmark icon + checkmarkIcon.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkmarkIcon) + + NSLayoutConstraint.activate( + [ + self.leftIcon.centerXAnchor.constraint( + equalTo: contentView.leadingAnchor, + constant: kPadding + 0.5 * kDefaultIconWidth + ), + self.leftIcon.centerYAnchor.constraint( + lessThanOrEqualTo: contentView.centerYAnchor + ), + self.checkmarkIcon.widthAnchor.constraint(equalToConstant: kCheckmarkWidth), + self.checkmarkIcon.heightAnchor.constraint( + equalTo: self.checkmarkIcon.widthAnchor, + multiplier: 1.0 + ), + self.checkmarkIcon.centerXAnchor.constraint( + equalTo: contentView.trailingAnchor, + constant: -kPadding + ), + self.checkmarkIcon.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + // Constrain label to leadingAnchor with the default + // icon width so that the text always aligns vertically + // even if the icond widths differ + self.titleLabel.leadingAnchor.constraint( + equalTo: contentView.leadingAnchor, + constant: 2.0 * kPadding + kDefaultIconWidth + ), + self.titleLabel.trailingAnchor.constraint( + equalTo: self.checkmarkIcon.leadingAnchor, + constant: -kPadding + ), + self.titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + accessibilityTraits.insert(.button) + isAccessibilityElement = true + } + + func primaryColorForPaymentOption(withSelected selected: Bool) -> UIColor { + let fadedColor: UIColor = UIColor(dynamicProvider: { _ in + return self.theme.primaryForegroundColor.withAlphaComponent(0.6) + }) + + return (selected ? theme.accentColor : fadedColor) + } + + func buildAttributedString( + with paymentOption: STPPaymentOption?, + selected: Bool + ) + -> NSAttributedString + { + if let paymentOption = paymentOption as? STPCard { + return buildAttributedString(with: paymentOption, selected: selected) + } else if let source = paymentOption as? STPSource { + if source.type == .card && source.cardDetails != nil { + return buildAttributedString(withCardSource: source, selected: selected) + } + } else if let paymentMethod = paymentOption as? STPPaymentMethod { + if paymentMethod.type == .card && paymentMethod.card != nil { + return buildAttributedString( + withCardPaymentMethod: paymentMethod, + selected: selected + ) + } + if paymentMethod.type == .FPX && paymentMethod.fpx != nil { + return buildAttributedString( + with: STPFPXBank.brandFrom(paymentMethod.fpx?.bankIdentifierCode), + selected: selected + ) + } + } else if paymentOption is STPApplePayPaymentOption { + let label = String.Localized.apple_pay + let primaryColor = primaryColorForPaymentOption(withSelected: selected) + return NSAttributedString( + string: label, + attributes: [ + NSAttributedString.Key.foregroundColor: primaryColor + ] + ) + } else if let paymentMethodParams = paymentOption as? STPPaymentMethodParams { + if paymentMethodParams.type == .card && paymentMethodParams.card != nil { + return buildAttributedString( + withCardPaymentMethodParams: paymentMethodParams, + selected: selected + ) + } + if paymentMethodParams.type == .FPX && paymentMethodParams.fpx != nil { + return buildAttributedString( + with: paymentMethodParams.fpx?.bank ?? STPFPXBankBrand.unknown, + selected: selected + ) + } + } + + // Unrecognized payment method + return NSAttributedString(string: "") + } + + func buildAttributedString(with card: STPCard, selected: Bool) -> NSAttributedString { + return buildAttributedString( + with: card.brand, + last4: card.last4, + selected: selected + ) + } + + func buildAttributedString(withCardSource card: STPSource, selected: Bool) -> NSAttributedString + { + return buildAttributedString( + with: card.cardDetails?.brand ?? .unknown, + last4: card.cardDetails?.last4 ?? "", + selected: selected + ) + } + + func buildAttributedString( + withCardPaymentMethod paymentMethod: STPPaymentMethod, + selected: Bool + ) + -> NSAttributedString + { + return buildAttributedString( + with: paymentMethod.card?.preferredDisplayBrand ?? .unknown, + last4: paymentMethod.card?.last4 ?? "", + selected: selected + ) + } + + func buildAttributedString( + withCardPaymentMethodParams paymentMethodParams: STPPaymentMethodParams, + selected: Bool + ) -> NSAttributedString { + let brand = paymentMethodParams.card?.preferredDisplayBrand ?? .unknown + return buildAttributedString( + with: brand, + last4: paymentMethodParams.card?.last4 ?? "", + selected: selected + ) + } + + func buildAttributedString( + with bankBrand: STPFPXBankBrand, + selected: Bool + ) + -> NSAttributedString + { + let label = (STPFPXBank.stringFrom(bankBrand) ?? "") + " (FPX)" + let primaryColor = primaryColorForPaymentOption(withSelected: selected) + return NSAttributedString( + string: label, + attributes: [ + NSAttributedString.Key.foregroundColor: primaryColor + ] + ) + } + + func buildAttributedString( + with brand: STPCardBrand, + last4: String, + selected: Bool + ) -> NSAttributedString { + let format = String.Localized.card_brand_ending_in_last_4 + let brandString = STPCard.string(from: brand) + let label = String(format: format, brandString, last4) + + let primaryColor = selected ? theme.accentColor : theme.primaryForegroundColor + + let secondaryColor = UIColor(dynamicProvider: { _ in + return primaryColor.withAlphaComponent(0.6) + }) + + let attributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.foregroundColor: secondaryColor, + NSAttributedString.Key.font: self.theme.font, + ] + + let attributedString = NSMutableAttributedString( + string: label, + attributes: attributes as [NSAttributedString.Key: Any] + ) + attributedString.addAttribute( + .foregroundColor, + value: primaryColor, + range: (label as NSString).range(of: brandString) + ) + attributedString.addAttribute( + .foregroundColor, + value: primaryColor, + range: (label as NSString).range(of: last4) + ) + attributedString.addAttribute( + .font, + value: theme.emphasisFont, + range: (label as NSString).range(of: brandString) + ) + attributedString.addAttribute( + .font, + value: theme.emphasisFont, + range: (label as NSString).range(of: last4) + ) + + return attributedString + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} + +private let kDefaultIconWidth: CGFloat = 26.0 +private let kPadding: CGFloat = 15.0 +private let kCheckmarkWidth: CGFloat = 14.0 diff --git a/Stripe/StripeiOS/Source/STPPaymentOptionTuple.swift b/Stripe/StripeiOS/Source/STPPaymentOptionTuple.swift new file mode 100644 index 00000000..6084d132 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentOptionTuple.swift @@ -0,0 +1,88 @@ +// +// STPPaymentOptionTuple.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/17/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation + +class STPPaymentOptionTuple: NSObject { + @objc + public convenience init( + paymentOptions: [STPPaymentOption], + selectedPaymentOption: STPPaymentOption? + ) { + self.init() + self.paymentOptions = paymentOptions + self.selectedPaymentOption = selectedPaymentOption + } + + @objc + public convenience init( + paymentOptions: [STPPaymentOption], + selectedPaymentOption: STPPaymentOption?, + addApplePayOption applePayEnabled: Bool, + addFPXOption fpxEnabled: Bool + ) { + var mutablePaymentOptions = paymentOptions + weak var selected = selectedPaymentOption + + if applePayEnabled { + let applePay = STPApplePayPaymentOption() + mutablePaymentOptions.append(applePay) + + if selected == nil { + selected = applePay + } + } + + if fpxEnabled { + let fpx = STPPaymentMethodFPXParams() + let fpxPaymentOption = STPPaymentMethodParams( + fpx: fpx, + billingDetails: nil, + metadata: nil + ) + mutablePaymentOptions.append(fpxPaymentOption) + } + + self.init( + paymentOptions: mutablePaymentOptions, + selectedPaymentOption: selected + ) + } + + /// Returns a tuple for the given array of STPPaymentMethod, filtered to only include the + /// the types supported by STPPaymentContext/STPPaymentOptionsViewController and adding + /// Apple Pay as a method if appropriate. + /// - Returns: A new tuple ready to be used by the SDK's UI elements + @objc(tupleFilteredForUIWithPaymentMethods:selectedPaymentMethod:configuration:) + public convenience init( + filteredForUIWith paymentMethods: [STPPaymentMethod], + selectedPaymentMethod selectedPaymentMethodID: String?, + configuration: STPPaymentConfiguration + ) { + var paymentOptions: [STPPaymentOption] = [] + var selectedPaymentMethod: STPPaymentMethod? + for paymentMethod in paymentMethods { + if paymentMethod.type == .card { + paymentOptions.append(paymentMethod) + if paymentMethod.stripeId == selectedPaymentMethodID { + selectedPaymentMethod = paymentMethod + } + } + } + + self.init( + paymentOptions: paymentOptions, + selectedPaymentOption: selectedPaymentMethod, + addApplePayOption: configuration.applePayEnabled, + addFPXOption: configuration.fpxEnabled + ) + } + + private(set) weak var selectedPaymentOption: STPPaymentOption? + private(set) var paymentOptions: [STPPaymentOption] = [] +} diff --git a/Stripe/StripeiOS/Source/STPPaymentOptionsInternalViewController.swift b/Stripe/StripeiOS/Source/STPPaymentOptionsInternalViewController.swift new file mode 100644 index 00000000..10bd0d48 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentOptionsInternalViewController.swift @@ -0,0 +1,593 @@ +// +// STPPaymentOptionsInternalViewController.swift +// StripeiOS +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +import UIKit + +@objc protocol STPPaymentOptionsInternalViewControllerDelegate: AnyObject { + func internalViewControllerDidSelect(_ paymentOption: STPPaymentOption?) + func internalViewControllerDidDelete(_ paymentOption: STPPaymentOption?) + func internalViewControllerDidCreatePaymentOption( + _ paymentOption: STPPaymentOption?, + completion: @escaping STPErrorBlock + ) + func internalViewControllerDidCancel() +} + +class STPPaymentOptionsInternalViewController: STPCoreTableViewController, UITableViewDataSource, + UITableViewDelegate, STPAddCardViewControllerDelegate, STPBankSelectionViewControllerDelegate +{ + init( + configuration: STPPaymentConfiguration, + customerContext: STPCustomerContext?, + analyticsLogger: STPPaymentContext.AnalyticsLogger, + apiClient: STPAPIClient, + theme: STPTheme, + prefilledInformation: STPUserInformation?, + shippingAddress: STPAddress?, + paymentOptionTuple tuple: STPPaymentOptionTuple, + delegate: STPPaymentOptionsInternalViewControllerDelegate? + ) { + self.analyticsLogger = analyticsLogger + super.init(theme: theme) + self.configuration = configuration + // This parameter may be a custom API adapter, and not a CustomerContext. + apiAdapter = customerContext + self.apiClient = apiClient + self.prefilledInformation = prefilledInformation + self.shippingAddress = shippingAddress + paymentOptions = tuple.paymentOptions + selectedPaymentOption = tuple.selectedPaymentOption + self.delegate = delegate + + title = STPLocalizedString("Payment Method", "Title for Payment Method screen") + } + + func update(with tuple: STPPaymentOptionTuple) { + if let selectedPaymentOption = selectedPaymentOption, + selectedPaymentOption.isEqual(tuple.selectedPaymentOption) + { + return + } + + paymentOptions = tuple.paymentOptions + selectedPaymentOption = tuple.selectedPaymentOption + + // Reload card list section + let sections = NSMutableIndexSet(index: PaymentOptionSectionCardList) + tableView?.reloadSections(sections as IndexSet, with: .automatic) + } + + private var _customFooterView: UIView? + internal let analyticsLogger: STPPaymentContext.AnalyticsLogger + var customFooterView: UIView? { + get { + _customFooterView + } + set(footerView) { + _customFooterView = footerView + _didSetCustomFooterView() + } + } + func _didSetCustomFooterView() { + if isViewLoaded { + if let size = _customFooterView?.sizeThatFits( + CGSize(width: view.bounds.size.width, height: CGFloat.greatestFiniteMagnitude) + ) { + _customFooterView?.frame = CGRect( + x: 0, + y: 0, + width: size.width, + height: size.height + ) + } + + tableView?.tableFooterView = _customFooterView + } + } + + var addCardViewControllerCustomFooterView: UIView? + var prefilledInformation: STPUserInformation? + private var configuration: STPPaymentConfiguration? + private var apiAdapter: STPBackendAPIAdapter? + private var shippingAddress: STPAddress? + private var paymentOptions: [STPPaymentOption]? + private var apiClient: STPAPIClient = .shared + private var selectedPaymentOption: STPPaymentOption? + private weak var delegate: STPPaymentOptionsInternalViewControllerDelegate? + private var cardImageView: UIImageView? + + override func createAndSetupViews() { + super.createAndSetupViews() + + // Table view + tableView?.register( + STPPaymentOptionTableViewCell.self, + forCellReuseIdentifier: PaymentOptionCellReuseIdentifier + ) + + tableView?.dataSource = self + tableView?.delegate = self + tableView?.reloadData() + + // Table header view + let cardImageView = UIImageView(image: STPLegacyImageLibrary.largeCardFrontImage()) + cardImageView.contentMode = .center + cardImageView.frame = CGRect( + x: 0.0, + y: 0.0, + width: view.bounds.size.width, + height: cardImageView.bounds.size.height + (57.0 * 2.0) + ) + cardImageView.image = STPLegacyImageLibrary.largeCardFrontImage() + cardImageView.tintColor = theme.accentColor + self.cardImageView = cardImageView + + tableView?.tableHeaderView = cardImageView + + // Table view editing state + tableView?.setEditing(false, animated: false) + reloadRightBarButtonItem( + withTableViewIsEditing: tableView?.isEditing ?? false, + animated: false + ) + + stp_navigationItemProxy?.leftBarButtonItem?.accessibilityIdentifier = + "PaymentOptionsViewControllerCancelButtonIdentifier" + // re-set the custom footer view if it was added before we loaded + _didSetCustomFooterView() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + analyticsLogger.logPaymentOptionsScreenAppeared() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Resetting it re-calculates the size based on new view width + // UITableView requires us to call setter again to actually pick up frame + // change on footers + if tableView?.tableFooterView != nil { + customFooterView = tableView?.tableFooterView + } + } + + func reloadRightBarButtonItem(withTableViewIsEditing tableViewIsEditing: Bool, animated: Bool) { + var barButtonItem: UIBarButtonItem? + + if !tableViewIsEditing { + if isAnyPaymentOptionDetachable() { + // Show edit button + barButtonItem = UIBarButtonItem( + barButtonSystemItem: .edit, + target: self, + action: #selector(handleEditButtonTapped(_:)) + ) + } else { + // Show no button + barButtonItem = nil + } + } else { + // Show done button + barButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(handleDoneButtonTapped(_:)) + ) + } + + barButtonItem?.stp_setTheme(theme) + + stp_navigationItemProxy?.setRightBarButton(barButtonItem, animated: animated) + } + + func isAnyPaymentOptionDetachable() -> Bool { + for paymentOption in cardPaymentOptions() { + if isPaymentOptionDetachable(paymentOption) { + return true + } + } + + return false + } + + func isPaymentOptionDetachable(_ paymentOption: STPPaymentOption?) -> Bool { + if !(configuration?.canDeletePaymentOptions ?? false) { + // Feature is disabled + return false + } + + if apiAdapter == nil { + // Cannot detach payment methods without customer context + return false + } + + if !(apiAdapter?.responds( + to: #selector(STPCustomerContext.detachPaymentMethod(fromCustomer:completion:)) + ) + ?? false) + { + // Cannot detach payment methods if customerContext is an apiAdapter + // that doesn't implement detachPaymentMethod + return false + } + + if paymentOption == nil { + // Cannot detach non-existent payment method + return false + } + + if !(paymentOption is STPPaymentMethod) { + // Cannot detach non-payment method + return false + } + + // Payment method can be deleted from customer + return true + } + + func cardPaymentOptions() -> [STPPaymentOption] { + guard let paymentOptions = paymentOptions else { + return [] + } + + return paymentOptions.filter({ (o) -> Bool in + if o is STPPaymentMethodParams { + let paymentMethodParams = o as? STPPaymentMethodParams + if paymentMethodParams?.type != .card { + return false + } + } + return true + }) + } + + func apmPaymentOptions() -> [STPPaymentOption] { + guard let paymentOptions = paymentOptions else { + return [] + } + return paymentOptions.filter({ (o) -> Bool in + if (o) is STPPaymentMethodParams { + let paymentMethodParams = o as? STPPaymentMethodParams + if paymentMethodParams?.type == .FPX { + // Add other APMs as we gain support for them in Basic Integration + return true + } + } + return false + }) + } + + // MARK: - Button Handlers + @objc override func handleCancelTapped(_ sender: Any?) { + delegate?.internalViewControllerDidCancel() + } + + @objc func handleEditButtonTapped(_ sender: Any?) { + tableView?.setEditing(true, animated: true) + reloadRightBarButtonItem( + withTableViewIsEditing: tableView?.isEditing ?? false, + animated: true + ) + } + + @objc func handleDoneButtonTapped(_ sender: Any?) { + _endTableViewEditing() + reloadRightBarButtonItem( + withTableViewIsEditing: tableView?.isEditing ?? false, + animated: true + ) + } + + func _endTableViewEditing() { + tableView?.setEditing(false, animated: true) + } + + // MARK: - UITableViewDataSource + func numberOfSections(in tableView: UITableView) -> Int { + if apmPaymentOptions().count > 0 { + return 3 + } else { + return 2 + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == PaymentOptionSectionCardList { + return cardPaymentOptions().count + } + + if section == PaymentOptionSectionAddCard { + return 1 + } + + if section == PaymentOptionSectionAPM { + return apmPaymentOptions().count + } + + return 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = + tableView.dequeueReusableCell( + withIdentifier: PaymentOptionCellReuseIdentifier, + for: indexPath + ) + as? STPPaymentOptionTableViewCell + + if indexPath.section == PaymentOptionSectionCardList { + weak var paymentOption = + cardPaymentOptions().stp_boundSafeObject(at: indexPath.row) + let selected = paymentOption!.isEqual(selectedPaymentOption) + + cell?.configure(with: paymentOption!, theme: theme, selected: selected) + } else if indexPath.section == PaymentOptionSectionAddCard { + cell?.configureForNewCardRow(with: theme) + cell?.accessibilityIdentifier = "PaymentOptionsTableViewAddNewCardButtonIdentifier" + } else if indexPath.section == PaymentOptionSectionAPM { + weak var paymentOption = + apmPaymentOptions().stp_boundSafeObject(at: indexPath.row) + if paymentOption is STPPaymentMethodParams { + let paymentMethodParams = paymentOption as? STPPaymentMethodParams + if paymentMethodParams?.type == .FPX { + cell?.configureForFPXRow(with: theme) + cell?.accessibilityIdentifier = "PaymentOptionsTableViewFPXButtonIdentifier" + } + } + } + + return cell! + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + if indexPath.section == PaymentOptionSectionCardList { + weak var paymentOption = + cardPaymentOptions().stp_boundSafeObject(at: indexPath.row) + + if isPaymentOptionDetachable(paymentOption) { + return true + } + } + + return false + } + + func tableView( + _ tableView: UITableView, + commit editingStyle: UITableViewCell.EditingStyle, + forRowAt indexPath: IndexPath + ) { + if indexPath.section == PaymentOptionSectionCardList { + if editingStyle != .delete { + // Showed the user a non-delete option when we shouldn't have + tableView.reloadData() + return + } + + if !(indexPath.row < cardPaymentOptions().count) { + // Data source and table view out of sync for some reason + tableView.reloadData() + return + } + + weak var paymentOptionToDelete = + cardPaymentOptions().stp_boundSafeObject(at: indexPath.row) + + if !isPaymentOptionDetachable(paymentOptionToDelete) { + // Showed the user a delete option for a payment method when we shouldn't have + tableView.reloadData() + return + } + + let paymentMethod = paymentOptionToDelete as? STPPaymentMethod + + // Kickoff request to delete payment method from customer + if let paymentMethod = paymentMethod { + apiAdapter?.detachPaymentMethod?(fromCustomer: paymentMethod, completion: nil) + } + + // Optimistically remove payment method from data source + var paymentOptions = self.paymentOptions + paymentOptions?.removeAll { $0 as AnyObject === paymentOptionToDelete as AnyObject } + self.paymentOptions = paymentOptions + + // Perform deletion animation for single row + tableView.deleteRows(at: [indexPath], with: .automatic) + + var tableViewIsEditing = tableView.isEditing + if !isAnyPaymentOptionDetachable() { + // we deleted the last available payment option, stop editing + // (but delay to next runloop because calling tableView setEditing:animated: + // in this function is not allowed) + DispatchQueue.main.async(execute: { + self._endTableViewEditing() + }) + // manually set the value passed to reloadRightBarButtonItemWithTableViewIsEditing + // below + tableViewIsEditing = false + } + + // Reload right bar button item text + reloadRightBarButtonItem(withTableViewIsEditing: tableViewIsEditing, animated: true) + + // Notify delegate + delegate?.internalViewControllerDidDelete(paymentOptionToDelete) + } + } + + // MARK: - UITableViewDelegate + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == PaymentOptionSectionCardList { + // Update data source + weak var paymentOption = + cardPaymentOptions().stp_boundSafeObject(at: indexPath.row) + selectedPaymentOption = paymentOption + + // Perform selection animation + tableView.reloadSections( + NSIndexSet(index: PaymentOptionSectionCardList) as IndexSet, + with: .fade + ) + + // Notify delegate + delegate?.internalViewControllerDidSelect(paymentOption) + } else if indexPath.section == PaymentOptionSectionAddCard { + var paymentCardViewController: STPAddCardViewController? + if let configuration = configuration { + paymentCardViewController = STPAddCardViewController( + configuration: configuration, + theme: theme + ) + } + paymentCardViewController?.analyticsLogger = analyticsLogger + paymentCardViewController?.apiClient = apiClient + paymentCardViewController?.delegate = self + paymentCardViewController?.prefilledInformation = prefilledInformation + paymentCardViewController?.shippingAddress = shippingAddress + paymentCardViewController?.customFooterView = addCardViewControllerCustomFooterView + + if let paymentCardViewController = paymentCardViewController { + navigationController?.pushViewController(paymentCardViewController, animated: true) + } + } else if indexPath.section == PaymentOptionSectionAPM { + weak var paymentOption = + apmPaymentOptions().stp_boundSafeObject(at: indexPath.row) + if paymentOption is STPPaymentMethodParams { + if let paymentMethodParams = paymentOption as? STPPaymentMethodParams, + paymentMethodParams.type == .FPX + { + var bankSelectionViewController: STPBankSelectionViewController? + if let configuration = configuration { + bankSelectionViewController = STPBankSelectionViewController( + bankMethod: .FPX, + configuration: configuration, + theme: theme + ) + } + bankSelectionViewController?.apiClient = apiClient + bankSelectionViewController?.delegate = self + + if let bankSelectionViewController = bankSelectionViewController { + navigationController?.pushViewController( + bankSelectionViewController, + animated: true + ) + } + } + } + } + + tableView.deselectRow(at: indexPath, animated: true) + } + + func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let isTopRow = indexPath.row == 0 + let isBottomRow = + self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 == indexPath.row + + cell.stp_setBorderColor(theme.tertiaryBackgroundColor) + cell.stp_setTopBorderHidden(!isTopRow) + cell.stp_setBottomBorderHidden(!isBottomRow) + cell.stp_setFakeSeparatorColor(theme.quaternaryBackgroundColor) + cell.stp_setFakeSeparatorLeftInset(15.0) + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if self.tableView(tableView, numberOfRowsInSection: section) == 0 { + return 0.01 + } + + return 27.0 + } + + override func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) + -> CGFloat + { + return 0.01 + } + + func tableView( + _ tableView: UITableView, + editingStyleForRowAt indexPath: IndexPath + ) + -> UITableViewCell.EditingStyle + { + if indexPath.section == PaymentOptionSectionCardList { + return .delete + } + + return .none + } + + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + reloadRightBarButtonItem(withTableViewIsEditing: true, animated: true) + } + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + reloadRightBarButtonItem(withTableViewIsEditing: tableView.isEditing, animated: true) + } + + // MARK: - STPAddCardViewControllerDelegate + func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) { + navigationController?.popViewController(animated: true) + } + + @objc func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + completion: @escaping STPErrorBlock + ) { + delegate?.internalViewControllerDidCreatePaymentOption( + paymentMethod, + completion: completion + ) + } + + @objc func bankSelectionViewController( + _ bankViewController: STPBankSelectionViewController, + didCreatePaymentMethodParams paymentMethodParams: STPPaymentMethodParams + ) { + delegate?.internalViewControllerDidCreatePaymentOption(paymentMethodParams) { _ in + } + } + + required init?( + coder aDecoder: NSCoder + ) { + fatalError("init(coder:) has not been implemented") + } + + required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + fatalError("init(nibName:bundle:) has not been implemented") + } + + required init( + theme: STPTheme? + ) { + fatalError("init(theme:) has not been implemented") + } +} + +private let PaymentOptionCellReuseIdentifier = "PaymentOptionCellReuseIdentifier" +private let PaymentOptionSectionCardList = 0 +private let PaymentOptionSectionAddCard = 1 +private let PaymentOptionSectionAPM = 2 diff --git a/Stripe/StripeiOS/Source/STPPaymentOptionsViewController.swift b/Stripe/StripeiOS/Source/STPPaymentOptionsViewController.swift new file mode 100644 index 00000000..aa392eae --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentOptionsViewController.swift @@ -0,0 +1,662 @@ +// +// STPPaymentOptionsViewController.swift +// StripeiOS +// +// Created by Jack Flintermann on 1/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +import UIKit + +/// This view controller presents a list of payment method options to the user, +/// which they can select between. They can also add credit cards to the list. +/// It must be displayed inside a `UINavigationController`, so you can either +/// create a `UINavigationController` with an `STPPaymentOptionsViewController` +/// as the `rootViewController` and then present the `UINavigationController`, +/// or push a new `STPPaymentOptionsViewController` onto an existing +/// `UINavigationController`'s stack. You can also have `STPPaymentContext` do this +/// for you automatically, by calling `presentPaymentOptionsViewController` +/// or `pushPaymentOptionsViewController` on it. +public class STPPaymentOptionsViewController: STPCoreViewController, + STPPaymentOptionsInternalViewControllerDelegate, STPAddCardViewControllerDelegate +{ + + /// The delegate for the view controller. + /// The delegate receives callbacks when the user selects a method or cancels, + /// and is responsible for dismissing the payments methods view controller when + /// it is finished. + @objc private(set) weak var delegate: STPPaymentOptionsViewControllerDelegate? + + /// Creates a new payment methods view controller. + /// - Parameter paymentContext: A payment context to power the view controller's view. + /// The payment context will in turn use its backend API adapter to fetch the + /// information it needs from your application. + /// - Returns: an initialized view controller. + @objc(initWithPaymentContext:) + public convenience init( + paymentContext: STPPaymentContext + ) { + self.init( + configuration: paymentContext.configuration, + apiAdapter: paymentContext.apiAdapter, + apiClient: paymentContext.apiClient, + analyticsLogger: paymentContext.analyticsLogger, + loadingPromise: paymentContext.currentValuePromise, + theme: paymentContext.theme, + shippingAddress: paymentContext.shippingAddress, + delegate: paymentContext + ) + } + + init( + configuration: STPPaymentConfiguration?, + apiAdapter: STPBackendAPIAdapter, + apiClient: STPAPIClient?, + analyticsLogger: STPPaymentContext.AnalyticsLogger, + loadingPromise: STPPromise?, + theme: STPTheme?, + shippingAddress: STPAddress?, + delegate: STPPaymentOptionsViewControllerDelegate + ) { + self.apiAdapter = apiAdapter + self.analyticsLogger = analyticsLogger + super.init(theme: theme) + commonInit( + configuration: configuration, + apiAdapter: apiAdapter, + apiClient: apiClient, + loadingPromise: loadingPromise, + shippingAddress: shippingAddress, + delegate: delegate + ) + } + + func commonInit( + configuration: STPPaymentConfiguration?, + apiAdapter: STPBackendAPIAdapter, + apiClient: STPAPIClient?, + loadingPromise: STPPromise?, + shippingAddress: STPAddress?, + delegate: STPPaymentOptionsViewControllerDelegate + ) { + STPAnalyticsClient.sharedClient.addClass( + toProductUsageIfNecessary: STPPaymentOptionsViewController.self + ) + + self.configuration = configuration + self.apiClient = apiClient ?? .shared + self.shippingAddress = shippingAddress + self.apiAdapter = apiAdapter + self.loadingPromise = loadingPromise + self.delegate = delegate + + navigationItem.title = STPLocalizedString( + "Loading…", + "Title for screen when data is still loading from the network." + ) + + weak var weakSelf = self + loadingPromise?.onSuccess({ tuple in + guard let strongSelf = weakSelf else { + return + } + var `internal`: UIViewController? + if (tuple.paymentOptions.count) > 0 { + let customerContext = strongSelf.apiAdapter as? STPCustomerContext + + var payMethodsInternal: STPPaymentOptionsInternalViewController? + if let configuration1 = strongSelf.configuration { + payMethodsInternal = STPPaymentOptionsInternalViewController( + configuration: configuration1, + customerContext: customerContext, + analyticsLogger: strongSelf.analyticsLogger, + apiClient: strongSelf.apiClient, + theme: strongSelf.theme, + prefilledInformation: strongSelf.prefilledInformation, + shippingAddress: strongSelf.shippingAddress, + paymentOptionTuple: tuple, + delegate: strongSelf + ) + } + if strongSelf.paymentOptionsViewControllerFooterView != nil { + payMethodsInternal?.customFooterView = + strongSelf.paymentOptionsViewControllerFooterView + } + if strongSelf.addCardViewControllerFooterView != nil { + payMethodsInternal?.addCardViewControllerCustomFooterView = + strongSelf.addCardViewControllerFooterView + } + `internal` = payMethodsInternal + } else { + var addCardViewController: STPAddCardViewController? + if let configuration1 = strongSelf.configuration { + addCardViewController = STPAddCardViewController( + configuration: configuration1, + theme: strongSelf.theme + ) + } + addCardViewController?.analyticsLogger = strongSelf.analyticsLogger + addCardViewController?.apiClient = strongSelf.apiClient + addCardViewController?.delegate = strongSelf + addCardViewController?.prefilledInformation = strongSelf.prefilledInformation + addCardViewController?.shippingAddress = strongSelf.shippingAddress + `internal` = addCardViewController + + if strongSelf.addCardViewControllerFooterView != nil { + addCardViewController?.customFooterView = + strongSelf.addCardViewControllerFooterView + } + } + + `internal`?.stp_navigationItemProxy = strongSelf.navigationItem + if let controller = `internal` { + strongSelf.addChild(controller) + } + `internal`?.view.alpha = 0 + if let view = `internal`?.view, let activityIndicator1 = strongSelf.activityIndicator { + strongSelf.view.insertSubview(view, belowSubview: activityIndicator1) + } + if let view = `internal`?.view { + strongSelf.view.addSubview(view) + } + `internal`?.view.frame = strongSelf.view.bounds + `internal`?.didMove(toParent: strongSelf) + UIView.animate( + withDuration: 0.2, + animations: { + strongSelf.activityIndicator?.alpha = 0 + `internal`?.view.alpha = 1 + } + ) { _ in + strongSelf.activityIndicator?.animating = false + } + strongSelf.navigationItem.setRightBarButton( + `internal`?.stp_navigationItemProxy?.rightBarButtonItem, + animated: true + ) + strongSelf.internalViewController = `internal` + }) + } + + /// Initializes a new payment methods view controller without using a + /// payment context. + /// - Parameters: + /// - configuration: The configuration to use to determine what types of + /// payment method to offer your user. - seealso: STPPaymentConfiguration.h + /// - theme: The theme to inform the appearance of the UI. + /// - customerContext: The customer context the view controller will use to + /// fetch and modify its Stripe customer + /// - delegate: A delegate that will be notified when the payment + /// methods view controller's selection changes. + /// - Returns: an initialized view controller. + @objc(initWithConfiguration:theme:customerContext:delegate:) + public convenience init( + configuration: STPPaymentConfiguration, + theme: STPTheme, + customerContext: STPCustomerContext, + delegate: STPPaymentOptionsViewControllerDelegate + ) { + self.init( + configuration: configuration, + theme: theme, + apiAdapter: customerContext, + delegate: delegate + ) + } + + /// Note: Instead of providing your own backend API adapter, we recommend using + /// `STPCustomerContext`, which will manage retrieving and updating a + /// Stripe customer for you. - seealso: STPCustomerContext.h + /// Initializes a new payment methods view controller without using + /// a payment context. + /// - Parameters: + /// - configuration: The configuration to use to determine what types of + /// payment method to offer your user. + /// - theme: The theme to inform the appearance of the UI. + /// - apiAdapter: The API adapter to use to retrieve a customer's stored + /// payment methods and save new ones. + /// - delegate: A delegate that will be notified when the payment methods + /// view controller's selection changes. + @objc(initWithConfiguration:theme:apiAdapter:delegate:) + public init( + configuration: STPPaymentConfiguration, + theme: STPTheme, + apiAdapter: STPBackendAPIAdapter, + delegate: STPPaymentOptionsViewControllerDelegate + ) { + self.apiAdapter = apiAdapter + super.init(theme: theme) + let promise = retrievePaymentMethods(with: configuration, apiAdapter: apiAdapter) + + commonInit( + configuration: configuration, + apiAdapter: apiAdapter, + apiClient: STPAPIClient.shared, + loadingPromise: promise, + shippingAddress: nil, + delegate: delegate + ) + } + + /// If you've already collected some information from your user, you can set it + /// here and it'll be automatically filled out when possible/appropriate in any UI + /// that the payment context creates. + @objc public var prefilledInformation: STPUserInformation? { + didSet { + if let payMethodsInternal = internalViewController as? STPPaymentOptionsInternalViewController { + payMethodsInternal.prefilledInformation = prefilledInformation + } else if let payMethodsInternal = internalViewController as? STPAddCardViewController { + payMethodsInternal.prefilledInformation = prefilledInformation + } + } + } + /// @note This is no longer recommended as of v18.3.0 - the SDK automatically saves the Stripe ID of the last selected + /// payment method using NSUserDefaults and displays it as the default pre-selected option. You can override this behavior + /// by setting this property. + /// The Stripe ID of a payment method to display as the default pre-selected option. + /// @note Setting this after the view controller's view has loaded has no effect. + @objc public var defaultPaymentMethod: String? + /// A view that will be placed as the footer of the view controller when it is + /// showing a list of saved payment methods to select from. + /// When the footer view needs to be resized, it will be sent a + /// `sizeThatFits:` call. The view should respond correctly to this method in order + /// to be sized and positioned properly. + @objc public var paymentOptionsViewControllerFooterView: UIView? { + didSet { + if let payMethodsInternal = internalViewController as? STPPaymentOptionsInternalViewController { + payMethodsInternal.customFooterView = paymentOptionsViewControllerFooterView + } + } + } + + /// A view that will be placed as the footer of the view controller when it is + /// showing the add card view. + /// When the footer view needs to be resized, it will be sent a + /// `sizeThatFits:` call. The view should respond correctly to this method in order + /// to be sized and positioned properly. + @objc public var addCardViewControllerFooterView: UIView? { + didSet { + if let payMethodsInternal = internalViewController as? STPPaymentOptionsInternalViewController { + payMethodsInternal.addCardViewControllerCustomFooterView = addCardViewControllerFooterView + } else if let payMethodsInternal = internalViewController as? STPAddCardViewController { + payMethodsInternal.customFooterView = addCardViewControllerFooterView + } + } + } + + /// The API Client to use to make requests. + /// Defaults to STPAPIClient.shared + public var apiClient: STPAPIClient = .shared + + /// If you're pushing `STPPaymentOptionsViewController` onto an existing + /// `UINavigationController`'s stack, you should use this method to dismiss it, + /// since it may have pushed an additional add card view controller onto the + /// navigation controller's stack. + /// - Parameter completion: The callback to run after the view controller is dismissed. + /// You may specify nil for this parameter. + @objc(dismissWithCompletion:) + public func dismiss(withCompletion completion: STPVoidBlock?) { + if stp_isAtRootOfNavigationController() { + presentingViewController?.dismiss(animated: true, completion: completion) + } else { + var previous = navigationController?.viewControllers.first + for viewController in navigationController?.viewControllers ?? [] { + if viewController == self { + break + } + previous = viewController + } + navigationController?.stp_pop( + to: previous, + animated: true, + completion: completion ?? {} + ) + } + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + @objc public required init( + theme: STPTheme? + ) { + fatalError("init(theme:) has not been implemented") + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + @objc public required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + fatalError("init(nibName:bundle:) has not been implemented") + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + @objc public required init?( + coder aDecoder: NSCoder + ) { + fatalError("init(coder:) has not been implemented") + } + + private var configuration: STPPaymentConfiguration? + private var shippingAddress: STPAddress? + private var apiAdapter: STPBackendAPIAdapter + var loadingPromise: STPPromise? + private var activityIndicator: STPPaymentActivityIndicatorView? + internal var internalViewController: UIViewController? + // Should be overwritten if this class is used by STPPaymentContext + internal var analyticsLogger: STPPaymentContext.AnalyticsLogger = .init(product: STPPaymentOptionsViewController.self) + + func retrievePaymentMethods( + with configuration: STPPaymentConfiguration, + apiAdapter: STPBackendAPIAdapter? + ) -> STPPromise { + let promise = STPPromise() + apiAdapter?.listPaymentMethodsForCustomer(completion: { paymentMethods, error in + // We don't use stpDispatchToMainThreadIfNecessary here because we want this completion block to always be called asynchronously, so that users can set self.defaultPaymentMethod in time. + DispatchQueue.main.async(execute: { + if let error = error { + promise.fail(error) + } else { + let defaultPaymentMethod = self.defaultPaymentMethod + if defaultPaymentMethod == nil && (apiAdapter is STPCustomerContext) { + // Retrieve the last selected payment method saved by STPCustomerContext + (apiAdapter as? STPCustomerContext)? + .retrieveLastSelectedPaymentMethodIDForCustomer( + completion: { paymentMethodID, _ in + var paymentTuple: STPPaymentOptionTuple? + if let paymentMethods = paymentMethods { + paymentTuple = STPPaymentOptionTuple.init( + filteredForUIWith: paymentMethods, + selectedPaymentMethod: paymentMethodID, + configuration: configuration + ) + } + promise.succeed(paymentTuple!) + }) + } + var paymentTuple: STPPaymentOptionTuple? + if let paymentMethods = paymentMethods { + paymentTuple = STPPaymentOptionTuple.init( + filteredForUIWith: paymentMethods, + selectedPaymentMethod: defaultPaymentMethod, + configuration: configuration + ) + } + promise.succeed(paymentTuple!) + } + }) + }) + return promise + } + + override func createAndSetupViews() { + super.createAndSetupViews() + + let activityIndicator = STPPaymentActivityIndicatorView() + activityIndicator.animating = true + view.addSubview(activityIndicator) + self.activityIndicator = activityIndicator + } + + /// :nodoc: + @objc + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + let centerX = (view.frame.size.width - (activityIndicator?.frame.size.width ?? 0.0)) / 2 + let centerY = (view.frame.size.height - (activityIndicator?.frame.size.height ?? 0.0)) / 2 + activityIndicator?.frame = CGRect( + x: centerX, + y: centerY, + width: activityIndicator?.frame.size.width ?? 0.0, + height: activityIndicator?.frame.size.height ?? 0.0 + ) + internalViewController?.view.frame = view.bounds + } + + /// :nodoc: + @objc + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + weak var weakSelf = self + loadingPromise?.onSuccess({ tuple in + let strongSelf = weakSelf + if strongSelf == nil { + return + } + + if tuple.selectedPaymentOption != nil { + if strongSelf?.delegate?.responds( + to: #selector( + STPPaymentOptionsViewControllerDelegate.paymentOptionsViewController( + _: + didSelect: + )) + ) + ?? false + { + if let strongSelf = strongSelf, + let selectedPaymentOption = tuple.selectedPaymentOption + { + strongSelf.delegate?.paymentOptionsViewController?( + strongSelf, + didSelect: selectedPaymentOption + ) + } + } + } + }).onFailure({ error in + let strongSelf = weakSelf + if strongSelf == nil { + return + } + + if let strongSelf = strongSelf { + strongSelf.delegate?.paymentOptionsViewController( + strongSelf, + didFailToLoadWithError: error + ) + } + }) + } + + @objc override func updateAppearance() { + super.updateAppearance() + + activityIndicator?.tintColor = theme.accentColor + } + + func finish(with paymentOption: STPPaymentOption?) { + let isReusablePaymentMethod = + (paymentOption is STPPaymentMethod) + && (paymentOption as? STPPaymentMethod)?.isReusable ?? false + + if apiAdapter is STPCustomerContext { + if isReusablePaymentMethod { + // Save the payment method + let paymentMethod = paymentOption as? STPPaymentMethod + (apiAdapter as? STPCustomerContext)?.saveLastSelectedPaymentMethodID( + forCustomer: paymentMethod?.stripeId ?? "", + completion: nil + ) + } else { + // The customer selected something else (like Apple Pay) + (apiAdapter as? STPCustomerContext)?.saveLastSelectedPaymentMethodID( + forCustomer: nil, + completion: nil + ) + } + } + + if delegate?.responds( + to: #selector( + STPPaymentOptionsViewControllerDelegate.paymentOptionsViewController(_:didSelect:)) + ) + ?? false + { + if let paymentOption = paymentOption { + delegate?.paymentOptionsViewController?(self, didSelect: paymentOption) + } + } + delegate?.paymentOptionsViewControllerDidFinish(self) + } + + func internalViewControllerDidSelect(_ paymentOption: STPPaymentOption?) { + finish(with: paymentOption) + } + + func internalViewControllerDidDelete(_ paymentOption: STPPaymentOption?) { + if delegate is STPPaymentContext { + // Notify payment context to update its copy of payment methods + if let paymentContext = delegate as? STPPaymentContext, + let paymentOption = paymentOption + { + paymentContext.remove(paymentOption) + } + } + } + + func internalViewControllerDidCreatePaymentOption( + _ paymentOption: STPPaymentOption?, + completion: @escaping STPErrorBlock + ) { + if !(paymentOption?.isReusable ?? false) { + // Don't save a non-reusable payment option + finish(with: paymentOption) + return + } + let paymentMethod = paymentOption as? STPPaymentMethod + if let paymentMethod = paymentMethod { + apiAdapter.attachPaymentMethod(toCustomer: paymentMethod) { error in + stpDispatchToMainThreadIfNecessary({ + completion(error) + if error == nil { + var promise: STPPromise? + if let configuration = self.configuration { + promise = self.retrievePaymentMethods( + with: configuration, + apiAdapter: self.apiAdapter + ) + } + weak var weakSelf = self + promise?.onSuccess({ tuple in + let strongSelf = weakSelf + if strongSelf == nil { + return + } + let paymentTuple = STPPaymentOptionTuple( + paymentOptions: tuple.paymentOptions, + selectedPaymentOption: paymentMethod + ) + if strongSelf?.internalViewController + is STPPaymentOptionsInternalViewController + { + let paymentOptionsVC = + strongSelf?.internalViewController + as? STPPaymentOptionsInternalViewController + paymentOptionsVC?.update(with: paymentTuple) + } + }) + self.finish(with: paymentMethod) + } + }) + } + } + } + + func internalViewControllerDidCancel() { + delegate?.paymentOptionsViewControllerDidCancel(self) + } + + @objc override func handleCancelTapped(_ sender: Any?) { + delegate?.paymentOptionsViewControllerDidCancel(self) + } + + @objc + public func addCardViewControllerDidCancel( + _ addCardViewController: STPAddCardViewController + ) { + // Add card is only our direct delegate if there are no other payment methods possible + // and we skipped directly to this screen. In this case, a cancel from it is the same as a cancel to us. + delegate?.paymentOptionsViewControllerDidCancel(self) + } + + @objc + public func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + completion: @escaping STPErrorBlock + ) { + internalViewControllerDidCreatePaymentOption(paymentMethod, completion: completion) + } +} + +// MARK: - STPPaymentOptionsViewControllerDelegate + +/// An `STPPaymentOptionsViewControllerDelegate` responds when a user selects a +/// payment option from (or cancels) an `STPPaymentOptionsViewController`. In both +/// of these instances, you should dismiss the view controller (either by popping +/// it off the navigation stack, or dismissing it). +@objc public protocol STPPaymentOptionsViewControllerDelegate: NSObjectProtocol { + /// This is called when the view controller encounters an error fetching the user's + /// payment options from its API adapter. You should dismiss the view controller + /// when this is called. + /// - Parameters: + /// - paymentOptionsViewController: the view controller in question + /// - error: the error that occurred + func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didFailToLoadWithError error: Error + ) + /// This is called when the user selects or adds a payment method, so it will often + /// be called immediately after calling `paymentOptionsViewController:didSelectPaymentOption:`. + /// You should dismiss the view controller when this is called. + /// - Parameter paymentOptionsViewController: the view controller that has finished + func paymentOptionsViewControllerDidFinish( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) + /// This is called when the user taps "cancel". + /// You should dismiss the view controller when this is called. + /// - Parameter paymentOptionsViewController: the view controller that has finished + func paymentOptionsViewControllerDidCancel( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) + + /// This is called when the user either makes a selection, or adds a new card. + /// This will be triggered after the view controller loads with the user's current + /// selection (if they have one) and then subsequently when they change their + /// choice. You should use this callback to update any necessary UI in your app + /// that displays the user's currently selected payment method. You should *not* + /// dismiss the view controller at this point, instead do this in + /// `paymentOptionsViewControllerDidFinish:`. `STPPaymentOptionsViewController` + /// will also call the necessary methods on your API adapter, so you don't need to + /// call them directly during this method. + /// - Parameters: + /// - paymentOptionsViewController: the view controller in question + /// - paymentOption: the selected payment method + @objc(paymentOptionsViewController:didSelectPaymentOption:) + optional func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didSelect paymentOption: STPPaymentOption + ) +} + +/// :nodoc: +@_spi(STP) extension STPPaymentOptionsViewController: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier = "STPPaymentOptionsViewController" +} diff --git a/Stripe/StripeiOS/Source/STPPaymentResult.swift b/Stripe/StripeiOS/Source/STPPaymentResult.swift new file mode 100644 index 00000000..0a170738 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPaymentResult.swift @@ -0,0 +1,43 @@ +// +// STPPaymentResult.swift +// StripeiOS +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation + +/// When you're using `STPPaymentContext` to request your user's payment details, this is the object that will be returned to your application when they've successfully made a payment. +/// See https://stripe.com/docs/mobile/ios/basic#submit-payment-intents +public class STPPaymentResult: NSObject { + /// The payment method that the user has selected. This may come from a variety of different payment methods, such as an Apple Pay payment or a stored credit card. - seealso: STPPaymentMethod.h + /// If paymentMethod is nil, paymentMethodParams will be populated instead. + @objc public private(set) var paymentMethod: STPPaymentMethod? + /// The parameters for a payment method that the user has selected. This is + /// populated for non-reusable payment methods, such as FPX and iDEAL. - seealso: STPPaymentMethodParams.h + /// If paymentMethodParams is nil, paymentMethod will be populated instead. + @objc public private(set) var paymentMethodParams: STPPaymentMethodParams? + /// The STPPaymentOption that was used to initialize this STPPaymentResult, either an STPPaymentMethod or an STPPaymentMethodParams. + + @objc public weak var paymentOption: STPPaymentOption? { + if paymentMethod != nil { + return paymentMethod + } else { + return paymentMethodParams + } + } + + /// Initializes the payment result with a given payment option. This is invoked by `STPPaymentContext` internally; you shouldn't have to call it directly. + @objc + public init( + paymentOption: STPPaymentOption? + ) { + super.init() + if paymentOption is STPPaymentMethod { + paymentMethod = paymentOption as? STPPaymentMethod + } else if paymentOption is STPPaymentMethodParams { + paymentMethodParams = paymentOption as? STPPaymentMethodParams + } + } +} diff --git a/Stripe/StripeiOS/Source/STPPinManagementService.swift b/Stripe/StripeiOS/Source/STPPinManagementService.swift new file mode 100644 index 00000000..3ba38b8e --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPinManagementService.swift @@ -0,0 +1,144 @@ +// +// STPPinManagementService.swift +// StripeiOS +// +// Created by Arnaud Cavailhez on 4/29/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +import UIKit + +/// STPAPIClient extensions to manage PIN on Stripe Issuing cards +@available(iOS, deprecated: 100000.0, message: "Please use Issuing Elements instead: https://stripe.com/docs/issuing/elements") +public class STPPinManagementService: NSObject { + /// The API Client to use to make requests. + /// Defaults to STPAPIClient.shared + public var apiClient: STPAPIClient = STPAPIClient.shared + + /// Create a STPPinManagementService, you must provide an implementation of STPIssuingCardEphemeralKeyProvider + @objc + public init( + keyProvider: STPIssuingCardEphemeralKeyProvider + ) { + super.init() + keyManager = STPEphemeralKeyManager( + keyProvider: keyProvider as Any, + apiVersion: STPAPIClient.apiVersion, + performsEagerFetching: false + ) + } + + /// Retrieves a PIN number for a given card, + /// this call is asynchronous, implement the completion block to receive the updates + @objc + public func retrievePin( + _ cardId: String, + verificationId: String, + oneTimeCode: String, + completion: @escaping STPPinCompletionBlock + ) { + let endpoint = "issuing/cards/\(cardId)/pin" + let parameters = [ + "verification": [ + "id": verificationId, + "one_time_code": oneTimeCode, + ], + ] + keyManager?.getOrCreateKey({ ephemeralKey, keyError in + if ephemeralKey == nil { + completion(nil, .ephemeralKeyError, keyError) + return + } + + if let ephemeralKey = ephemeralKey { + APIRequest.getWith( + self.apiClient, + endpoint: endpoint, + additionalHeaders: self.apiClient.authorizationHeader(using: ephemeralKey), + parameters: parameters + ) { details, _, error in + // Find if there were errors + if details?.error != nil { + let code = details?.error?["code"] as? String + if "api_key_expired" == code { + completion(nil, .ephemeralKeyError, error) + } else if "expired" == code { + completion(nil, .errorVerificationExpired, nil) + } else if "incorrect_code" == code { + completion(nil, .errorVerificationCodeIncorrect, nil) + } else if "too_many_attempts" == code { + completion(nil, .errorVerificationTooManyAttempts, nil) + } else if "already_redeemed" == code { + completion(nil, .errorVerificationAlreadyRedeemed, nil) + } else { + completion(nil, .unknownError, error) + } + return + } + completion(details, .success, nil) + } + } + }) + } + + /// Updates a PIN number for a given card, + /// this call is asynchronous, implement the completion block to receive the updates + @objc + public func updatePin( + _ cardId: String, + newPin: String, + verificationId: String, + oneTimeCode: String, + completion: @escaping STPPinCompletionBlock + ) { + let endpoint = "issuing/cards/\(cardId)/pin" + let parameters = + [ + "verification": [ + "id": verificationId, + "one_time_code": oneTimeCode, + ], + "pin": newPin, + ] as [String: Any] + keyManager?.getOrCreateKey({ ephemeralKey, keyError in + if ephemeralKey == nil { + completion(nil, .ephemeralKeyError, keyError) + return + } + if let ephemeralKey = ephemeralKey { + APIRequest.post( + with: self.apiClient, + endpoint: endpoint, + additionalHeaders: self.apiClient.authorizationHeader(using: ephemeralKey), + parameters: parameters + ) { details, _, error in + // Find if there were errors + if details?.error != nil { + let code = details?.error?["code"] as? String + if "api_key_expired" == code { + completion(nil, .ephemeralKeyError, error) + } else if "expired" == code { + completion(nil, .errorVerificationExpired, nil) + } else if "incorrect_code" == code { + completion(nil, .errorVerificationCodeIncorrect, nil) + } else if "too_many_attempts" == code { + completion(nil, .errorVerificationTooManyAttempts, nil) + } else if "already_redeemed" == code { + completion(nil, .errorVerificationAlreadyRedeemed, nil) + } else { + completion(nil, .unknownError, error) + } + return + } + completion(details, .success, nil) + } + } + }) + } + + private var keyManager: STPEphemeralKeyManagerProtocol? +} diff --git a/Stripe/StripeiOS/Source/STPPushProvisioningContext.swift b/Stripe/StripeiOS/Source/STPPushProvisioningContext.swift new file mode 100644 index 00000000..62973701 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPushProvisioningContext.swift @@ -0,0 +1,144 @@ +// +// STPPushProvisioningContext.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/27/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI + +/// This class makes it easier to implement "Push Provisioning", the process by which an end-user can add a card to their Apple Pay wallet without having to type their number. This process is mediated by an Apple class called `PKAddPaymentPassViewController`; this class will help you implement that class' delegate methods. Note that this flow requires a special entitlement from Apple; for more information please see https://stripe.com/docs/issuing/cards/digital-wallets . +public class STPPushProvisioningContext: NSObject { + /// The API Client to use to make requests. + /// Defaults to STPAPIClient.shared + public var apiClient: STPAPIClient = .shared + + /// This is a helper method to generate a PKAddPaymentPassRequestConfiguration that will work with + /// Stripe's Issuing APIs. Pass the returned configuration object to `PKAddPaymentPassViewController`'s `initWithRequestConfiguration:delegate:` initializer. + /// @deprecated Use requestConfiguration(withName:description:last4:brand:primaryAccountIdentifier:) instead. + /// - Parameters: + /// - name: Your cardholder's name. Example: John Appleseed + /// - description: A localized description of your card's name. This will appear in Apple's UI as "{description} will be available in Wallet". Example: Platinum Rewards Card + /// - last4: The last 4 of the card to be added to the user's Apple Pay wallet. Example: 4242 + /// - brand: The brand of the card. Example: `STPCardBrandVisa` + @objc + @available( + *, + deprecated, + message: + "Use `requestConfiguration(withName:description:last4:brand:primaryAccountIdentifier:)` instead.", + renamed: "requestConfiguration(withName:description:last4:brand:primaryAccountIdentifier:)" + ) + public class func requestConfiguration( + withName name: String, + description: String?, + last4: String?, + brand: STPCardBrand + ) -> PKAddPaymentPassRequestConfiguration { + return self.requestConfiguration( + withName: name, + description: description, + last4: last4, + brand: brand, + primaryAccountIdentifier: nil + ) + } + + /// This is a helper method to generate a PKAddPaymentPassRequestConfiguration that will work with + /// Stripe's Issuing APIs. Pass the returned configuration object to `PKAddPaymentPassViewController`'s `initWithRequestConfiguration:delegate:` initializer. + /// - Parameters: + /// - name: Your cardholder's name. Example: John Appleseed + /// - description: A localized description of your card's name. This will appear in Apple's UI as "{description} will be available in Wallet". Example: Platinum Rewards Card + /// - last4: The last 4 of the card to be added to the user's Apple Pay wallet. Example: 4242 + /// - brand: The brand of the card. Example: `STPCardBrandVisa` + /// - primaryAccountIdentifier: The `primary_account_identifier` value from the issued card. + @objc + public class func requestConfiguration( + withName name: String, + description: String?, + last4: String?, + brand: STPCardBrand, + primaryAccountIdentifier: String? + ) -> PKAddPaymentPassRequestConfiguration { + let config = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2) + config?.cardholderName = name + config?.primaryAccountSuffix = last4 + config?.localizedDescription = description + config?.style = .payment + if brand == .visa { + config?.paymentNetwork = .visa + } + if brand == .mastercard { + config?.paymentNetwork = .masterCard + } + config?.primaryAccountIdentifier = primaryAccountIdentifier + return config! + } + + /// In order to retreive the encrypted payload that PKAddPaymentPassViewController expects, the Stripe SDK must talk to the Stripe API. As this requires privileged access, you must write a "key provider" that generates an Ephemeral Key on your backend and provides it to the SDK when requested. For more information, see https://stripe.com/docs/mobile/ios/basic#ephemeral-key + @objc + public init( + keyProvider: STPIssuingCardEphemeralKeyProvider + ) { + apiClient = STPAPIClient.shared + keyManager = STPEphemeralKeyManager( + keyProvider: keyProvider, + apiVersion: STPAPIClient.apiVersion, + performsEagerFetching: false + ) + super.init() + } + + /// This method lines up with the method of the same name on `PKAddPaymentPassViewControllerDelegate`. You should implement that protocol in your own app, and when that method is called, call this method on your `STPPushProvisioningContext`. This in turn will first initiate a call to your `keyProvider` (see above) to obtain an Ephemeral Key, then make a call to the Stripe Issuing API to fetch an encrypted payload for the card in question, then return that payload to iOS. + @objc + public func addPaymentPassViewController( + _ controller: PKAddPaymentPassViewController, + generateRequestWithCertificateChain certificates: [Data], + nonce: Data, + nonceSignature: Data, + completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void + ) { + keyManager.getOrCreateKey({ ephemeralKey, keyError in + if let keyError = keyError { + let request = PKAddPaymentPassRequest() + request.stp_error = keyError as NSError + // handler, bizarrely, cannot take an NSError, but passing an empty PKAddPaymentPassRequest causes roughly equivalent behavior. + handler(request) + return + } + let params = STPPushProvisioningDetailsParams( + cardId: ephemeralKey?.issuingCardID ?? "", + certificates: certificates, + nonce: nonce, + nonceSignature: nonceSignature + ) + if let ephemeralKey = ephemeralKey { + self.apiClient.retrievePushProvisioningDetails( + with: params, + ephemeralKey: ephemeralKey + ) { + details, + error in + if let error = error { + let request = PKAddPaymentPassRequest() + request.stp_error = error as NSError + handler(request) + return + } + let request = PKAddPaymentPassRequest() + request.activationData = details?.activationData + request.encryptedPassData = details?.encryptedPassData + request.ephemeralPublicKey = details?.ephemeralPublicKey + handler(request) + } + } + }) + } + + private var keyManager: STPEphemeralKeyManagerProtocol + private var ephemeralKey: STPEphemeralKey? +} diff --git a/Stripe/StripeiOS/Source/STPPushProvisioningDetails.swift b/Stripe/StripeiOS/Source/STPPushProvisioningDetails.swift new file mode 100644 index 00000000..ae260456 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPushProvisioningDetails.swift @@ -0,0 +1,97 @@ +// +// STPPushProvisioningDetails.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/26/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePayments + +class STPPushProvisioningDetails: NSObject, STPAPIResponseDecodable { + let cardId: String + let livemode: Bool + let encryptedPassData: Data + let activationData: Data + let ephemeralPublicKey: Data + + required init( + cardId: String, + livemode: Bool, + encryptedPass encryptedPassData: Data, + activationData: Data, + ephemeralPublicKey: Data + ) { + self.cardId = cardId + self.livemode = livemode + self.encryptedPassData = encryptedPassData + self.activationData = activationData + self.ephemeralPublicKey = ephemeralPublicKey + super.init() + } + private(set) var allResponseFields: [AnyHashable: Any] = [:] + + // MARK: - STPAPIResponseDecodable + class func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard + let dict = response?.stp_dictionaryByRemovingNulls() + else { + return nil + } + + // required fields + let cardId = dict.stp_string(forKey: "card") + let livemode = dict.stp_bool(forKey: "livemode", or: false) + let encryptedPassString = dict.stp_string(forKey: "contents") + let encryptedPassData = + encryptedPassString != nil + ? Data(base64Encoded: encryptedPassString ?? "", options: []) : nil + + let activationString = dict.stp_string(forKey: "activation_data") + let activationData = + activationString != nil ? Data(base64Encoded: activationString ?? "", options: []) : nil + + let ephemeralPublicKeyString = dict.stp_string(forKey: "ephemeral_public_key") + let ephemeralPublicKeyData = + ephemeralPublicKeyString != nil + ? Data(base64Encoded: ephemeralPublicKeyString ?? "", options: []) : nil + + if cardId == nil || encryptedPassData == nil || activationData == nil + || ephemeralPublicKeyData == nil + { + return nil + } + + if let encryptedPassData = encryptedPassData, let activationData = activationData, + let ephemeralPublicKeyData = ephemeralPublicKeyData + { + let details = self.init( + cardId: cardId ?? "", + livemode: livemode, + encryptedPass: encryptedPassData, + activationData: activationData, + ephemeralPublicKey: ephemeralPublicKeyData + ) + details.allResponseFields = dict + return details + } + return nil + } + + // MARK: - Equality + override func isEqual(_ object: Any?) -> Bool { + if let details = object as? STPPushProvisioningDetails { + return isEqual(to: details) + } + return false + } + + override var hash: Int { + return activationData.hashValue + } + + func isEqual(to details: STPPushProvisioningDetails) -> Bool { + return details.activationData == self.activationData + } +} diff --git a/Stripe/StripeiOS/Source/STPPushProvisioningDetailsParams.swift b/Stripe/StripeiOS/Source/STPPushProvisioningDetailsParams.swift new file mode 100644 index 00000000..c7fbcccb --- /dev/null +++ b/Stripe/StripeiOS/Source/STPPushProvisioningDetailsParams.swift @@ -0,0 +1,73 @@ +// +// STPPushProvisioningDetailsParams.swift +// StripeiOS +// +// Created by Jack Flintermann on 9/26/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Foundation + +/// A helper class for turning the raw certificate array, nonce, and nonce signature emitted by PKAddPaymentPassViewController into a format that is understandable by the Stripe API. +/// If you are using STPPushProvisioningContext to implement your integration, you do not need to use this class. +public class STPPushProvisioningDetailsParams: NSObject { + /// The Stripe ID of the Issuing card object to retrieve details for. + @objc public private(set) var cardId: String + /// An array of certificates that should be used to encrypt the card details. + @objc public private(set) var certificates: [Data] + /// A nonce that should be used during the encryption of the card details. + @objc public private(set) var nonce: Data + /// A nonce signature that should be used during the encryption of the card details. + @objc public private(set) var nonceSignature: Data + /// Implemented for convenience - the Stripe API expects the certificate chain as an array of base64-encoded strings. + + @objc public var certificatesBase64: [String] { + var base64Certificates: [AnyHashable] = [] + for certificate in certificates { + base64Certificates.append(certificate.base64EncodedString(options: [])) + } + return base64Certificates as? [String] ?? [] + } + /// Implemented for convenience - the Stripe API expects the nonce as a hex-encoded string. + + @objc public var nonceHex: String { + STPPushProvisioningDetailsParams.hexadecimalString(for: nonce) + } + /// Implemented for convenience - the Stripe API expects the nonce signature as a hex-encoded string. + + @objc public var nonceSignatureHex: String { + STPPushProvisioningDetailsParams.hexadecimalString(for: nonceSignature) + } + + /// Instantiates a new params object with the provided attributes. + @objc public required init( + cardId: String, + certificates: [Data], + nonce: Data, + nonceSignature: Data + ) { + self.cardId = cardId + self.certificates = certificates + self.nonce = nonce + self.nonceSignature = nonceSignature + } + + @objc(paramsWithCardId:certificates:nonce:nonceSignature:) class func paramsWithCardId( + cardId: String, + certificates: [Data], + nonce: Data, + nonceSignature: Data + ) -> Self { + return self.init( + cardId: cardId, + certificates: certificates, + nonce: nonce, + nonceSignature: nonceSignature + ) + } + + // Adapted from https://stackoverflow.com/questions/39075043/how-to-convert-data-to-hex-string-in-swift + class func hexadecimalString(for data: Data) -> String { + return data.map { String(format: "%02hhx", $0) }.joined() + } +} diff --git a/Stripe/StripeiOS/Source/STPSectionHeaderView.swift b/Stripe/StripeiOS/Source/STPSectionHeaderView.swift new file mode 100644 index 00000000..2b45b2af --- /dev/null +++ b/Stripe/StripeiOS/Source/STPSectionHeaderView.swift @@ -0,0 +1,161 @@ +// +// STPSectionHeaderView.swift +// StripeiOS +// +// Created by Ben Guo on 1/3/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import UIKit + +class STPSectionHeaderView: UIView { + private var _theme: STPTheme = STPTheme.defaultTheme + var theme: STPTheme { + get { + _theme + } + set(theme) { + _theme = theme + updateAppearance() + } + } + + private var _title: String? + var title: String? { + get { + _title + } + set(title) { + _title = title + if let title = title { + let style = NSMutableParagraphStyle() + style.firstLineHeadIndent = 15 + style.headIndent = style.firstLineHeadIndent + let attributes = [ + NSAttributedString.Key.paragraphStyle: style + ] + label?.attributedText = NSAttributedString( + string: title, + attributes: attributes + ) + } else { + label?.attributedText = nil + } + setNeedsLayout() + } + } + weak var button: UIButton? + + private var _buttonHidden = false + var buttonHidden: Bool { + get { + _buttonHidden + } + set(buttonHidden) { + _buttonHidden = buttonHidden + button?.alpha = buttonHidden ? 0 : 1 + } + } + private weak var label: UILabel? + private let buttonInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 15) + + override init( + frame: CGRect + ) { + super.init(frame: frame) + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.accessibilityTraits.insert(.header) + addSubview(label) + self.label = label + let button = UIButton(type: .system) + button.contentHorizontalAlignment = .right + button.titleLabel?.numberOfLines = 0 + button.titleLabel?.lineBreakMode = .byWordWrapping + button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15) + button.contentEdgeInsets = .zero + addSubview(button) + self.button = button + backgroundColor = UIColor.clear + updateAppearance() + } + + @objc func updateAppearance() { + label?.font = theme.smallFont + label?.textColor = theme.secondaryForegroundColor + button?.titleLabel?.font = theme.smallFont + button?.tintColor = theme.accentColor + } + + override func layoutSubviews() { + super.layoutSubviews() + let bounds = stp_boundsWithHorizontalSafeAreaInsets() + if buttonHidden { + label?.frame = bounds + } else { + let halfWidth = bounds.size.width / 2 + let heightThatFits = self.heightThatFits(bounds.size) + label?.frame = CGRect( + x: bounds.origin.x, + y: bounds.origin.y, + width: halfWidth, + height: heightThatFits + ) + button?.frame = CGRect( + x: bounds.origin.x + halfWidth, + y: bounds.origin.y, + width: halfWidth, + height: heightThatFits + ) + } + } + + func heightThatFits(_ size: CGSize) -> CGFloat { + let labelPadding: CGFloat = 16 + if buttonHidden { + let labelHeight = label?.sizeThatFits(size).height ?? 0.0 + return labelHeight + labelPadding + } else { + let halfSize = CGSize(width: size.width / 2, height: size.height) + let labelHeight = (label?.sizeThatFits(halfSize).height ?? 0.0) + labelPadding + let buttonHeight = height( + forButtonText: button?.titleLabel?.text, + width: halfSize.width + ) + return CGFloat(max(buttonHeight, labelHeight)) + } + } + + private func height(forButtonText text: String?, width: CGFloat) -> CGFloat { + let insets = buttonInsets + let textSize = CGSize( + width: width - insets.left - insets.right, + height: CGFloat.greatestFiniteMagnitude + ) + var attributes: [NSAttributedString.Key: Any]? + if let font1 = button?.titleLabel?.font { + attributes = [ + NSAttributedString.Key.font: font1 + ] + } + let buttonSize = + text?.boundingRect( + with: textSize, + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil + ).size ?? .zero + return buttonSize.height + insets.top + insets.bottom + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + return CGSize(width: size.width, height: heightThatFits(size)) + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPShippingAddressViewController.swift b/Stripe/StripeiOS/Source/STPShippingAddressViewController.swift new file mode 100644 index 00000000..ac63189d --- /dev/null +++ b/Stripe/StripeiOS/Source/STPShippingAddressViewController.swift @@ -0,0 +1,674 @@ +// +// STPShippingAddressViewController.swift +// StripeiOS +// +// Created by Ben Guo on 8/29/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import PassKit +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +/// This view controller contains a shipping address collection form. It renders a right bar button item that submits the form, so it must be shown inside a `UINavigationController`. Depending on your configuration's shippingType, the view controller may present a shipping method selection form after the user enters an address. +public class STPShippingAddressViewController: STPCoreTableViewController { + + /// A convenience initializer; equivalent to calling `init(configuration: STPPaymentConfiguration.shared theme: STPTheme.defaultTheme currency:"" shippingAddress:nil selectedShippingMethod:nil prefilledInformation:nil)`. + @objc + public convenience init() { + self.init( + configuration: STPPaymentConfiguration.shared, + theme: STPTheme.defaultTheme, + currency: "", + shippingAddress: nil, + selectedShippingMethod: nil, + prefilledInformation: nil + ) + } + + /// Initializes a new `STPShippingAddressViewController` with the given payment context and sets the payment context as its delegate. + /// - Parameter paymentContext: The payment context to use. + @objc(initWithPaymentContext:) + public convenience init( + paymentContext: STPPaymentContext + ) { + STPAnalyticsClient.sharedClient.addClass( + toProductUsageIfNecessary: STPShippingAddressViewController.self + ) + + var billingAddress: STPAddress? + weak var paymentOption = paymentContext.selectedPaymentOption + if paymentOption is STPCard { + let card = paymentOption as? STPCard + billingAddress = card?.address + } else if paymentOption is STPPaymentMethod { + let paymentMethod = paymentOption as? STPPaymentMethod + if let billingDetails1 = paymentMethod?.billingDetails { + billingAddress = STPAddress(paymentMethodBillingDetails: billingDetails1) + } + } + var prefilledInformation: STPUserInformation? + if paymentContext.prefilledInformation != nil { + prefilledInformation = paymentContext.prefilledInformation + } else { + prefilledInformation = STPUserInformation() + } + prefilledInformation?.billingAddress = billingAddress + self.init( + configuration: paymentContext.configuration, + theme: paymentContext.theme, + currency: paymentContext.paymentCurrency, + shippingAddress: paymentContext.shippingAddress, + selectedShippingMethod: paymentContext.selectedShippingMethod, + prefilledInformation: prefilledInformation + ) + + self.delegate = paymentContext + } + + /// Initializes a new `STPShippingAddressCardViewController` with the provided parameters. + /// - Parameters: + /// - configuration: The configuration to use (this determines the required shipping address fields and shipping type). - seealso: STPPaymentConfiguration + /// - theme: The theme to use to inform the view controller's visual appearance. - seealso: STPTheme + /// - currency: The currency to use when displaying amounts for shipping methods. The default is USD. + /// - shippingAddress: If set, the shipping address view controller will be pre-filled with this address. - seealso: STPAddress + /// - selectedShippingMethod: If set, the shipping methods view controller will use this method as the selected shipping method. If `selectedShippingMethod` is nil, the first shipping method in the array of methods returned by your delegate will be selected. + /// - prefilledInformation: If set, the shipping address view controller will be pre-filled with this information. - seealso: STPUserInformation + @objc( + initWithConfiguration: + theme: + currency: + shippingAddress: + selectedShippingMethod: + prefilledInformation: + ) + public init( + configuration: STPPaymentConfiguration, + theme: STPTheme, + currency: String?, + shippingAddress: STPAddress?, + selectedShippingMethod: PKShippingMethod?, + prefilledInformation: STPUserInformation? + ) { + STPAnalyticsClient.sharedClient.addClass( + toProductUsageIfNecessary: STPShippingAddressViewController.self + ) + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(theme: theme) + assert( + (configuration.requiredShippingAddressFields?.count ?? 0) > 0, + "`requiredShippingAddressFields` must not be empty when initializing an STPShippingAddressViewController." + ) + self.configuration = configuration + self.currency = currency + self.selectedShippingMethod = selectedShippingMethod + billingAddress = prefilledInformation?.billingAddress + hasUsedBillingAddress = false + addressViewModel = STPAddressViewModel( + requiredShippingFields: configuration.requiredShippingAddressFields ?? [], + availableCountries: configuration.availableCountries + ) + addressViewModel.delegate = self + if let shippingAddress = shippingAddress { + addressViewModel.address = shippingAddress + } else if prefilledInformation?.shippingAddress != nil { + addressViewModel.address = prefilledInformation?.shippingAddress ?? STPAddress() + } + title = title(for: self.configuration?.shippingType ?? .shipping) + } + + /// The view controller's delegate. This must be set before showing the view controller in order for it to work properly. - seealso: STPShippingAddressViewControllerDelegate + @objc public weak var delegate: STPShippingAddressViewControllerDelegate? + + /// If you're pushing `STPShippingAddressViewController` onto an existing `UINavigationController`'s stack, you should use this method to dismiss it, since it may have pushed an additional shipping method view controller onto the navigation controller's stack. + /// - Parameter completion: The callback to run after the view controller is dismissed. You may specify nil for this parameter. + @objc(dismissWithCompletion:) + public func dismiss(withCompletion completion: STPVoidBlock?) { + if stp_isAtRootOfNavigationController() { + presentingViewController?.dismiss(animated: true, completion: completion ?? {}) + } else { + var previous = navigationController?.viewControllers.first + for viewController in navigationController?.viewControllers ?? [] { + if viewController == self { + break + } + previous = viewController + } + navigationController?.stp_pop( + to: previous, + animated: true, + completion: completion ?? {} + ) + } + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + @objc public required init( + theme: STPTheme? + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + + super.init(theme: theme) + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + @objc public required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + /// Use one of the initializers declared in this interface. + @available( + *, + unavailable, + message: "Use one of the initializers declared in this interface instead." + ) + required init?( + coder aDecoder: NSCoder + ) { + let configuration = STPPaymentConfiguration.shared + addressViewModel = STPAddressViewModel( + requiredBillingFields: configuration.requiredBillingAddressFields, + availableCountries: configuration.availableCountries + ) + super.init(coder: aDecoder) + } + + private var configuration: STPPaymentConfiguration? + private var currency: String? + private var selectedShippingMethod: PKShippingMethod? + private weak var imageView: UIImageView? + private var nextItem: UIBarButtonItem? + + private var _loading = false + private var loading: Bool { + get { + _loading + } + set(loading) { + if loading == _loading { + return + } + _loading = loading + stp_navigationItemProxy?.setHidesBackButton(loading, animated: true) + stp_navigationItemProxy?.leftBarButtonItem?.isEnabled = !loading + activityIndicator?.animating = loading + if loading { + tableView?.endEditing(true) + var loadingItem: UIBarButtonItem? + if let activityIndicator = activityIndicator { + loadingItem = UIBarButtonItem(customView: activityIndicator) + } + stp_navigationItemProxy?.setRightBarButton(loadingItem, animated: true) + } else { + stp_navigationItemProxy?.setRightBarButton(nextItem, animated: true) + } + for cell in addressViewModel.addressCells { + cell.isUserInteractionEnabled = !loading + UIView.animate( + withDuration: 0.1, + animations: { + cell.alpha = loading ? 0.7 : 1.0 + } + ) + } + } + } + private var activityIndicator: STPPaymentActivityIndicatorView? + internal var addressViewModel: STPAddressViewModel + private var billingAddress: STPAddress? + private var hasUsedBillingAddress = false + private var addressHeaderView: STPSectionHeaderView? + + override func createAndSetupViews() { + super.createAndSetupViews() + + var nextItem: UIBarButtonItem? + switch configuration?.shippingType { + case .shipping: + nextItem = UIBarButtonItem( + title: STPLocalizedString("Next", "Button to move to the next text entry field"), + style: .done, + target: self, + action: #selector(next(_:)) + ) + case .delivery, .none, .some: + nextItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(next(_:)) + ) + } + self.nextItem = nextItem + stp_navigationItemProxy?.rightBarButtonItem = nextItem + stp_navigationItemProxy?.rightBarButtonItem?.isEnabled = false + stp_navigationItemProxy?.rightBarButtonItem?.accessibilityIdentifier = + "ShippingViewControllerNextButtonIdentifier" + + let imageView = UIImageView(image: STPLegacyImageLibrary.largeShippingImage()) + imageView.contentMode = .center + imageView.frame = CGRect( + x: 0, + y: 0, + width: view.bounds.size.width, + height: imageView.bounds.size.height + (57 * 2) + ) + self.imageView = imageView + tableView?.tableHeaderView = imageView + + activityIndicator = STPPaymentActivityIndicatorView( + frame: CGRect(x: 0, y: 0, width: 20.0, height: 20.0) + ) + + tableView?.dataSource = self + tableView?.delegate = self + tableView?.reloadData() + view.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(NSMutableAttributedString.endEditing) + ) + ) + + let headerView = STPSectionHeaderView() + headerView.theme = theme + if let shippingType1 = configuration?.shippingType { + headerView.title = headerTitle(for: shippingType1) + } + headerView.button?.setTitle( + STPLocalizedString( + "Use Billing", + "Button to fill shipping address from billing address." + ), + for: .normal + ) + headerView.button?.addTarget( + self, + action: #selector(useBillingAddress(_:)), + for: .touchUpInside + ) + headerView.button?.accessibilityIdentifier = "ShippingAddressViewControllerUseBillingButton" + var buttonVisible = false + if let requiredFields = configuration?.requiredShippingAddressFields { + let needsAddress = + requiredFields.contains(.postalAddress) && !(addressViewModel.isValid) + buttonVisible = + needsAddress + && billingAddress?.containsContent(forShippingAddressFields: requiredFields) + ?? false + && !hasUsedBillingAddress + } + headerView.button?.alpha = buttonVisible ? 1 : 0 + headerView.setNeedsLayout() + addressHeaderView = headerView + + updateDoneButton() + } + + @objc func endEditing() { + view.endEditing(false) + } + + @objc override func updateAppearance() { + super.updateAppearance() + let navBarTheme = navigationController?.navigationBar.stp_theme ?? theme + nextItem?.stp_setTheme(navBarTheme) + + tableView?.allowsSelection = false + + imageView?.tintColor = theme.accentColor + activityIndicator?.tintColor = theme.accentColor + for cell in addressViewModel.addressCells { + cell.theme = theme + } + addressHeaderView?.theme = theme + } + + /// :nodoc: + @objc + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + stp_beginObservingKeyboardAndInsettingScrollView( + tableView, + onChange: nil + ) + firstEmptyField()?.becomeFirstResponder() + } + + func firstEmptyField() -> UIResponder? { + for cell in addressViewModel.addressCells { + if (cell.contents?.count ?? 0) == 0 { + return cell + } + } + return nil + } + + @objc override func handleCancelTapped(_ sender: Any?) { + delegate?.shippingAddressViewControllerDidCancel(self) + } + + @objc func next(_ sender: Any?) { + let address = addressViewModel.address + switch configuration?.shippingType { + case .shipping: + loading = true + delegate?.shippingAddressViewController(self, didEnter: address) { + status, + shippingValidationError, + shippingMethods, + selectedShippingMethod in + self.loading = false + if status == .valid { + if (shippingMethods?.count ?? 0) > 0 { + var nextViewController: STPShippingMethodsViewController? + if let shippingMethods = shippingMethods, + let selectedShippingMethod = selectedShippingMethod + { + nextViewController = STPShippingMethodsViewController( + shippingMethods: shippingMethods, + selectedShippingMethod: selectedShippingMethod, + currency: self.currency ?? "", + theme: self.theme + ) + } + nextViewController?.delegate = self + if let nextViewController = nextViewController { + self.navigationController?.pushViewController( + nextViewController, + animated: true + ) + } + } else { + self.delegate?.shippingAddressViewController( + self, + didFinishWith: address, + shippingMethod: nil + ) + } + } else { + self.handleShippingValidationError(shippingValidationError) + } + } + case .delivery, .none, .some: + delegate?.shippingAddressViewController( + self, + didFinishWith: address, + shippingMethod: nil + ) + } + } + + func updateDoneButton() { + stp_navigationItemProxy?.rightBarButtonItem?.isEnabled = addressViewModel.isValid + } + + func handleShippingValidationError(_ error: Error?) { + firstEmptyField()?.becomeFirstResponder() + var title = STPLocalizedString("Invalid Shipping Address", "Shipping form error message") + var message: String? + if let error = error { + title = error.localizedDescription + message = (error as NSError).localizedFailureReason + } + let alertController = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction( + title: String.Localized.ok, + style: .cancel, + handler: nil + ) + ) + present(alertController, animated: true) + } + + /// :nodoc: + @objc + public override func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) -> CGFloat { + let size = addressHeaderView?.sizeThatFits( + CGSize(width: view.bounds.size.width, height: CGFloat.greatestFiniteMagnitude) + ) + return size?.height ?? 0.0 + } + + @objc func useBillingAddress(_ sender: UIButton) { + guard let billingAddress = billingAddress else { + return + } + tableView?.beginUpdates() + addressViewModel.address = billingAddress + hasUsedBillingAddress = true + firstEmptyField()?.becomeFirstResponder() + UIView.animate( + withDuration: 0.2, + animations: { + self.addressHeaderView?.buttonHidden = true + } + ) + tableView?.endUpdates() + } + + func title(for type: STPShippingType) -> String { + if let shippingAddressFields = configuration?.requiredShippingAddressFields, + shippingAddressFields.contains(.postalAddress) + { + switch type { + case .shipping: + return STPLocalizedString("Shipping", "Title for shipping info form") + case .delivery: + return STPLocalizedString("Delivery", "Title for delivery info form") + } + } else { + return STPLocalizedString("Contact", "Title for contact info form") + } + } + + func headerTitle(for type: STPShippingType) -> String { + if let shippingAddressFields = configuration?.requiredShippingAddressFields, + shippingAddressFields.contains(.postalAddress) + { + switch type { + case .shipping: + return String.Localized.shipping_address + case .delivery: + return STPLocalizedString( + "Delivery Address", + "Title for delivery address entry section" + ) + } + } else { + return STPLocalizedString("Contact", "Title for contact info form") + } + } +} + +/// An `STPShippingAddressViewControllerDelegate` is notified when an `STPShippingAddressViewController` receives an address, completes with an address, or is cancelled. +@objc public protocol STPShippingAddressViewControllerDelegate: NSObjectProtocol { + /// Called when the user cancels entering a shipping address. You should dismiss (or pop) the view controller at this point. + /// - Parameter addressViewController: the view controller that has been cancelled + func shippingAddressViewControllerDidCancel( + _ addressViewController: STPShippingAddressViewController + ) + /// This is called when the user enters a shipping address and taps next. You + /// should validate the address and determine what shipping methods are available, + /// and call the `completion` block when finished. If an error occurrs, call + /// the `completion` block with the error. Otherwise, call the `completion` + /// block with a nil error and an array of available shipping methods. If you don't + /// need to collect a shipping method, you may pass an empty array or nil. + /// - Parameters: + /// - addressViewController: the view controller where the address was entered + /// - address: the address that was entered. - seealso: STPAddress + /// - completion: call this callback when you're done validating the address and determining available shipping methods. + + @objc(shippingAddressViewController:didEnterAddress:completion:) + func shippingAddressViewController( + _ addressViewController: STPShippingAddressViewController, + didEnter address: STPAddress, + completion: @escaping STPShippingMethodsCompletionBlock + ) + /// This is called when the user selects a shipping method. If no shipping methods are given, or if the shipping type doesn't require a shipping method, this will be called after the user has a shipping address and your validation has succeeded. After updating your app with the user's shipping info, you should dismiss (or pop) the view controller. Note that if `shippingMethod` is non-nil, there will be an additional shipping methods view controller on the navigation controller's stack. + /// - Parameters: + /// - addressViewController: the view controller where the address was entered + /// - address: the address that was entered. - seealso: STPAddress + /// - method: the shipping method that was selected. + @objc(shippingAddressViewController:didFinishWithAddress:shippingMethod:) + func shippingAddressViewController( + _ addressViewController: STPShippingAddressViewController, + didFinishWith address: STPAddress, + shippingMethod method: PKShippingMethod? + ) +} + +extension STPShippingAddressViewController: + STPAddressViewModelDelegate, UITableViewDelegate, UITableViewDataSource, + STPShippingMethodsViewControllerDelegate +{ + + // MARK: - UITableView + /// :nodoc: + @objc + public func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + /// :nodoc: + @objc + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return addressViewModel.addressCells.count + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + let cell = + addressViewModel.addressCells.stp_boundSafeObject(at: indexPath.row) + cell?.backgroundColor = theme.secondaryBackgroundColor + cell?.contentView.backgroundColor = UIColor.clear + return cell! + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let topRow = indexPath.row == 0 + let bottomRow = tableView.numberOfRows(inSection: indexPath.section) - 1 == indexPath.row + cell.stp_setBorderColor(theme.tertiaryBackgroundColor) + cell.stp_setTopBorderHidden(!topRow) + cell.stp_setBottomBorderHidden(!bottomRow) + cell.stp_setFakeSeparatorColor(theme.quaternaryBackgroundColor) + cell.stp_setFakeSeparatorLeftInset(15.0) + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + heightForFooterInSection section: Int + ) + -> CGFloat + { + return 0.01 + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + viewForFooterInSection section: Int + ) + -> UIView? + { + return UIView() + } + + /// :nodoc: + @objc + public func tableView( + _ tableView: UITableView, + viewForHeaderInSection section: Int + ) + -> UIView? + { + return addressHeaderView + } + + // MARK: - STPShippingMethodsViewControllerDelegate + func shippingMethodsViewController( + _ methodsViewController: STPShippingMethodsViewController, + didFinishWith method: PKShippingMethod + ) { + delegate?.shippingAddressViewController( + self, + didFinishWith: addressViewModel.address, + shippingMethod: method + ) + } + + // MARK: - STPAddressViewModelDelegate + func addressViewModel(_ addressViewModel: STPAddressViewModel, addedCellAt index: Int) { + let indexPath = IndexPath(row: index, section: 0) + tableView?.insertRows(at: [indexPath], with: .automatic) + } + + func addressViewModel(_ addressViewModel: STPAddressViewModel, removedCellAt index: Int) { + let indexPath = IndexPath(row: index, section: 0) + tableView?.deleteRows(at: [indexPath], with: .automatic) + } + + func addressViewModelDidChange(_ addressViewModel: STPAddressViewModel) { + updateDoneButton() + } + + func addressViewModelWillUpdate(_ addressViewModel: STPAddressViewModel) { + tableView?.beginUpdates() + } + + func addressViewModelDidUpdate(_ addressViewModel: STPAddressViewModel) { + tableView?.endUpdates() + } +} + +/// :nodoc: +@_spi(STP) extension STPShippingAddressViewController: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier = "STPShippingAddressViewController" +} diff --git a/Stripe/StripeiOS/Source/STPShippingMethodTableViewCell.swift b/Stripe/StripeiOS/Source/STPShippingMethodTableViewCell.swift new file mode 100644 index 00000000..f40c493c --- /dev/null +++ b/Stripe/StripeiOS/Source/STPShippingMethodTableViewCell.swift @@ -0,0 +1,147 @@ +// +// STPShippingMethodTableViewCell.swift +// StripeiOS +// +// Created by Ben Guo on 8/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import PassKit +@_spi(STP) import StripePayments +import UIKit + +class STPShippingMethodTableViewCell: UITableViewCell { + private var _theme: STPTheme? + var theme: STPTheme? { + get { + _theme + } + set(theme) { + _theme = theme + updateAppearance() + } + } + + func setShippingMethod(_ method: PKShippingMethod, currency: String) { + shippingMethod = method + titleLabel?.text = method.label + subtitleLabel?.text = method.detail + var localeInfo = [ + NSLocale.Key.currencyCode.rawValue: currency + ] + localeInfo[NSLocale.Key.languageCode.rawValue] = NSLocale.preferredLanguages.first ?? "" + let localeID = NSLocale.localeIdentifier(fromComponents: localeInfo) + let locale = NSLocale(localeIdentifier: localeID) + numberFormatter?.locale = locale as Locale + let amount = method.amount.stp_amount(withCurrency: currency) + if amount == 0 { + amountLabel?.text = STPLocalizedString("Free", "Label for free shipping method") + } else { + let number = NSDecimalNumber.stp_decimalNumber( + withAmount: amount, + currency: currency + ) + amountLabel?.text = numberFormatter?.string(from: number) + } + setNeedsLayout() + } + + private weak var titleLabel: UILabel? + private weak var subtitleLabel: UILabel? + private weak var amountLabel: UILabel? + private weak var checkmarkIcon: UIImageView? + private var shippingMethod: PKShippingMethod? + private var numberFormatter: NumberFormatter? + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + theme = STPTheme() + let titleLabel = UILabel() + self.titleLabel = titleLabel + let subtitleLabel = UILabel() + self.subtitleLabel = subtitleLabel + let amountLabel = UILabel() + self.amountLabel = amountLabel + let checkmarkIcon = UIImageView(image: STPLegacyImageLibrary.checkmarkIcon()) + self.checkmarkIcon = checkmarkIcon + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.usesGroupingSeparator = true + numberFormatter = formatter + contentView.addSubview(titleLabel) + contentView.addSubview(subtitleLabel) + contentView.addSubview(amountLabel) + contentView.addSubview(checkmarkIcon) + updateAppearance() + } + + override var isSelected: Bool { + get { + return super.isSelected + } + set(selected) { + super.isSelected = selected + updateAppearance() + } + } + + @objc func updateAppearance() { + contentView.backgroundColor = theme?.secondaryBackgroundColor + backgroundColor = UIColor.clear + titleLabel?.font = theme?.font + subtitleLabel?.font = theme?.smallFont + amountLabel?.font = theme?.font + titleLabel?.textColor = isSelected ? theme?.accentColor : theme?.primaryForegroundColor + amountLabel?.textColor = titleLabel?.textColor + var subduedAccentColor: UIColor? + if #available(iOS 13.0, *) { + subduedAccentColor = UIColor(dynamicProvider: { _ in + return self.theme?.accentColor.withAlphaComponent(0.6) ?? UIColor.clear + }) + } else { + subduedAccentColor = theme?.accentColor.withAlphaComponent(0.6) + } + subtitleLabel?.textColor = isSelected ? subduedAccentColor : theme?.secondaryForegroundColor + checkmarkIcon?.tintColor = theme?.accentColor + checkmarkIcon?.isHidden = !isSelected + } + + override func layoutSubviews() { + super.layoutSubviews() + let midY = bounds.midY + checkmarkIcon?.frame = CGRect(x: 0, y: 0, width: 14, height: 14) + checkmarkIcon?.center = CGPoint( + x: bounds.width - 15 - (checkmarkIcon?.bounds.midX ?? 0.0), + y: midY + ) + amountLabel?.sizeToFit() + amountLabel?.center = CGPoint( + x: (checkmarkIcon?.frame.minX ?? 0.0) - 15 - (amountLabel?.bounds.midX ?? 0.0), + y: midY + ) + let labelWidth = (amountLabel?.frame.minX ?? 0.0) - 30 + titleLabel?.sizeToFit() + titleLabel?.frame = CGRect( + x: 15, + y: 8, + width: labelWidth, + height: titleLabel?.frame.size.height ?? 0.0 + ) + subtitleLabel?.sizeToFit() + subtitleLabel?.frame = CGRect( + x: 15, + y: bounds.size.height - 8 - (subtitleLabel?.frame.size.height ?? 0.0), + width: labelWidth, + height: subtitleLabel?.frame.size.height ?? 0.0 + ) + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/STPShippingMethodsViewController.swift b/Stripe/StripeiOS/Source/STPShippingMethodsViewController.swift new file mode 100644 index 00000000..8d915855 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPShippingMethodsViewController.swift @@ -0,0 +1,211 @@ +// +// STPShippingMethodsViewController.swift +// StripeiOS +// +// Created by Ben Guo on 8/29/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import PassKit +@_spi(STP) import StripeCore +import UIKit + +class STPShippingMethodsViewController: STPCoreTableViewController, UITableViewDataSource, + UITableViewDelegate +{ + init( + shippingMethods methods: [PKShippingMethod], + selectedShippingMethod selectedMethod: PKShippingMethod, + currency: String, + theme: STPTheme + ) { + super.init(theme: theme) + shippingMethods = methods + if (methods.firstIndex(of: selectedMethod) ?? NSNotFound) != NSNotFound { + selectedShippingMethod = selectedMethod + } else { + selectedShippingMethod = methods.stp_boundSafeObject(at: 0) + } + + self.currency = currency + title = STPLocalizedString("Shipping", "Title for shipping info form") + } + + weak var delegate: STPShippingMethodsViewControllerDelegate? + private var shippingMethods: [PKShippingMethod]? + private var selectedShippingMethod: PKShippingMethod? + private var currency: String? + private weak var imageView: UIImageView? + private var doneItem: UIBarButtonItem? + private var headerView: STPSectionHeaderView? + + override func createAndSetupViews() { + super.createAndSetupViews() + + tableView?.register( + STPShippingMethodTableViewCell.self, + forCellReuseIdentifier: STPShippingMethodCellReuseIdentifier + ) + + let doneItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(done(_:)) + ) + self.doneItem = doneItem + stp_navigationItemProxy?.rightBarButtonItem = doneItem + stp_navigationItemProxy?.rightBarButtonItem?.accessibilityIdentifier = + "ShippingMethodsViewControllerDoneButtonIdentifier" + + let imageView = UIImageView(image: STPLegacyImageLibrary.largeShippingImage()) + imageView.contentMode = .center + imageView.frame = CGRect( + x: 0, + y: 0, + width: view.bounds.size.width, + height: imageView.bounds.size.height + (57 * 2) + ) + self.imageView = imageView + + tableView?.tableHeaderView = imageView + tableView?.dataSource = self + tableView?.delegate = self + tableView?.reloadData() + + let headerView = STPSectionHeaderView() + headerView.theme = theme + headerView.buttonHidden = true + headerView.title = STPLocalizedString("Shipping Method", "Label for shipping method form") + headerView.setNeedsLayout() + self.headerView = headerView + } + + @objc override func updateAppearance() { + super.updateAppearance() + + let navBarTheme = navigationController?.navigationBar.stp_theme ?? theme + doneItem?.stp_setTheme(navBarTheme) + + imageView?.tintColor = theme.accentColor + for cell in tableView?.visibleCells ?? [] { + let shippingCell = cell as? STPShippingMethodTableViewCell + shippingCell?.theme = theme + } + } + + @objc func done(_ sender: Any?) { + if let selectedShippingMethod = selectedShippingMethod { + delegate?.shippingMethodsViewController(self, didFinishWith: selectedShippingMethod) + } + } + + override func useSystemBackButton() -> Bool { + return true + } + + // MARK: - UITableView + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return shippingMethods?.count ?? 0 + } + + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + let cell = + tableView.dequeueReusableCell( + withIdentifier: STPShippingMethodCellReuseIdentifier, + for: indexPath + ) + as? STPShippingMethodTableViewCell + let method = + shippingMethods?.stp_boundSafeObject(at: indexPath.row) + cell?.theme = theme + if let method = method { + cell?.setShippingMethod(method, currency: currency ?? "") + } + cell?.isSelected = method?.identifier == selectedShippingMethod?.identifier + return cell! + } + + func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let topRow = indexPath.row == 0 + let bottomRow = + self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 == indexPath.row + cell.stp_setBorderColor(theme.tertiaryBackgroundColor) + cell.stp_setTopBorderHidden(!topRow) + cell.stp_setBottomBorderHidden(!bottomRow) + cell.stp_setFakeSeparatorColor(theme.quaternaryBackgroundColor) + cell.stp_setFakeSeparatorLeftInset(15.0) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 57 + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return 27.0 + } + + override func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) + -> CGFloat + { + let size = headerView?.sizeThatFits( + CGSize(width: view.bounds.size.width, height: CGFloat.greatestFiniteMagnitude) + ) + return size?.height ?? 0.0 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return headerView + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + selectedShippingMethod = + shippingMethods?.stp_boundSafeObject(at: indexPath.row) + tableView.reloadSections( + NSIndexSet(index: indexPath.section) as IndexSet, + with: .fade + ) + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } + + required init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + fatalError("init(nibName:bundle:) has not been implemented") + } + + required init( + theme: STPTheme? + ) { + fatalError("init(theme:) has not been implemented") + } +} + +@objc protocol STPShippingMethodsViewControllerDelegate: NSObjectProtocol { + func shippingMethodsViewController( + _ methodsViewController: STPShippingMethodsViewController, + didFinishWith method: PKShippingMethod + ) +} + +private let STPShippingMethodCellReuseIdentifier = "STPShippingMethodCellReuseIdentifier" diff --git a/Stripe/StripeiOS/Source/STPSource+BasicUI.swift b/Stripe/StripeiOS/Source/STPSource+BasicUI.swift new file mode 100644 index 00000000..67510e70 --- /dev/null +++ b/Stripe/StripeiOS/Source/STPSource+BasicUI.swift @@ -0,0 +1,74 @@ +// +// STPSource+BasicUI.swift +// StripeiOS +// +// Created by David Estes on 6/30/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripePayments +import UIKit + +extension STPSource: STPPaymentOption { + // MARK: - STPPaymentOption + @objc public var image: UIImage { + if type == .card, let cardDetails = cardDetails { + return STPImageLibrary.cardBrandImage(for: cardDetails.brand) + } else { + return STPImageLibrary.cardBrandImage(for: .unknown) + } + } + + @objc public var templateImage: UIImage { + if type == .card, let cardDetails = cardDetails { + return STPImageLibrary.templatedBrandImage(for: cardDetails.brand) + } else { + return STPImageLibrary.templatedBrandImage(for: .unknown) + } + } + + @objc public var label: String { + switch type { + case .bancontact: + return STPPaymentMethodType.bancontact.displayName + case .card: + if let cardDetails = cardDetails { + let brand = STPCard.string(from: cardDetails.brand) + return "\(brand) \(cardDetails.last4 ?? "")" + } else { + return STPCard.string(from: .unknown) + } + case .giropay: + return STPPaymentMethodType.giropay.displayName + case .iDEAL: + return STPPaymentMethodType.iDEAL.displayName + case .SEPADebit: + return STPPaymentMethodType.SEPADebit.displayName + case .sofort: + return STPPaymentMethodType.sofort.displayName + case .threeDSecure: + return STPLocalizedString("3D Secure", "Source type brand name") + case .alipay: + return STPPaymentMethodType.alipay.displayName + case .P24: + return STPPaymentMethodType.przelewy24.displayName + case .EPS: + return STPPaymentMethodType.EPS.displayName + case .multibanco: + return STPLocalizedString("Multibanco", "Source type brand name") + case .weChatPay: + return STPPaymentMethodType.weChatPay.displayName + case .klarna: + return STPPaymentMethodType.klarna.displayName + case .unknown: + return STPPaymentMethodType.unknown.displayName + @unknown default: + return STPPaymentMethodType.unknown.displayName + } + } + + @objc public var isReusable: Bool { + return usage != .singleUse + } +} diff --git a/Stripe/StripeiOS/Source/STPTheme.swift b/Stripe/StripeiOS/Source/STPTheme.swift new file mode 100644 index 00000000..2a36f1ef --- /dev/null +++ b/Stripe/StripeiOS/Source/STPTheme.swift @@ -0,0 +1,212 @@ +// +// STPTheme.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/3/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeUICore +import UIKit + +// swift-format-ignore: DontRepeatTypeInStaticProperties +/// STPTheme objects can be used to visually style Stripe-provided UI. See https://stripe.com/docs/mobile/ios/basic#theming for more information. +final public class STPTheme: NSObject { + + /// The default theme used by all Stripe UI. All themable UI classes, such as `STPAddCardViewController`, have one initializer that takes a `theme` and one that does not. If you use the one that does not, the default theme will be used to customize that view controller's appearance. + @objc public static let defaultTheme = STPTheme() + + /// :nodoc: + @available(*, deprecated, message: "Use defaultTheme instead", renamed: "defaultTheme") + public static func `default`() -> STPTheme { + return STPTheme.defaultTheme + } + + /// The primary background color of the theme. This will be used as the `backgroundColor` for any views with this theme. + @objc public var primaryBackgroundColor: UIColor = STPThemeDefaultPrimaryBackgroundColor + + /// The secondary background color of this theme. This will be used as the `backgroundColor` for any supplemental views inside a view with this theme - for example, a `UITableView` will set it's cells' background color to this value. + @objc public var secondaryBackgroundColor: UIColor = STPThemeDefaultSecondaryBackgroundColor + + /// This color is automatically derived by reducing the alpha of the `primaryBackgroundColor` and is used as a section border color in table view cells. + @objc public var tertiaryBackgroundColor: UIColor { + let colorBlock: STPColorBlock = { + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + self.primaryBackgroundColor.getHue( + &hue, + saturation: &saturation, + brightness: &brightness, + alpha: &alpha + ) + + return UIColor( + hue: hue, + saturation: saturation, + brightness: brightness - 0.09, + alpha: alpha + ) + } + return UIColor(dynamicProvider: { _ in + return colorBlock() + }) + } + + /// This color is automatically derived by reducing the brightness of the `primaryBackgroundColor` and is used as a separator color in table view cells. + @objc public var quaternaryBackgroundColor: UIColor { + let colorBlock: STPColorBlock = { + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + self.primaryBackgroundColor.getHue( + &hue, + saturation: &saturation, + brightness: &brightness, + alpha: &alpha + ) + + return UIColor( + hue: hue, + saturation: saturation, + brightness: brightness - 0.03, + alpha: alpha + ) + } + return UIColor(dynamicProvider: { _ in + return colorBlock() + }) + } + + /// The primary foreground color of this theme. This will be used as the text color for any important labels in a view with this theme (such as the text color for a text field that the user needs to fill out). + @objc public var primaryForegroundColor: UIColor = STPThemeDefaultPrimaryForegroundColor + + /// The secondary foreground color of this theme. This will be used as the text color for any supplementary labels in a view with this theme (such as the placeholder color for a text field that the user needs to fill out). + @objc public var secondaryForegroundColor: UIColor = STPThemeDefaultSecondaryForegroundColor + + /// This color is automatically derived from the `secondaryForegroundColor` with a lower alpha component, used for disabled text. + @objc public var tertiaryForegroundColor: UIColor { + return UIColor(dynamicProvider: { _ in + return self.primaryForegroundColor.withAlphaComponent(0.25) + }) + } + + /// The accent color of this theme - it will be used for any buttons and other elements on a view that are important to highlight. + @objc public var accentColor: UIColor = STPThemeDefaultAccentColor + + /// The error color of this theme - it will be used for rendering any error messages or views. + @objc public var errorColor: UIColor = STPThemeDefaultErrorColor + + /// The font to be used for all views using this theme. Make sure to select an appropriate size. + @objc public var font: UIFont { + get { + if let _font = _font { + return _font + } else { + let fontMetrics = UIFontMetrics(forTextStyle: .body) + return fontMetrics.scaledFont(for: STPThemeDefaultFont) + } + } + set { + _font = newValue + } + } + private var _font: UIFont? + + /// The medium-weight font to be used for all bold text in views using this theme. Make sure to select an appropriate size. + @objc public var emphasisFont: UIFont { + get { + if let _emphasisFont = _emphasisFont { + return _emphasisFont + } else { + let fontMetrics = UIFontMetrics(forTextStyle: .body) + return fontMetrics.scaledFont(for: STPThemeDefaultMediumFont) + } + } + set { + _emphasisFont = newValue + } + } + private var _emphasisFont: UIFont? + + /// The navigation bar style to use for any view controllers presented modally + /// by the SDK. The default value will be determined based on the brightness + /// of the theme's `secondaryBackgroundColor`. + @objc public var barStyle: UIBarStyle { + get { + if let _barStyle = _barStyle { + return _barStyle + } else { + return barStyle(for: secondaryBackgroundColor) + } + } + set { + _barStyle = newValue + } + } + private var _barStyle: UIBarStyle? + + /// A Boolean value indicating whether the navigation bar for any view controllers + /// presented modally by the SDK should be translucent. The default value is YES. + @objc public var translucentNavigationBar = true + + /// This font is automatically derived from the font, with a slightly lower point size, and will be used for supplementary labels. + @objc public var smallFont: UIFont { + return font.withSize(max(font.pointSize - 2, 1)) + } + + /// This font is automatically derived from the font, with a larger point size, and will be used for large labels such as SMS code entry. + @objc public var largeFont: UIFont { + return font.withSize(font.pointSize + 15) + } + + private func barStyle(for color: UIColor) -> UIBarStyle { + if color.isBright { + return .default + } else { + return .black + } + } +} + +extension STPTheme: NSCopying { + /// :nodoc: + @objc + public func copy(with zone: NSZone? = nil) -> Any { + let otherTheme = STPTheme() + otherTheme.primaryBackgroundColor = primaryBackgroundColor + otherTheme.secondaryBackgroundColor = secondaryBackgroundColor + otherTheme.primaryForegroundColor = primaryForegroundColor + otherTheme.secondaryForegroundColor = secondaryForegroundColor + otherTheme.accentColor = accentColor + otherTheme.errorColor = errorColor + otherTheme.translucentNavigationBar = translucentNavigationBar + otherTheme._font = _font + otherTheme._emphasisFont = _emphasisFont + otherTheme._barStyle = _barStyle + + return otherTheme + } +} + +private typealias STPColorBlock = () -> UIColor + +// MARK: Default Colors + +private var STPThemeDefaultPrimaryBackgroundColor: UIColor = .secondarySystemBackground + +private var STPThemeDefaultSecondaryBackgroundColor: UIColor = .systemBackground + +private var STPThemeDefaultPrimaryForegroundColor: UIColor = .label + +private var STPThemeDefaultSecondaryForegroundColor: UIColor = .secondaryLabel + +private var STPThemeDefaultAccentColor: UIColor = .systemBlue + +private var STPThemeDefaultErrorColor: UIColor = .systemRed + +// MARK: Default Fonts +private let STPThemeDefaultFont = UIFont.systemFont(ofSize: 17) +private let STPThemeDefaultMediumFont = UIFont.systemFont(ofSize: 17, weight: .medium) diff --git a/Stripe/StripeiOS/Source/STPUserInformation.swift b/Stripe/StripeiOS/Source/STPUserInformation.swift new file mode 100644 index 00000000..5e6eb2cd --- /dev/null +++ b/Stripe/StripeiOS/Source/STPUserInformation.swift @@ -0,0 +1,42 @@ +// +// STPUserInformation.swift +// StripeiOS +// +// Created by Jack Flintermann on 6/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Foundation + +/// You can use this class to specify information that you've already collected +/// from your user. You can then set the `prefilledInformation` property on +/// `STPPaymentContext`, `STPAddCardViewController`, etc and it will pre-fill +/// this information whenever possible. +public class STPUserInformation: NSObject, NSCopying { + /// The user's billing address. When set, the add card form will be filled with + /// this address. The user will also have the option to fill their shipping address + /// using this address. + /// @note Set this using `setBillingAddressWithBillingDetails:` to use the billing + /// details from an `STPPaymentMethod` or `STPPaymentMethodParams` instance. + @objc public var billingAddress: STPAddress? + /// The user's shipping address. When set, the shipping address form will be filled + /// with this address. The user will also have the option to fill their billing + /// address using this address. + @objc public var shippingAddress: STPAddress? + + /// A convenience method to populate `billingAddress` with a PaymentMethod's billing details. + /// @note Calling this overwrites the value of `billingAddress`. + @objc(setBillingAddressWithBillingDetails:) + public func setBillingAddress(with billingDetails: STPPaymentMethodBillingDetails) { + billingAddress = STPAddress(paymentMethodBillingDetails: billingDetails) + } + + /// :nodoc: + @objc + public func copy(with zone: NSZone? = nil) -> Any { + let copy = STPUserInformation() + copy.billingAddress = billingAddress + copy.shippingAddress = shippingAddress + return copy + } +} diff --git a/Stripe/StripeiOS/Source/String+Localized.swift b/Stripe/StripeiOS/Source/String+Localized.swift new file mode 100644 index 00000000..55bd59dd --- /dev/null +++ b/Stripe/StripeiOS/Source/String+Localized.swift @@ -0,0 +1,22 @@ +// +// String+Localized.swift +// StripeiOS +// +// Created by Mel Ludowise on 7/6/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore + +// MARK: - Legacy strings + +/// Legacy strings +extension StripeSharedStrings { + static func localizedPostalCodeString(for countryCode: String?) -> String { + return countryCode == "US" + ? String.Localized.zip : String.Localized.postal_code + } +} diff --git a/Stripe/StripeiOS/Source/Stripe+Exports.swift b/Stripe/StripeiOS/Source/Stripe+Exports.swift new file mode 100644 index 00000000..16160f1e --- /dev/null +++ b/Stripe/StripeiOS/Source/Stripe+Exports.swift @@ -0,0 +1,13 @@ +// +// Stripe+Exports.swift +// StripeiOS +// +// This file re-exports classes from Stripe's dependent SDKs, enabling users to use classes from all +// Stripe Payments SDKs with one `import Stripe` declaration. +// + +import Foundation +@_exported import StripeApplePay +@_exported import StripeCore +@_exported import StripePayments +@_exported import StripePaymentsUI diff --git a/Stripe/StripeiOS/Source/StripeBundleLocator.swift b/Stripe/StripeiOS/Source/StripeBundleLocator.swift new file mode 100644 index 00000000..a6c6694f --- /dev/null +++ b/Stripe/StripeiOS/Source/StripeBundleLocator.swift @@ -0,0 +1,20 @@ +// +// StripeBundleLocator.swift +// StripeiOS +// +// Created by Mel Ludowise on 7/6/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +/// :nodoc: +@_spi(STP) public final class StripeBundleLocator: BundleLocatorProtocol { + public static let internalClass: AnyClass = StripeBundleLocator.self + public static let bundleName = "StripeBundle" + #if SWIFT_PACKAGE + public static let spmResourcesBundle = Bundle.module + #endif + public static let resourcesBundle = StripeBundleLocator.computeResourcesBundle() +} diff --git a/Stripe/StripeiOS/Source/UIBarButtonItem+Stripe.swift b/Stripe/StripeiOS/Source/UIBarButtonItem+Stripe.swift new file mode 100644 index 00000000..2c792c1b --- /dev/null +++ b/Stripe/StripeiOS/Source/UIBarButtonItem+Stripe.swift @@ -0,0 +1,54 @@ +// +// UIBarButtonItem+Stripe.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeUICore +import UIKit + +extension UIBarButtonItem { + @objc(stp_setTheme:) func stp_setTheme(_ theme: STPTheme) { + let image = backgroundImage(for: .normal, barMetrics: .default) + if let image = image { + let enabledImage: UIImage = STPLegacyImageLibrary.image( + withTintColor: theme.accentColor, + for: image + ) + let disabledImage: UIImage = STPLegacyImageLibrary.image( + withTintColor: theme.secondaryForegroundColor, + for: image + ) + setBackgroundImage(enabledImage, for: .normal, barMetrics: .default) + setBackgroundImage(disabledImage, for: .disabled, barMetrics: .default) + } + + tintColor = isEnabled ? theme.accentColor : theme.secondaryForegroundColor + + setTitleTextAttributes( + [ + NSAttributedString.Key.font: style == .plain ? theme.font : theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ], + for: .normal + ) + + setTitleTextAttributes( + [ + NSAttributedString.Key.font: style == .plain ? theme.font : theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.secondaryForegroundColor, + ], + for: .disabled + ) + + setTitleTextAttributes( + [ + NSAttributedString.Key.font: style == .plain ? theme.font : theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ], + for: .highlighted + ) + } +} diff --git a/Stripe/StripeiOS/Source/UINavigationBar+Stripe_Theme.swift b/Stripe/StripeiOS/Source/UINavigationBar+Stripe_Theme.swift new file mode 100644 index 00000000..d46ee524 --- /dev/null +++ b/Stripe/StripeiOS/Source/UINavigationBar+Stripe_Theme.swift @@ -0,0 +1,142 @@ +// +// UINavigationBar+Stripe_Theme.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/17/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import ObjectiveC +import UIKit + +/// This allows quickly setting the appearance of a `UINavigationBar` to match your +/// application. This is useful if you're presenting an `STPAddCardViewController` +/// or `STPPaymentOptionsViewController` inside a `UINavigationController`. +extension UINavigationBar { + /// Sets the navigation bar's appearance to the desired theme. This will affect the + /// bar's `tintColor` and `barTintColor` properties, as well as the color of the + /// single-pixel line at the bottom of the navbar. + /// - Parameter theme: the theme to use to style the navigation bar. - seealso: STPTheme.h + /// @deprecated Use the `stp_theme` property instead + @available(*, deprecated, message: "Use the `stp_theme` property instead") + @objc + public func stp_setTheme(_ theme: STPTheme) { + stp_theme = theme + } + + /// Sets the navigation bar's appearance to the desired theme. This will affect the bar's `tintColor` and `barTintColor` properties, as well as the color of the single-pixel line at the bottom of the navbar. + /// Stripe view controllers will use their navigation bar's theme for their UIBarButtonItems instead of their own theme if it is not nil. + /// - seealso: STPTheme.h + + @objc public var stp_theme: STPTheme? { + get { + return objc_getAssociatedObject( + self, + UnsafeRawPointer(&kUINavigationBarSTPThemeObjectKey) + ) + as? STPTheme + } + set(theme) { + objc_setAssociatedObject( + self, + UnsafeRawPointer(&kUINavigationBarSTPThemeObjectKey), + theme, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + + if let hairlineImageView = stp_hairlineImageView() { + hairlineImageView.isHidden = theme != nil + } + + guard let theme = theme else { + return + } + + stp_artificialHairlineView().backgroundColor = theme.tertiaryBackgroundColor + barTintColor = theme.secondaryBackgroundColor + tintColor = theme.accentColor + barStyle = theme.barStyle + isTranslucent = theme.translucentNavigationBar + + titleTextAttributes = [ + NSAttributedString.Key.font: theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.primaryForegroundColor, + ] + + largeTitleTextAttributes = [ + NSAttributedString.Key.foregroundColor: theme.primaryForegroundColor + ] + + standardAppearance.backgroundColor = theme.secondaryBackgroundColor + if let titleTextAttributes = titleTextAttributes { + standardAppearance.titleTextAttributes = titleTextAttributes + } + if let largeTitleTextAttributes = largeTitleTextAttributes { + standardAppearance.largeTitleTextAttributes = largeTitleTextAttributes + } + standardAppearance.buttonAppearance.normal.titleTextAttributes = [ + NSAttributedString.Key.font: theme.font, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ] + + standardAppearance.buttonAppearance.highlighted.titleTextAttributes = [ + NSAttributedString.Key.font: theme.font, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ] + + standardAppearance.buttonAppearance.disabled.titleTextAttributes = [ + NSAttributedString.Key.font: theme.font, + NSAttributedString.Key.foregroundColor: theme.secondaryForegroundColor, + ] + + standardAppearance.doneButtonAppearance.normal.titleTextAttributes = [ + NSAttributedString.Key.font: theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ] + + standardAppearance.doneButtonAppearance.highlighted.titleTextAttributes = [ + NSAttributedString.Key.font: theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.accentColor, + ] + + standardAppearance.doneButtonAppearance.disabled.titleTextAttributes = [ + NSAttributedString.Key.font: theme.emphasisFont, + NSAttributedString.Key.foregroundColor: theme.secondaryForegroundColor, + ] + scrollEdgeAppearance = standardAppearance + compactAppearance = standardAppearance + } + } + + func stp_artificialHairlineView() -> UIView { + var view = viewWithTag(STPNavigationBarHairlineViewTag) + if view == nil { + view = UIView(frame: CGRect(x: 0, y: bounds.maxY, width: bounds.width, height: 0.5)) + view?.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] + view?.tag = STPNavigationBarHairlineViewTag + if let view = view { + addSubview(view) + } + } + return view! + } + + func stp_hairlineImageView() -> UIImageView? { + return stp_hairlineImageView(self) + } + + func stp_hairlineImageView(_ view: UIView) -> UIImageView? { + if (view is UIImageView) && view.bounds.size.height <= 1.0 { + return (view as? UIImageView)! + } + for subview in view.subviews { + if let imageView = stp_hairlineImageView(subview) { + return imageView + } + } + return nil + } +} + +private let STPNavigationBarHairlineViewTag = 787473 +private var kUINavigationBarSTPThemeObjectKey = 0 diff --git a/Stripe/StripeiOS/Source/UINavigationController+Stripe_Completion.swift b/Stripe/StripeiOS/Source/UINavigationController+Stripe_Completion.swift new file mode 100644 index 00000000..f0ed8296 --- /dev/null +++ b/Stripe/StripeiOS/Source/UINavigationController+Stripe_Completion.swift @@ -0,0 +1,61 @@ +// +// UINavigationController+Stripe_Completion.swift +// StripeiOS +// +// Created by Jack Flintermann on 3/23/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +// See http://stackoverflow.com/questions/9906966/completion-handler-for-uinavigationcontroller-pushviewcontrolleranimated/33767837#33767837 for some discussion around why using CATransaction is unreliable here. + +extension UINavigationController { + func stp_push( + _ viewController: UIViewController?, + animated: Bool, + completion: @escaping STPVoidBlock + ) { + if let viewController = viewController { + pushViewController(viewController, animated: animated) + } + if transitionCoordinator != nil && animated { + transitionCoordinator?.animate(alongsideTransition: nil) { _ in + completion() + } + } else { + completion() + } + } + + func stp_popViewController( + animated: Bool, + completion: @escaping STPVoidBlock + ) { + popViewController(animated: animated) + if transitionCoordinator != nil && animated { + transitionCoordinator?.animate(alongsideTransition: nil) { _ in + completion() + } + } else { + completion() + } + } + + @objc(stp_popToViewController:animated:completion:) func stp_pop( + to viewController: UIViewController?, + animated: Bool, + completion: @escaping STPVoidBlock + ) { + if let viewController = viewController { + popToViewController(viewController, animated: animated) + } + if transitionCoordinator != nil && animated { + transitionCoordinator?.animate(alongsideTransition: nil) { _ in + completion() + } + } else { + completion() + } + } +} diff --git a/Stripe/StripeiOS/Source/UITableViewCell+Stripe_Borders.swift b/Stripe/StripeiOS/Source/UITableViewCell+Stripe_Borders.swift new file mode 100644 index 00000000..c70200a0 --- /dev/null +++ b/Stripe/StripeiOS/Source/UITableViewCell+Stripe_Borders.swift @@ -0,0 +1,103 @@ +// +// UITableViewCell+Stripe_Borders.swift +// StripeiOS +// +// Created by Jack Flintermann on 5/16/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +private let STPTableViewCellTopBorderTag = 787473 +private let STPTableViewCellBottomBorderTag = 787474 +private let STPTableViewCellFakeSeparatorTag = 787475 + +extension UITableViewCell { + @objc(stp_setBorderColor:) func stp_setBorderColor(_ color: UIColor?) { + stp_topBorderView()?.backgroundColor = color + stp_bottomBorderView()?.backgroundColor = color + } + + @objc(stp_setTopBorderHidden:) func stp_setTopBorderHidden(_ hidden: Bool) { + stp_topBorderView()?.isHidden = hidden + } + + @objc(stp_setBottomBorderHidden:) func stp_setBottomBorderHidden(_ hidden: Bool) { + stp_bottomBorderView()?.isHidden = hidden + stp_fakeSeparatorView()?.isHidden = !hidden + } + + @objc(stp_setFakeSeparatorLeftInset:) func stp_setFakeSeparatorLeftInset(_ leftInset: CGFloat) { + stp_fakeSeparatorView()?.frame = CGRect( + x: leftInset, + y: bounds.size.height - 0.5, + width: bounds.size.width - leftInset, + height: 0.5 + ) + } + + @objc(stp_setFakeSeparatorColor:) func stp_setFakeSeparatorColor(_ color: UIColor?) { + stp_fakeSeparatorView()?.backgroundColor = color + } + + func stp_topBorderView() -> UIView? { + var view = viewWithTag(STPTableViewCellTopBorderTag) + if view == nil { + view = UIView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: 0.5)) + view?.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin] + view?.tag = STPTableViewCellTopBorderTag + view?.backgroundColor = backgroundColor + view?.isHidden = true + view?.accessibilityIdentifier = "stp_topBorderView" + if let view = view { + addSubview(view) + } + } + return view + } + + func stp_bottomBorderView() -> UIView? { + var view = viewWithTag(STPTableViewCellBottomBorderTag) + if view == nil { + view = UIView( + frame: CGRect( + x: 0, + y: bounds.size.height - 0.5, + width: bounds.size.width, + height: 0.5 + ) + ) + view?.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] + view?.tag = STPTableViewCellBottomBorderTag + view?.backgroundColor = backgroundColor + view?.isHidden = true + view?.accessibilityIdentifier = "stp_bottomBorderView" + if let view = view { + addSubview(view) + } + } + return view + } + + func stp_fakeSeparatorView() -> UIView? { + var view = viewWithTag(STPTableViewCellFakeSeparatorTag) + if view == nil { + view = UIView( + frame: CGRect( + x: 0, + y: bounds.size.height - 0.5, + width: bounds.size.width, + height: 0.5 + ) + ) + view?.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] + view?.tag = STPTableViewCellFakeSeparatorTag + view?.backgroundColor = backgroundColor + view?.accessibilityIdentifier = "stp_fakeSeparatorView" + if let view = view { + addSubview(view) + } + } + return view + } +} diff --git a/Stripe/StripeiOS/Source/UIToolbar+Stripe_InputAccessory.swift b/Stripe/StripeiOS/Source/UIToolbar+Stripe_InputAccessory.swift new file mode 100644 index 00000000..4194764e --- /dev/null +++ b/Stripe/StripeiOS/Source/UIToolbar+Stripe_InputAccessory.swift @@ -0,0 +1,38 @@ +// +// UIToolbar+Stripe_InputAccessory.swift +// StripeiOS +// +// Created by Jack Flintermann on 4/22/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +extension UIToolbar { + @objc(stp_inputAccessoryToolbarWithTarget:action:) class func stp_inputAccessoryToolbar( + withTarget target: Any?, + action: Selector + ) -> Self { + let toolbar = self.init() + let flexibleItem = UIBarButtonItem( + barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil + ) + let nextItem = UIBarButtonItem( + title: STPLocalizedString("Next", "Button to move to the next text entry field"), + style: .done, + target: target, + action: action + ) + toolbar.items = [flexibleItem, nextItem] + toolbar.autoresizingMask = .flexibleHeight + return toolbar + } + + @objc(stp_setEnabled:) func stp_setEnabled(_ enabled: Bool) { + for barButtonItem in items ?? [] { + barButtonItem.isEnabled = enabled + } + } +} diff --git a/Stripe/StripeiOS/Source/UIView+Stripe_FirstResponder.swift b/Stripe/StripeiOS/Source/UIView+Stripe_FirstResponder.swift new file mode 100644 index 00000000..40267b0a --- /dev/null +++ b/Stripe/StripeiOS/Source/UIView+Stripe_FirstResponder.swift @@ -0,0 +1,24 @@ +// +// UIView+Stripe_FirstResponder.swift +// StripeiOS +// +// Created by Jack Flintermann on 4/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +extension UIView { + @objc func stp_findFirstResponder() -> UIView? { + if isFirstResponder { + return self + } + for subView in subviews { + let responder = subView.stp_findFirstResponder() + if let responder = responder { + return responder + } + } + return nil + } +} diff --git a/Stripe/StripeiOS/Source/UIView+Stripe_SafeAreaBounds.swift b/Stripe/StripeiOS/Source/UIView+Stripe_SafeAreaBounds.swift new file mode 100644 index 00000000..d29b49b8 --- /dev/null +++ b/Stripe/StripeiOS/Source/UIView+Stripe_SafeAreaBounds.swift @@ -0,0 +1,24 @@ +// +// UIView+Stripe_SafeAreaBounds.swift +// StripeiOS +// +// Created by Ben Guo on 12/12/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import UIKit + +extension UIView { + /// Returns this view's bounds inset to `safeAreaInsets.left` and `safeAreaInsets.right`. + /// Top and bottom safe area insets are ignored. On iOS <11, this returns self.bounds. + @objc func stp_boundsWithHorizontalSafeAreaInsets() -> CGRect { + let insets = safeAreaInsets + let safeBounds = CGRect( + x: bounds.origin.x + insets.left, + y: bounds.origin.y, + width: bounds.size.width - (insets.left + insets.right), + height: bounds.size.height + ) + return safeBounds + } +} diff --git a/Stripe/StripeiOS/Source/UIViewController+Stripe_KeyboardAvoiding.swift b/Stripe/StripeiOS/Source/UIViewController+Stripe_KeyboardAvoiding.swift new file mode 100644 index 00000000..ce6408dc --- /dev/null +++ b/Stripe/StripeiOS/Source/UIViewController+Stripe_KeyboardAvoiding.swift @@ -0,0 +1,166 @@ +// +// UIViewController+Stripe_KeyboardAvoiding.swift +// StripeiOS +// +// Created by Jack Flintermann on 4/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +typealias STPKeyboardFrameBlock = (CGRect, UIView?) -> Void +extension UIViewController { + @objc(stp_beginObservingKeyboardAndInsettingScrollView:onChangeBlock:) + func stp_beginObservingKeyboardAndInsettingScrollView( + _ scrollView: UIScrollView?, + onChange block: STPKeyboardFrameBlock? + ) { + if let existing = stp_keyboardDetectingViewController() { + existing.removeFromParent() + existing.view.removeFromSuperview() + existing.didMove(toParent: nil) + } + let keyboardAvoiding = STPKeyboardDetectingViewController( + keyboardFrameBlock: block, + scrollView: scrollView + ) + addChild(keyboardAvoiding) + view.addSubview(keyboardAvoiding.view) + keyboardAvoiding.didMove(toParent: self) + } + + @objc func stp_keyboardDetectingViewController() -> STPKeyboardDetectingViewController? { + return + (children as NSArray).filtered( + using: NSPredicate(block: { viewController, _ in + return viewController is STPKeyboardDetectingViewController + }) + ).first as? STPKeyboardDetectingViewController + } +} + +// This is a private class that is only a UIViewController subclass by virtue of the fact +// that that makes it easier to attach to another UIViewController as a child. +class STPKeyboardDetectingViewController: UIViewController { + var lastKeyboardFrame = CGRect.zero + weak var lastResponder: UIView? + var keyboardFrameBlock: STPKeyboardFrameBlock? + weak var managedScrollView: UIScrollView? + var currentBottomInsetChange: CGFloat = 0.0 + + init( + keyboardFrameBlock block: STPKeyboardFrameBlock?, + scrollView: UIScrollView? + ) { + keyboardFrameBlock = block + super.init(nibName: nil, bundle: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(textFieldWillBeginEditing(_:)), + name: UITextField.textDidBeginEditingNotification, + object: nil + ) + managedScrollView = scrollView + currentBottomInsetChange = 0 + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func loadView() { + let view = UIView() + view.backgroundColor = UIColor.clear + view.autoresizingMask = [] + self.view = view + } + + @objc func textFieldWillBeginEditing(_ notification: Notification) { + guard let textField = notification.object as? UITextField, let parentView = parent?.view, + textField.isDescendant(of: parentView) + else { + return + } + if let keyboardFrameBlock = keyboardFrameBlock, + textField != lastResponder && !lastKeyboardFrame.isEmpty + { + UIView.animate( + withDuration: 0.3, + delay: 0, + options: .curveEaseOut, + animations: { + keyboardFrameBlock(self.lastKeyboardFrame, textField) + } + ) + } + } + + @objc func keyboardWillChangeFrame(_ notification: Notification) { + // As of iOS 8, this all takes place inside the necessary animation block + // https://twitter.com/SmileyKeith/status/684100833823174656 + guard + var keyboardFrame = + (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)? + .cgRectValue, + let window = view.window + else { + return + } + keyboardFrame = window.convert(keyboardFrame, from: nil) + + if managedScrollView != nil { + if !lastKeyboardFrame.equalTo(keyboardFrame) { + let responder = parent?.view.stp_findFirstResponder() + lastResponder = responder + doKeyboardChangeAnimation(withNewFrame: keyboardFrame) + } + } + } + + func doKeyboardChangeAnimation(withNewFrame keyboardFrame: CGRect) { + lastKeyboardFrame = keyboardFrame + + if let managedScrollView = managedScrollView { + let scrollView = managedScrollView + let scrollViewSuperView = managedScrollView.superview + + var contentInsets = scrollView.contentInset + var scrollIndicatorInsets: UIEdgeInsets = .zero + #if !TARGET_OS_MACCATALYST + scrollIndicatorInsets = scrollView.verticalScrollIndicatorInsets + #else + scrollIndicatorInsets = scrollView.scrollIndicatorInsets + #endif + + let windowFrame = scrollViewSuperView?.convert( + scrollViewSuperView?.frame ?? CGRect.zero, + to: nil + ) + + let bottomIntersection = windowFrame?.intersection(keyboardFrame) + let bottomInsetDelta = + (bottomIntersection?.size.height ?? 0.0) - currentBottomInsetChange + contentInsets.bottom += bottomInsetDelta + scrollIndicatorInsets.bottom += bottomInsetDelta + currentBottomInsetChange += bottomInsetDelta + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = scrollIndicatorInsets + } + + if let keyboardFrameBlock = keyboardFrameBlock { + keyboardFrameBlock(keyboardFrame, lastResponder) + } + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } +} diff --git a/Stripe/StripeiOS/Source/UIViewController+Stripe_NavigationItemProxy.swift b/Stripe/StripeiOS/Source/UIViewController+Stripe_NavigationItemProxy.swift new file mode 100644 index 00000000..0b5fd9ee --- /dev/null +++ b/Stripe/StripeiOS/Source/UIViewController+Stripe_NavigationItemProxy.swift @@ -0,0 +1,38 @@ +// +// UIViewController+Stripe_NavigationItemProxy.swift +// StripeiOS +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import ObjectiveC +import UIKit + +extension UIViewController { + @objc var stp_navigationItemProxy: UINavigationItem? { + get { + return objc_getAssociatedObject(self, UnsafeRawPointer(&kSTPNavigationItemProxyKey)) + as? UINavigationItem ?? self.navigationItem + } + set(stp_navigationItemProxy) { + objc_setAssociatedObject( + self, + UnsafeRawPointer(&kSTPNavigationItemProxyKey), + stp_navigationItemProxy, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + if navigationItem.leftBarButtonItem != nil { + stp_navigationItemProxy?.leftBarButtonItem = navigationItem.leftBarButtonItem + } + if navigationItem.rightBarButtonItem != nil { + stp_navigationItemProxy?.rightBarButtonItem = navigationItem.rightBarButtonItem + } + if navigationItem.title != nil { + stp_navigationItemProxy?.title = navigationItem.title + } + } + } +} + +private var kSTPNavigationItemProxyKey = 0 diff --git a/Stripe/StripeiOS/Source/UIViewController+Stripe_ParentViewController.swift b/Stripe/StripeiOS/Source/UIViewController+Stripe_ParentViewController.swift new file mode 100644 index 00000000..534172cb --- /dev/null +++ b/Stripe/StripeiOS/Source/UIViewController+Stripe_ParentViewController.swift @@ -0,0 +1,50 @@ +// +// UIViewController+Stripe_ParentViewController.swift +// StripeiOS +// +// Created by Jack Flintermann on 1/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import UIKit + +extension UIViewController { + @objc(stp_parentViewControllerOfClass:) func stp_parentViewControllerOf( + _ klass: AnyClass + ) + -> UIViewController? + { + if let parent = parent, parent.isKind(of: klass) { + return parent + } + return parent?.stp_parentViewControllerOf(klass) + } + + @objc func stp_isTopNavigationController() -> Bool { + return navigationController?.topViewController == self + } + + @objc func stp_isAtRootOfNavigationController() -> Bool { + let viewController = navigationController?.viewControllers.first + var tested: UIViewController? = self + while tested != nil { + if tested == viewController { + return true + } + if let parent = tested?.parent { + tested = parent + } else { + return false + } + } + return false + } + + @objc func stp_previousViewControllerInNavigation() -> UIViewController? { + let index = navigationController?.viewControllers.firstIndex(of: self) ?? NSNotFound + if index == NSNotFound || index <= 0 { + return nil + } + return navigationController?.viewControllers[index - 1] + } +} diff --git a/Stripe/StripeiOS/Source/UserDefaults+Stripe.swift b/Stripe/StripeiOS/Source/UserDefaults+Stripe.swift new file mode 100644 index 00000000..72ee855f --- /dev/null +++ b/Stripe/StripeiOS/Source/UserDefaults+Stripe.swift @@ -0,0 +1,29 @@ +// +// UserDefaults+Stripe.swift +// StripeiOS +// +// Created by Yuki Tokuhiro on 5/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation + +extension UserDefaults { + /// Canonical list of all UserDefaults keys the SDK uses + @_spi(STP) public enum StripeKeys: String { + /// The key for a dictionary of Customer id to their last selected payment method ID + case customerToLastSelectedPaymentMethod = "com.stripe.lib:STPStripeCustomerToLastSelectedPaymentMethodKey" + } + + @_spi(STP) public var customerToLastSelectedPaymentMethod: [String: String]? { + get { + let key = StripeKeys.customerToLastSelectedPaymentMethod.rawValue + return dictionary(forKey: key) as? [String: String] + } + set { + let key = StripeKeys.customerToLastSelectedPaymentMethod.rawValue + setValue(newValue, forKey: key) + } + } + +} diff --git a/Stripe/StripeiOS/Stripe-umbrella.h b/Stripe/StripeiOS/Stripe-umbrella.h new file mode 100644 index 00000000..b062ed0c --- /dev/null +++ b/Stripe/StripeiOS/Stripe-umbrella.h @@ -0,0 +1,11 @@ +// +// Stripe-umbrella.h +// StripeiOS +// +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +#ifndef Stripe_umbrella_h +#define Stripe_umbrella_h + +#endif /* Stripe_umbrella_h */ diff --git a/Stripe/StripeiOS/Stripe.modulemap b/Stripe/StripeiOS/Stripe.modulemap new file mode 100644 index 00000000..35dad19e --- /dev/null +++ b/Stripe/StripeiOS/Stripe.modulemap @@ -0,0 +1,6 @@ +framework module Stripe { + umbrella header "Stripe-umbrella.h" + + export * + module * { export * } +} diff --git a/Stripe/StripeiOSAppHostedTests/Info.plist b/Stripe/StripeiOSAppHostedTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/Stripe/StripeiOSAppHostedTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Stripe/StripeiOSTestHostApp/AppDelegate.swift b/Stripe/StripeiOSTestHostApp/AppDelegate.swift new file mode 100644 index 00000000..729e9852 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/AppDelegate.swift @@ -0,0 +1,18 @@ +// +// AppDelegate.swift +// StripeiOSTestHostApp +// +// Created by Cameron Sabol on 11/4/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } +} diff --git a/Stripe/StripeiOSTestHostApp/Info.plist b/Stripe/StripeiOSTestHostApp/Info.plist new file mode 100644 index 00000000..5b531f7b --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/Contents.json b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/LaunchScreen.storyboard b/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/Main.storyboard b/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe/StripeiOSTestHostApp/ViewController.swift b/Stripe/StripeiOSTestHostApp/ViewController.swift new file mode 100644 index 00000000..a8daad08 --- /dev/null +++ b/Stripe/StripeiOSTestHostApp/ViewController.swift @@ -0,0 +1,18 @@ +// +// ViewController.swift +// StripeiOSTestHostApp +// +// Created by Cameron Sabol on 11/4/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + +} diff --git a/Stripe/StripeiOSTests.xctestplan b/Stripe/StripeiOSTests.xctestplan new file mode 100644 index 00000000..13c50bee --- /dev/null +++ b/Stripe/StripeiOSTests.xctestplan @@ -0,0 +1,45 @@ +{ + "configurations" : [ + { + "id" : "E4E61B3B-0FEC-4C2A-BE82-E8558946FFBC", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "environmentVariableEntries" : [ + { + "key" : "FB_REFERENCE_IMAGE_DIR", + "value" : "$(SOURCE_ROOT)\/..\/Tests\/ReferenceImages" + } + ], + "locationScenario" : { + "identifier" : "San Francisco, CA, USA", + "referenceType" : "built-in" + }, + "region" : "US", + "targetForVariableExpansion" : { + "containerPath" : "container:Stripe.xcodeproj", + "identifier" : "ADF894AA8F6022D9BED17346", + "name" : "StripeiOS" + } + }, + "testTargets" : [ + { + "skippedTests" : [ + "PaymentMethodMessagingViewSnapshotTests", + "STPCardBINMetadataTests", + "TextFieldElementCardTest\/testBINRangeThatRequiresNetworkCallToValidate()" + ], + "target" : { + "containerPath" : "container:Stripe.xcodeproj", + "identifier" : "8BE23AD5D9A3D939AF46F31E", + "name" : "StripeiOSTests" + } + } + ], + "version" : 1 +} diff --git a/Stripe/StripeiOSTests/APIRequestTest.swift b/Stripe/StripeiOSTests/APIRequestTest.swift new file mode 100644 index 00000000..2a097d20 --- /dev/null +++ b/Stripe/StripeiOSTests/APIRequestTest.swift @@ -0,0 +1,294 @@ +// +// APIRequestTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 9/23/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class AnyAPIResponse: NSObject, STPAPIResponseDecodable { + override required init() { + super.init() + } + + static func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard let response = response else { + return nil + } + let a = Self() + a.allResponseFields = response + return a + } + + var allResponseFields: [AnyHashable: Any] = [:] + +} + +class APIRequestTest: STPNetworkStubbingTestCase { + var apiClient: STPAPIClient! + override func setUp() { + super.setUp() + apiClient = STPAPIClient() + // HTTPBin clone + apiClient.apiURL = URL(string: "https://luxurious-alpine-devourer.glitch.me") + } + + func testPublishableKeyAuthorization() { + let e = expectation(description: "Request completed") + apiClient.publishableKey = "pk_foo" + APIRequest.getWith( + apiClient, + endpoint: "bearer", + parameters: ["foo": "bar"] + ) { + (obj, _, error) in + guard let obj = obj, + let token = obj.allResponseFields["token"] as? String + else { + XCTFail() + XCTAssertNil(error) + return + } + XCTAssertEqual(token, self.apiClient.publishableKey) + e.fulfill() + } + waitForExpectations(timeout: 2) + } + + func testStripeAccountAuthorization() { + + } + + func testGet() { + let e = expectation(description: "Request completed") + APIRequest.getWith(apiClient, endpoint: "get", parameters: [:]) { + (obj, response, error) in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 200) + XCTAssertNotNil(obj) + e.fulfill() + } + waitForExpectations(timeout: 2) + } + + func testPost() { + let parameters = ["foo": "bar"] + let e = expectation(description: "Request completed") + APIRequest.post( + with: apiClient, + endpoint: "post", + parameters: ["foo": "bar"] + ) { + (obj, response, error) in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 200) + guard let obj = obj, + let form = obj.allResponseFields["form"] as? [String: String], + let headers = obj.allResponseFields["headers"] as? [String: String] + else { + XCTFail() + return + } + XCTAssertNotNil(headers["X-Stripe-User-Agent"]) + XCTAssertEqual(headers["Stripe-Version"], STPAPIClient.apiVersion) + XCTAssertEqual(form, parameters) + e.fulfill() + } + waitForExpectations(timeout: 2) + } + + func testDelete() { + let e = expectation(description: "Request completed") + APIRequest.delete(with: apiClient, endpoint: "delete", parameters: [:]) { + (obj, response, error) in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 200) + guard let obj = obj, + let headers = obj.allResponseFields["headers"] as? [String: String] + else { + XCTFail() + return + } + XCTAssertNotNil(headers["X-Stripe-User-Agent"]) + XCTAssertEqual(headers["Stripe-Version"], STPAPIClient.apiVersion) + + e.fulfill() + } + waitForExpectations(timeout: 2) + } + + func testParseResponseWithConnectionError() { + let expectation = self.expectation(description: "parseResponse") + + let httpURLResponse = HTTPURLResponse() + let json: [AnyHashable: Any] = [:] + let body = try? JSONSerialization.data(withJSONObject: json, options: []) + let errorParameter = NSError( + domain: NSURLErrorDomain, + code: NSURLErrorNotConnectedToInternet, + userInfo: nil + ) + + APIRequest.parseResponse( + httpURLResponse, + body: body, + error: errorParameter + ) { (object: STPCard?, response, error) in + guard let error = error else { + XCTFail() + return + } + XCTAssertNil(object) + XCTAssertEqual(response, httpURLResponse) + XCTAssertEqual(error as NSError, errorParameter) + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func testParseResponseWithReturnedError() { + let expectation = self.expectation(description: "parseResponse") + + let httpURLResponse = HTTPURLResponse() + let json = [ + "error": [ + "type": "invalid_request_error", + "message": "Your card number is incorrect.", + "code": "incorrect_number", + ], + ] + let body = try? JSONSerialization.data(withJSONObject: json, options: []) + let errorParameter: NSError? = nil + let expectedError = NSError.stp_error(fromStripeResponse: json) + + APIRequest.parseResponse( + httpURLResponse, + body: body, + error: errorParameter + ) { (object: STPCard?, response, error) in + guard let error = error, let expectedError = expectedError else { + XCTFail() + return + } + XCTAssertNil(object) + XCTAssertEqual(response, httpURLResponse) + XCTAssertEqual(error as NSError, expectedError as NSError) + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func testParseResponseWithMissingError() { + let expectation = self.expectation(description: "parseResponse") + + let httpURLResponse = HTTPURLResponse() + let json: [AnyHashable: Any] = [:] + let body = try? JSONSerialization.data(withJSONObject: json, options: []) + let errorParameter: NSError? = nil + let expectedError = NSError.stp_genericFailedToParseResponseError() + + APIRequest.parseResponse( + httpURLResponse, + body: body, + error: errorParameter + ) { (object: STPCard?, response, error) in + guard let error = error else { + XCTFail() + return + } + XCTAssertNil(object) + XCTAssertEqual(response, httpURLResponse) + XCTAssertEqual(error as NSError, expectedError as NSError) + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func testParseResponseWithResponseObjectAndReturnedError() { + let expectation = self.expectation(description: "parseResponse") + + let httpURLResponse = HTTPURLResponse() + let json: [AnyHashable: Any] = STPTestUtils.jsonNamed("CardSource")! + let body = try? JSONSerialization.data(withJSONObject: json, options: []) + let errorParameter: NSError? = NSError( + domain: NSURLErrorDomain, + code: NSURLErrorUnknown, + userInfo: nil + ) + + APIRequest.parseResponse( + httpURLResponse, + body: body, + error: errorParameter + ) { (object: STPCard?, response, error) in + guard let error = error else { + XCTFail() + return + } + XCTAssertNil(object) + XCTAssertEqual(response, httpURLResponse) + XCTAssertEqual(error as NSError, errorParameter) + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0, handler: nil) + } + + func test429Backoff() { + let oldMaxRetries = StripeAPI.maxRetries + StripeAPI.maxRetries = 2 + var inProgress = true + + let e = expectation(description: "Request completed") + // We expect this request to retry a few times with exponential backoff before calling the completion handler. + APIRequest.getWith(apiClient, endpoint: "status/429", parameters: [:]) { + (_, response, _) in + XCTAssertEqual(response?.statusCode, 429) + inProgress = false + e.fulfill() + } + + let checkedStillInProgress = expectation( + description: "Checked that we're still in progress after 2s" + ) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) { + // Make sure we're still in progress after 2 seconds + // This shows that we're retrying the request a few times + // while applying an appropriate amount of backoff. + XCTAssertEqual(inProgress, true) + checkedStillInProgress.fulfill() + } + + wait(for: [e, checkedStillInProgress], timeout: 30) + StripeAPI.maxRetries = oldMaxRetries + } + + func test429NoBackoff() { + let oldMaxRetries = StripeAPI.maxRetries + StripeAPI.maxRetries = 0 + + let e = expectation(description: "Request completed") + APIRequest.getWith(apiClient, endpoint: "status/429", parameters: [:]) { + (_, response, _) in + XCTAssertEqual(response?.statusCode, 429) + e.fulfill() + } + + // We expect this request to return ~immediately, so we set a timeout lower than the highest + // amount of backoff. + wait(for: [e], timeout: 5.0) + StripeAPI.maxRetries = oldMaxRetries + } +} diff --git a/Stripe/StripeiOSTests/AfterpayPriceBreakdownViewSnapshotTests.swift b/Stripe/StripeiOSTests/AfterpayPriceBreakdownViewSnapshotTests.swift new file mode 100644 index 00000000..4fa28519 --- /dev/null +++ b/Stripe/StripeiOSTests/AfterpayPriceBreakdownViewSnapshotTests.swift @@ -0,0 +1,52 @@ +// +// AfterpayPriceBreakdownViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Jaime Park on 6/15/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class AfterpayPriceBreakdownViewSnapshotTests: STPSnapshotTestCase { + func embedInRenderableView( + _ priceBreakdownView: AfterpayPriceBreakdownView, + width: Int, + height: Int + ) -> UIView { + let containingView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: height)) + containingView.addSubview(priceBreakdownView) + priceBreakdownView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + priceBreakdownView.leadingAnchor.constraint(equalTo: containingView.leadingAnchor), + containingView.trailingAnchor.constraint(equalTo: priceBreakdownView.trailingAnchor), + priceBreakdownView.topAnchor.constraint(equalTo: containingView.topAnchor), + containingView.bottomAnchor.constraint(equalTo: priceBreakdownView.bottomAnchor), + ]) + + return containingView + } + + func testClearpayInMultiRow() { + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_GB")) { [self] in + let priceBreakdownView = AfterpayPriceBreakdownView(amount: 1000, currency: "GBP") + let containingView = embedInRenderableView(priceBreakdownView, width: 320, height: 50) + + STPSnapshotVerifyView(containingView) + } + } + + func testAfterpayInSingleRow() { + let priceBreakdownView = AfterpayPriceBreakdownView(amount: 1000, currency: "USD") + let containingView = embedInRenderableView(priceBreakdownView, width: 500, height: 30) + + STPSnapshotVerifyView(containingView) + } +} diff --git a/Stripe/StripeiOSTests/AnalyticsHelperTests.swift b/Stripe/StripeiOSTests/AnalyticsHelperTests.swift new file mode 100644 index 00000000..e82d89e1 --- /dev/null +++ b/Stripe/StripeiOSTests/AnalyticsHelperTests.swift @@ -0,0 +1,60 @@ +// +// AnalyticsHelperTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 2/22/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class AnalyticsHelperTests: XCTestCase { + + func test_getDuration() { + let (sut, timeReference) = makeSUT() + + sut.startTimeMeasurement(.linkPopup) + + // Advance the clock by 10 seconds. + timeReference.advanceBy(10) + XCTAssertEqual(sut.getDuration(for: .linkPopup), 10) + + // Advance the clock by 5 seconds. + timeReference.advanceBy(5) + XCTAssertEqual(sut.getDuration(for: .linkPopup), 15) + } + + func test_getDuration_returnsNilWhenNotStarted() { + let (sut, _) = makeSUT() + XCTAssertNil(sut.getDuration(for: .linkPopup)) + } + +} + +extension AnalyticsHelperTests { + + class MockTimeReference { + var date = Date() + + func advanceBy(_ timeInterval: TimeInterval) { + date = date.addingTimeInterval(timeInterval) + } + + func now() -> Date { + return date + } + } + + func makeSUT() -> (AnalyticsHelper, MockTimeReference) { + let timeReference = MockTimeReference() + let helper = AnalyticsHelper(timeProvider: timeReference.now) + return (helper, timeReference) + } + +} diff --git a/Stripe/StripeiOSTests/AutoCompleteViewControllerSnapshotTests.swift b/Stripe/StripeiOSTests/AutoCompleteViewControllerSnapshotTests.swift new file mode 100644 index 00000000..d603df4b --- /dev/null +++ b/Stripe/StripeiOSTests/AutoCompleteViewControllerSnapshotTests.swift @@ -0,0 +1,144 @@ +// +// AutoCompleteViewControllerSnapshotTests.swift +// StripeiOS Tests +// +// Created by Nick Porter on 6/7/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class AutoCompleteViewControllerSnapshotTests: STPSnapshotTestCase { + + private var configuration: AddressViewController.Configuration { + return AddressViewController.Configuration() + } + + private let addressSpecProvider: AddressSpecProvider = { + let specProvider = AddressSpecProvider() + specProvider.addressSpecs = [ + "US": AddressSpec( + format: "ACSZP", + require: "AZ", + cityNameType: .post_town, + stateNameType: .state, + zip: "", + zipNameType: .pin + ), + ] + return specProvider + }() + + private let mockSearchResults: [AddressSearchResult] = [ + MockAddressSearchResult( + title: "199 Water Street", + subtitle: "New York, NY 10038 United States", + titleHighlightRanges: [NSValue(range: NSRange(location: 0, length: 6))], + subtitleHighlightRanges: [NSValue(range: NSRange(location: 2, length: 4))] + ), + MockAddressSearchResult( + title: "354 Oyster Point Blvd", + subtitle: "San Francisco, CA 94080 United States", + titleHighlightRanges: [NSValue(range: NSRange(location: 2, length: 4))], + subtitleHighlightRanges: [NSValue(range: NSRange(location: 4, length: 2))] + ), + MockAddressSearchResult( + title: "10 Boulevard", + subtitle: "Haussmann Paris 75009 France", + titleHighlightRanges: [NSValue(range: NSRange(location: 4, length: 2))], + subtitleHighlightRanges: [NSValue(range: NSRange(location: 0, length: 4))] + ), + ] + + func testAutoCompleteViewController() { + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 428, height: 500)) + testWindow.isHidden = false + let vc = AutoCompleteViewController( + configuration: configuration, + initialLine1Text: nil, + addressSpecProvider: addressSpecProvider + ) + vc.results = mockSearchResults + testWindow.rootViewController = vc + + verify(vc.view) + } + + func testAutoCompleteViewController_darkMode() { + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 428, height: 500)) + testWindow.isHidden = false + testWindow.overrideUserInterfaceStyle = .dark + let vc = AutoCompleteViewController( + configuration: configuration, + initialLine1Text: nil, + addressSpecProvider: addressSpecProvider + ) + + vc.results = mockSearchResults + testWindow.rootViewController = vc + + verify(vc.view) + } + + func testAutoCompleteViewController_appearance() { + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 428, height: 500)) + testWindow.isHidden = false + + var config = configuration + config.appearance.colors.background = .blue + config.appearance.colors.text = .yellow + config.appearance.colors.textSecondary = .red + config.appearance.colors.componentPlaceholderText = .cyan + config.appearance.colors.componentBackground = .red + config.appearance.colors.componentDivider = .green + config.appearance.cornerRadius = 0.0 + config.appearance.borderWidth = 2.0 + config.appearance.font.base = UIFont(name: "AmericanTypeWriter", size: 12)! + config.appearance.colors.primary = .red + + let vc = AutoCompleteViewController( + configuration: config, + initialLine1Text: nil, + addressSpecProvider: addressSpecProvider + ) + vc.results = mockSearchResults + testWindow.rootViewController = vc + + verify(vc.view) + } + + func verify( + _ view: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + STPSnapshotVerifyView( + view, + identifier: identifier, + suffixes: FBSnapshotTestCaseDefaultSuffixes(), + file: file, + line: line + ) + } +} + +private struct MockAddressSearchResult: AddressSearchResult { + let title: String + let subtitle: String + let titleHighlightRanges: [NSValue] + let subtitleHighlightRanges: [NSValue] + + func asAddress(completion: @escaping (PaymentSheet.Address?) -> Void) { + completion(nil) + } +} diff --git a/Stripe/StripeiOSTests/CardExpiryDateTests.swift b/Stripe/StripeiOSTests/CardExpiryDateTests.swift new file mode 100644 index 00000000..8f92f3a2 --- /dev/null +++ b/Stripe/StripeiOSTests/CardExpiryDateTests.swift @@ -0,0 +1,81 @@ +// +// CardExpiryDateTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 4/15/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class CardExpiryDateTests: XCTestCase { + + func test_init() { + let sut = CardExpiryDate(month: 2, year: 2026) + XCTAssertEqual(sut.month, 2) + XCTAssertEqual(sut.year, 2026) + } + + func test_init_shouldNormalizeTheYear() { + let sut = CardExpiryDate(month: 2, year: 50) + XCTAssertEqual(sut.year, 2050) + } + + func test_initFromString() { + let sut = CardExpiryDate("0226") + XCTAssertEqual(sut?.month, 2) + XCTAssertEqual(sut?.year, 2026) + } + + func test_initFromString_withInvalidString() { + XCTAssertNil(CardExpiryDate("")) // empty + XCTAssertNil(CardExpiryDate("0")) // missing 3 digits + XCTAssertNil(CardExpiryDate("023")) // missing a digit + XCTAssertNil(CardExpiryDate("1234567890")) // too many digits + XCTAssertNil(CardExpiryDate("abcd")) // alpha + + // month out of range + XCTAssertNil(CardExpiryDate("1326")) + XCTAssertNil(CardExpiryDate("0026")) + XCTAssertNil(CardExpiryDate("-126")) + + // year out of range + XCTAssertNil(CardExpiryDate("02-1")) + } + + func test_displayString() { + let sut = CardExpiryDate(month: 2, year: 2026) + XCTAssertEqual(sut.displayString, "0226") + } + + func test_expired() throws { + let calendar = Calendar(identifier: .gregorian) + + let sut = CardExpiryDate(month: 2, year: 2026) + + let aDayBefore = try XCTUnwrap(calendar.date(from: .init(year: 2026, month: 2, day: 28))) + let aMonthBefore = try XCTUnwrap(calendar.date(from: .init(year: 2026, month: 1, day: 31))) + let aDayAfter = try XCTUnwrap(calendar.date(from: .init(year: 2026, month: 3, day: 1))) + let aMonthAfter = try XCTUnwrap(calendar.date(from: .init(year: 2026, month: 3, day: 30))) + + XCTAssertFalse(sut.expired(now: aDayBefore)) + XCTAssertFalse(sut.expired(now: aMonthBefore)) + XCTAssertTrue(sut.expired(now: aDayAfter)) + XCTAssertTrue(sut.expired(now: aMonthAfter)) + } + + func test_90s_not_allowed() throws { + let sutPast = CardExpiryDate(month: 2, year: 1995) + let sutFuture = CardExpiryDate(month: 2, year: 2095) + + XCTAssertTrue(sutPast.expired()) + XCTAssertTrue(sutFuture.expired()) + } + +} diff --git a/Stripe/StripeiOSTests/CircularButtonSnapshotTests.swift b/Stripe/StripeiOSTests/CircularButtonSnapshotTests.swift new file mode 100644 index 00000000..814ac8a2 --- /dev/null +++ b/Stripe/StripeiOSTests/CircularButtonSnapshotTests.swift @@ -0,0 +1,62 @@ +// +// CircularButtonSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 1/7/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +@_spi(STP) import StripeUICore + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class CircularButtonSnapshotTests: STPSnapshotTestCase { + + func testNormal() { + let button = CircularButton(style: .close) + verify(button) + } + + func testDisabled() { + let button = CircularButton(style: .close) + button.isEnabled = false + verify(button) + } + + func verify( + _ button: CircularButton, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + // Ensures that the button shadow gets captured + let wrapper = UIView() + wrapper.addAndPinSubview( + button, + insets: .insets(top: 10, leading: 10, bottom: 10, trailing: 10) + ) + + // Adding the view to a window updates the traits + let window = UIWindow() + window.addSubview(wrapper) + + let size = wrapper.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + wrapper.bounds = CGRect(origin: .zero, size: size) + + // Test light mode + wrapper.overrideUserInterfaceStyle = .light + STPSnapshotVerifyView(wrapper, identifier: identifier, file: file, line: line) + + // Test dark mode + wrapper.overrideUserInterfaceStyle = .dark + let updatedIdentifier = (identifier ?? "").appending("darkMode") + STPSnapshotVerifyView(wrapper, identifier: updatedIdentifier, file: file, line: line) + } + +} diff --git a/Stripe/StripeiOSTests/ConfirmButtonSnapshotTests.swift b/Stripe/StripeiOSTests/ConfirmButtonSnapshotTests.swift new file mode 100644 index 00000000..1f3ec317 --- /dev/null +++ b/Stripe/StripeiOSTests/ConfirmButtonSnapshotTests.swift @@ -0,0 +1,113 @@ +// +// ConfirmButtonSnapshotTests.swift +// StripeiOS Tests +// +// Created by Nick Porter on 3/11/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import iOSSnapshotTestCase +import StripeCoreTestUtils +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet + +class ConfirmButtonSnapshotTests: STPSnapshotTestCase { + + func testConfirmButton() { + let confirmButton = ConfirmButton(style: .stripe, callToAction: .setup, didTap: {}) + + verify(confirmButton) + } + + // Tests that `primaryButton` appearance is used over standard variables + func testConfirmButtonBackgroundColor() { + var appearance = PaymentSheet.Appearance.default + var button = PaymentSheet.Appearance.PrimaryButton() + button.backgroundColor = .red + appearance.primaryButton = button + + let confirmButton = ConfirmButton( + style: .stripe, + callToAction: .setup, + appearance: appearance, + didTap: {} + ) + + verify(confirmButton) + } + + func testConfirmButtonCustomFont() throws { + var appearance = PaymentSheet.Appearance.default + appearance.font.base = try XCTUnwrap(UIFont(name: "AmericanTypewriter", size: 12.0)) + + let confirmButton = ConfirmButton( + style: .stripe, + callToAction: .custom(title: "Custom Title"), + appearance: appearance, + didTap: {} + ) + + verify(confirmButton) + } + + func testConfirmButtonCustomFontScales() throws { + var appearance = PaymentSheet.Appearance.default + appearance.font.base = try XCTUnwrap(UIFont(name: "AmericanTypewriter", size: 12.0)) + appearance.font.sizeScaleFactor = 0.85 + + let confirmButton = ConfirmButton( + style: .stripe, + callToAction: .custom(title: "Custom Title"), + appearance: appearance, + didTap: {} + ) + + verify(confirmButton) + } + + // Tests that `primaryButton` success color is correct for the default theme + func testConfirmButtonDefaultSuccessColor() { + let confirmButton = ConfirmButton( + state: .succeeded, + style: .stripe, + callToAction: .setup, + appearance: .default, + didTap: {} + ) + + verify(confirmButton) + } + + // Tests that `primaryButton` success color is updated properly + func testConfirmButtonSuccessColor() { + var appearance = PaymentSheet.Appearance.default + var button = PaymentSheet.Appearance.PrimaryButton() + button.successBackgroundColor = .red + button.successTextColor = .green + appearance.primaryButton = button + + let confirmButton = ConfirmButton( + state: .succeeded, + style: .stripe, + callToAction: .setup, + appearance: appearance, + didTap: {} + ) + + verify(confirmButton) + } + + func verify( + _ view: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + view.autosizeHeight(width: 300) + STPSnapshotVerifyView(view, identifier: identifier, file: file, line: line) + } +} diff --git a/Stripe/StripeiOSTests/ConfirmButtonTests.swift b/Stripe/StripeiOSTests/ConfirmButtonTests.swift new file mode 100644 index 00000000..57b559a9 --- /dev/null +++ b/Stripe/StripeiOSTests/ConfirmButtonTests.swift @@ -0,0 +1,91 @@ +// +// ConfirmButtonTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 10/6/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class ConfirmButtonTests: XCTestCase { + + func testBuyButtonShouldAutomaticallyAdjustItsForegroundColor() { + let testCases: [(background: UIColor, foreground: UIColor)] = [ + // Dark backgrounds + (background: .systemBlue, foreground: .white), + (background: .black, foreground: .white), + // Light backgrounds + ( + background: UIColor(red: 1.0, green: 0.87, blue: 0.98, alpha: 1.0), + foreground: .black + ), + ( + background: UIColor(red: 1.0, green: 0.89, blue: 0.35, alpha: 1.0), + foreground: .black + ), + ] + + for (backgroundColor, expectedForeground) in testCases { + let button = ConfirmButton.BuyButton() + button.tintColor = backgroundColor + button.update( + status: .enabled, + callToAction: .pay(amount: 900, currency: "usd"), + animated: false + ) + + XCTAssertEqual( + // Test against `.cgColor` because any color set as `.backgroundColor` + // will be automatically wrapped in `UIDynamicModifiedColor` (private subclass) by iOS. + button.backgroundColor?.cgColor, + backgroundColor.cgColor + ) + + XCTAssertEqual( + button.foregroundColor, + expectedForeground, + "The foreground color should contrast with the background color" + ) + } + } + + func testUpdateShouldCallTheCompletionBlock() { + let sut = ConfirmButton( + style: .stripe, + callToAction: .pay(amount: 1000, currency: "usd"), + didTap: {} + ) + + let expectation = XCTestExpectation(description: "Should call the completion block") + + sut.update(state: .disabled, animated: false) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testUpdateShouldCallTheCompletionBlockWhenAnimated() { + let sut = ConfirmButton( + style: .stripe, + callToAction: .pay(amount: 1000, currency: "usd"), + didTap: {} + ) + + let expectation = XCTestExpectation(description: "Should call the completion block") + + sut.update(state: .disabled, animated: true) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + } + +} diff --git a/Stripe/StripeiOSTests/ConsumerSessionTests.swift b/Stripe/StripeiOSTests/ConsumerSessionTests.swift new file mode 100644 index 00000000..820e89c4 --- /dev/null +++ b/Stripe/StripeiOSTests/ConsumerSessionTests.swift @@ -0,0 +1,214 @@ +// +// ConsumerSessionTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 4/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class ConsumerSessionTests: STPNetworkStubbingTestCase { + + var apiClient: STPAPIClient! + + override func setUp() { + strictParamsEnforcement = false + super.setUp() + apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + } + + func testLookupSession_noParams() { + let expectation = self.expectation(description: "Lookup ConsumerSession") + + ConsumerSession.lookupSession(for: nil, with: apiClient) { + result in + switch result { + case .success(let lookupResponse): + switch lookupResponse.responseType { + case .found: + XCTFail("Got a response without any params") + + case .notFound(let errorMessage): + XCTFail("Got not found response with \(errorMessage)") + + case .noAvailableLookupParams: + break // Pass + } + case .failure(let error): + XCTFail("Received error: \(error.nonGenericDescription)") + } + + expectation.fulfill() + } + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + } + + func testLookupSession_existingConsumer() { + let expectation = self.expectation(description: "Lookup ConsumerSession") + + ConsumerSession.lookupSession( + for: "mobile-payments-sdk-ci+a-consumer@stripe.com", + with: apiClient + ) { result in + switch result { + case .success(let lookupResponse): + switch lookupResponse.responseType { + case .found: + break // Pass + + case .notFound(let errorMessage): + XCTFail("Got not found response with \(errorMessage)") + + case .noAvailableLookupParams: + XCTFail("Got no available lookup params") + } + case .failure(let error): + XCTFail("Received error: \(error.nonGenericDescription)") + } + + expectation.fulfill() + } + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + } + + func testLookupSession_newConsumer() { + let expectation = self.expectation(description: "Lookup ConsumerSession") + + ConsumerSession.lookupSession( + for: "mobile-payments-sdk-ci+not-a-consumer+\(UUID())@stripe.com", + with: apiClient + ) { result in + switch result { + case .success(let lookupResponse): + switch lookupResponse.responseType { + case .found(let consumerSession): + XCTFail("Got unexpected found response with \(consumerSession)") + + case .notFound: + break // Pass + + case .noAvailableLookupParams: + XCTFail("Got no available lookup params") + } + case .failure(let error): + XCTFail("Received error: \(error.nonGenericDescription)") + } + + expectation.fulfill() + } + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + } + + // tests signup, createPaymentDetails + func testSignUpAndCreateDetailsAndLogout() { + let expectation = self.expectation(description: "consumer sign up") + let newAccountEmail = "mobile-payments-sdk-ci+\(UUID())@stripe.com" + + var sessionWithKey: ConsumerSession.SessionWithPublishableKey? + + ConsumerSession.signUp( + email: newAccountEmail, + phoneNumber: "+13105551234", + legalName: nil, + countryCode: "US", + consentAction: PaymentSheetLinkAccount.ConsentAction.checkbox_v0.rawValue, + with: apiClient + ) { result in + switch result { + case .success(let signupResponse): + XCTAssertTrue(signupResponse.consumerSession.isVerifiedForSignup) + XCTAssertTrue( + signupResponse.consumerSession.verificationSessions.isVerifiedForSignup + ) + XCTAssertTrue( + signupResponse.consumerSession.verificationSessions.contains(where: { + $0.type == .signup + }) + ) + + sessionWithKey = signupResponse + case .failure(let error): + XCTFail("Received error: \(error.nonGenericDescription)") + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + + let consumerSession = sessionWithKey!.consumerSession + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4242424242424242" + cardParams.expMonth = 12 + cardParams.expYear = NSNumber( + value: Calendar.autoupdatingCurrent.component(.year, from: Date()) + 1 + ) + cardParams.cvc = "123" + + let billingParams = STPPaymentMethodBillingDetails() + billingParams.name = "Payments SDK CI" + let address = STPPaymentMethodAddress() + address.postalCode = "55555" + address.country = "US" + billingParams.address = address + + let paymentMethodParams = STPPaymentMethodParams.paramsWith( + card: cardParams, + billingDetails: billingParams, + metadata: nil + ) + + let createExpectation = self.expectation(description: "create payment details") + let logoutExpectation = self.expectation(description: "logout") + let useDetailsAfterLogoutExpectation = self.expectation(description: "try using payment details after logout") + consumerSession.createPaymentDetails( + paymentMethodParams: paymentMethodParams, + with: apiClient, + consumerAccountPublishableKey: sessionWithKey?.publishableKey + ) { result in + switch result { + case .success: + // If this succeeds, log out... + consumerSession.logout(with: self.apiClient, consumerAccountPublishableKey: sessionWithKey?.publishableKey) { logoutResult in + switch logoutResult { + case .success: + // Try to use the session again, it shouldn't work + consumerSession.createPaymentDetails(paymentMethodParams: paymentMethodParams, with: self.apiClient, consumerAccountPublishableKey: sessionWithKey?.publishableKey) { loggedOutAuthenticatedActionResult in + switch loggedOutAuthenticatedActionResult { + case .success: + XCTFail("Logout failed to invalidate token") + case .failure(let error): + + guard let stripeError = error as? StripeError, + case let .apiError(stripeAPIError) = stripeError else { + XCTFail("Received unexpected error response") + return + } + XCTAssertEqual(stripeAPIError.code, "consumer_session_credentials_invalid") + } + useDetailsAfterLogoutExpectation.fulfill() + } + case .failure(let error): + XCTFail("Logout failed: \(error.nonGenericDescription)") + } + logoutExpectation.fulfill() + } + case .failure(let error): + XCTFail("Received error: \(error.nonGenericDescription)") + } + + createExpectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/CustomerAdapterTests.swift b/Stripe/StripeiOSTests/CustomerAdapterTests.swift new file mode 100644 index 00000000..3b30eee4 --- /dev/null +++ b/Stripe/StripeiOSTests/CustomerAdapterTests.swift @@ -0,0 +1,288 @@ +// +// CustomerAdapterTests.swift +// StripePaymentSheetTests +// + +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift +@_spi(STP) @testable import StripeCore +@_spi(STP) @testable import StripeCore +@testable import StripeCoreTestUtils +@_spi(STP) @testable import StripePayments +@_spi(STP) @_spi(CustomerSessionBetaAccess) @testable import StripePaymentSheet +@_spi(STP) @testable import StripePaymentsTestUtils +import XCTest + +enum MockEphemeralKeyEndpoint { + case customerEphemeralKey(CustomerEphemeralKey) + case error(Error) + + init(_ error: Error) { + self = .error(error) + } + + init(_ customerEphemeralKey: CustomerEphemeralKey) { + self = .customerEphemeralKey(customerEphemeralKey) + } + + func getEphemeralKey() async throws -> CustomerEphemeralKey { + switch self { + case .customerEphemeralKey(let key): + return key + case .error(let error): + throw error + } + } +} + +class CustomerAdapterTests: APIStubbedTestCase { + + func stubListPaymentMethods( + key: CustomerEphemeralKey, + paymentMethodType: String, + paymentMethodJSONs: [[AnyHashable: Any]], + apiClient: STPAPIClient + ) { + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + && urlRequest.url?.absoluteString.contains("type=\(paymentMethodType)") ?? false + && urlRequest.httpMethod == "GET" + { + // Check to make sure we pass the ephemeral key correctly + let keyFromHeader = urlRequest.allHTTPHeaderFields!["Authorization"]? + .replacingOccurrences(of: "Bearer ", with: "") + XCTAssertEqual(keyFromHeader, key.ephemeralKeySecret) + return true + } + return false + } response: { urlRequest in + let paymentMethodsJSON = """ + { + "object": "list", + "url": "/v1/payment_methods", + "has_more": false, + "data": [ + ] + } + """ + var pmList = + try! JSONSerialization.jsonObject( + with: paymentMethodsJSON.data(using: .utf8)!, + options: [] + ) as! [AnyHashable: Any] + // Only send the example cards for a card request + if urlRequest.url?.absoluteString.contains("card") ?? false { + pmList["data"] = paymentMethodJSONs + } else if urlRequest.url?.absoluteString.contains("us_bank_account") ?? false { + pmList["data"] = paymentMethodJSONs + } + return HTTPStubsResponse(jsonObject: pmList, statusCode: 200, headers: nil) + } + } + func stubElementsSession( + paymentMethodJSONs: [[AnyHashable: Any]]? + ) { + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/v1/elements/sessions") ?? false + } response: { _ in + let elementsSessionJSON = """ + { + "payment_method_preference": {"ordered_payment_method_types": ["card"], + "country_code": "US" + }, + "ordered_payment_method_types" : ["card"], + "session_id": "123", + "apple_pay_preference": "enabled", + "customer": {"payment_methods": [ + ], + "customer_session": { + "id": "cuss_654321", + "livemode": false, + "api_key": "ek_12345", + "api_key_expiry": 1899787184, + "customer": "cus_12345" + } + } + } + """ + var elementSession = try! JSONSerialization.jsonObject( + with: elementsSessionJSON.data(using: .utf8)!, + options: [] + ) as! [AnyHashable: Any] + if var customer = elementSession["customer"] as? [AnyHashable: Any], + paymentMethodJSONs != nil { + customer["payment_methods"] = paymentMethodJSONs + elementSession["customer"] = customer + } + return HTTPStubsResponse(jsonObject: elementSession, statusCode: 200, headers: nil) + } + } + + func testGetOrCreateKeyErrorForwardedToFetchPMs() async throws { + let exp = expectation(description: "fetchPMs") + let expectedError = NSError(domain: "test", code: 123, userInfo: nil) + let apiClient = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + } response: { _ in + XCTFail("Retrieve PMs should not be called") + return HTTPStubsResponse(error: NSError(domain: "test", code: 100, userInfo: nil)) + } + let ekm = MockEphemeralKeyEndpoint(expectedError) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + do { + _ = try await sut.fetchPaymentMethods() + } catch { + XCTAssertEqual((error as NSError?)?.domain, expectedError.domain) + exp.fulfill() + } + await fulfillment(of: [exp]) + } + + let exampleKey = CustomerEphemeralKey(customerId: "abc123", ephemeralKeySecret: "ek_123") + + func testFetchPMs() async throws { + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()] + let apiClient = stubbedAPIClient() + // Expect 1 call per PM: cards + stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethodsJSON, apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: [], apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "sepa_debit", paymentMethodJSONs: [], apiClient: apiClient) + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + let pms = try await sut.fetchPaymentMethods() + + XCTAssertEqual(pms.count, 1) + XCTAssertEqual(pms[0].stripeId, expectedPaymentMethods[0].stripeId) + } + + func testFetchPM_CardAndUSBankAccount() async throws { + let expectedPaymentMethods_card = [STPFixtures.paymentMethod()] + let expectedPaymentMethods_cardJSON = [STPFixtures.paymentMethodJSON()] + + let expectedPaymentMethods_usbank = [STPFixtures.bankAccountPaymentMethod()] + let expectedPaymentMethods_usbankJSON = [STPFixtures.bankAccountPaymentMethodJSON()] + + let apiClient = stubbedAPIClient() + stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethods_cardJSON, apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: expectedPaymentMethods_usbankJSON, apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "sepa_debit", paymentMethodJSONs: [], apiClient: apiClient) + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, + setupIntentClientSecretProvider: { return "si_" }, + apiClient: apiClient) + let pms = try await sut.fetchPaymentMethods() + XCTAssertEqual(pms.count, 2) + XCTAssertEqual(pms[0].stripeId, expectedPaymentMethods_card[0].stripeId) + XCTAssertEqual(pms[1].stripeId, expectedPaymentMethods_usbank[0].stripeId) + } + + func testAttachPM() async throws { + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()] + let apiClient = stubbedAPIClient() + // Expect 1 call per PM: cards + stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethodsJSON, apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "sepa_debit", paymentMethodJSONs: [], apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: [], apiClient: apiClient) + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + let pms = try await sut.fetchPaymentMethods() + + XCTAssertEqual(pms.count, 1) + XCTAssertEqual(pms[0].stripeId, expectedPaymentMethods[0].stripeId) + } + + func testAttachPaymentMethodCallsAPIClientCorrectly() async { + let apiClient = stubbedAPIClient() + let expectedPaymentMethodJSON = STPFixtures.paymentMethodJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + + let exp = expectation(description: "payment method attach") + // We're attaching 1 payment method: + exp.expectedFulfillmentCount = 1 + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_method") ?? false + && urlRequest.httpMethod == "POST" + { + return true + } + return false + } response: { _ in + exp.fulfill() + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodJSON, + statusCode: 200, + headers: nil + ) + } + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + try! await sut.attachPaymentMethod(expectedPaymentMethods.first!.stripeId) + await fulfillment(of: [exp]) + } + + func testDetachPaymentMethodCallsAPIClientCorrectly() async { + let apiClient = stubbedAPIClient() + let expectedPaymentMethodJSON = STPFixtures.paymentMethodJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + + let exp = expectation(description: "payment method detach") + // We're detaching 1 payment method: + exp.expectedFulfillmentCount = 1 + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_method") ?? false + && urlRequest.httpMethod == "POST" + { + return true + } + return false + } response: { _ in + exp.fulfill() + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodJSON, + statusCode: 200, + headers: nil + ) + } + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + try! await sut.detachPaymentMethod(paymentMethodId: expectedPaymentMethods.first!.stripeId) + await fulfillment(of: [exp]) + } + + func testCustomerSheetLoadFiltersSavedApplePayCards() async throws { + let apiClient = stubbedAPIClient() + // Given a Customer with a saved card... + var savedCardJSON = STPFixtures.paymentMethodJSON() + savedCardJSON["id"] = "pm_saved_card" + // ...and a saved card that came from Apple Pay... + var savedApplePayCardJSON = STPFixtures.paymentMethodJSON() + savedApplePayCardJSON[jsonDict: "card"]?[jsonDict: "wallet"] = ["type": "apple_pay"] + savedApplePayCardJSON["id"] = "pm_saved_apple_pay_card" + + // ...fetching the customer's payment methods... + stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: [savedCardJSON, savedApplePayCardJSON], apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "sepa_debit", paymentMethodJSONs: [], apiClient: apiClient) + stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: [], apiClient: apiClient) + + let ekm = MockEphemeralKeyEndpoint(exampleKey) + let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient) + let pms = try await sut.fetchPaymentMethods() + + // ...should return the saved card but not the Apple Pay saved card + XCTAssertEqual(pms.count, 1) + XCTAssertEqual(pms[0].stripeId, "pm_saved_card") + } + + func configuration() -> CustomerSheet.Configuration { + return CustomerSheet.Configuration() + } +} diff --git a/Stripe/StripeiOSTests/Error+PaymentSheetTests.swift b/Stripe/StripeiOSTests/Error+PaymentSheetTests.swift new file mode 100644 index 00000000..eb2980b2 --- /dev/null +++ b/Stripe/StripeiOSTests/Error+PaymentSheetTests.swift @@ -0,0 +1,54 @@ +// +// Error+PaymentSheetTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 3/21/23. +// + +import Foundation +import XCTest + +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet + +class Error_PaymentSheetTests: XCTestCase { + + private enum TestableError: LocalizedError { + case generic + case custom(String) + + var errorDescription: String? { + switch self { + case .generic: + return NSError.stp_unexpectedErrorMessage() + case .custom(let errorMessage): + return errorMessage + } + } + } + + func testPaymentSheetError_UsesDebugDescription() { + let error = PaymentSheetError.unknown(debugDescription: "Test debugDescription") + + XCTAssertEqual("An unknown error occurred in PaymentSheet. Test debugDescription", error.nonGenericDescription) + } + + func testError_HasGenericLocalizedDescription_NoSeverError() { + XCTAssertEqual(NSError.stp_unexpectedErrorMessage(), TestableError.generic.nonGenericDescription) + } + + func testError_HasGenericLocalizedDescription_WithServerError() { + let serverErrorMessage = "Test failed server response error messasge" + let info = [NSLocalizedDescriptionKey: NSError.stp_unexpectedErrorMessage(), STPError.errorMessageKey: serverErrorMessage] + let error = NSError(domain: "Test error domain", code: 123, userInfo: info) + + XCTAssertEqual(serverErrorMessage, error.nonGenericDescription) + } + + func testError_HasLocalizedDescription() { + let errorMessage = "Test errorMessage" + let error = TestableError.custom(errorMessage) + + XCTAssertEqual(errorMessage, error.nonGenericDescription) + } +} diff --git a/Stripe/StripeiOSTests/FBSnapshotTestCase+STPViewControllerLoading.swift b/Stripe/StripeiOSTests/FBSnapshotTestCase+STPViewControllerLoading.swift new file mode 100644 index 00000000..3b75e218 --- /dev/null +++ b/Stripe/StripeiOSTests/FBSnapshotTestCase+STPViewControllerLoading.swift @@ -0,0 +1,79 @@ +// +// FBSnapshotTestCase+STPViewControllerLoading.swift +// StripeiOS Tests +// +// Created by Brian Dorfman on 12/11/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase + +extension FBSnapshotTestCase { + /// Embeds the given controller in a navigation controller, prepares it for + /// snapshot testing and returns the view controller's view. + @objc(stp_preparedAndSizedViewForSnapshotTestFromViewController:) + func stp_preparedAndSizedViewForSnapshotTest(from viewController: UIViewController?) -> UIView? + { + let navController = stp_navigationControllerForSnapshotTest(withRootVC: viewController) + return stp_preparedAndSizedViewForSnapshotTest(from: navController) + } + + /// Returns a navigation controller initialized with the given root view controller + /// and prepares it for snapshot testing (adding it to a UIWindow and loading views) + @objc func stp_navigationControllerForSnapshotTest( + withRootVC viewController: UIViewController? + ) + -> UINavigationController? + { + var navController: UINavigationController? + if let viewController = viewController { + navController = UINavigationController(rootViewController: viewController) + } + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) + testWindow.rootViewController = navController + testWindow.isHidden = false + + // Test that views loaded properly + loads them on first call + XCTAssertNotNil(navController?.view) + XCTAssertNotNil(viewController?.view) + + return navController + } + + /// Returns a view for snapshot testing from the topViewController of the given + /// navigation controller, making necessary layout adjustments for + /// `STPCoreScrollViewController`. + @objc(stp_preparedAndSizedViewForSnapshotTestFromNavigationController:) + func stp_preparedAndSizedViewForSnapshotTest( + from navController: UINavigationController? + ) + -> UIView? + { + let viewController = navController?.topViewController + + // Test that views loaded properly + loads them on first call + XCTAssertNotNil(navController?.view) + XCTAssertNotNil(viewController?.view) + + if viewController is STPCoreScrollViewController { + guard let scrollView = (viewController as? STPCoreScrollViewController)?.scrollView, + let navController = navController + else { + return nil + } + navController.view.layoutIfNeeded() + + let topOffset = scrollView.convert(scrollView.frame.origin, to: navController.view).y + navController.view.frame = CGRect( + x: 0, + y: 0, + width: 320, + height: (topOffset) + (scrollView.contentSize.height) + + (scrollView.contentInset.top) + + (scrollView.contentInset.bottom) + ) + } + + return navController?.view + } +} diff --git a/Stripe/StripeiOSTests/FormSpecProviderTest.swift b/Stripe/StripeiOSTests/FormSpecProviderTest.swift new file mode 100644 index 00000000..1453fdbc --- /dev/null +++ b/Stripe/StripeiOSTests/FormSpecProviderTest.swift @@ -0,0 +1,252 @@ +// +// FormSpecProviderTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/7/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class FormSpecProviderTest: XCTestCase { + func testLoadsJSON() throws { + let e = expectation(description: "Loads form specs file") + let sut = FormSpecProvider() + sut.load { loaded in + XCTAssertTrue(loaded) + e.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + + guard let eps = sut.formSpec(for: "eps") else { + XCTFail() + return + } + XCTAssertEqual(eps.fields.count, 5) + XCTAssertEqual( + eps.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + + // ...and iDEAL has the correct dropdown spec + guard let ideal = sut.formSpec(for: "ideal"), + case .name = ideal.fields[0], + case .selector(let selector) = ideal.fields[3] + else { + XCTFail() + return + } + XCTAssertEqual(selector.apiPath?["v1"], "ideal[bank]") + XCTAssertEqual(selector.items.count, 13) + } + + func testLoadJsonCanOverwriteLoadedSpecs() throws { + let e1 = expectation(description: "Loads form specs file") + let sut = FormSpecProvider() + let paymentMethodType = "eps" + sut.load { loaded in + XCTAssertTrue(loaded) + e1.fulfill() + } + wait(for: [e1], timeout: 2) + let eps = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(eps.fields.count, 5) + XCTAssertEqual( + eps.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + let updatedSpecJson = + """ + [{ + "type": "eps", + "async": false, + "fields": [ + { + "type": "name", + "api_path": { + "v1": "billing_details[someOtherValue]" + } + } + ] + }] + """.data(using: .utf8)! + let formSpec = try! JSONSerialization.jsonObject(with: updatedSpecJson) + + let result = sut.loadFrom(formSpec) + XCTAssert(result) + + let epsUpdated = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(epsUpdated.fields.count, 1) + XCTAssertEqual( + epsUpdated.fields.first, + .name( + FormSpec.NameFieldSpec( + apiPath: ["v1": "billing_details[someOtherValue]"], + translationId: nil + ) + ) + ) + + // If load is called again, ensure that on-disk specs do not override + let e2 = expectation(description: "Loads form specs file, 2nd time") + sut.load { loaded in + XCTAssertTrue(loaded) + e2.fulfill() + } + wait(for: [e2], timeout: 2) + XCTAssertEqual( + epsUpdated.fields.first, + .name( + FormSpec.NameFieldSpec( + apiPath: ["v1": "billing_details[someOtherValue]"], + translationId: nil + ) + ) + ) + } + + func testLoadJsonFailsGracefully() throws { + let e = expectation(description: "Loads form specs file") + let sut = FormSpecProvider() + let paymentMethodType = "eps" + sut.load { loaded in + XCTAssertTrue(loaded) + e.fulfill() + } + waitForExpectations(timeout: 20, handler: nil) + let eps = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(eps.fields.count, 5) + XCTAssertEqual( + eps.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + let updatedSpecJson = + """ + [{ + "INVALID_type": "eps", + "async": false, + "fields": [ + { + "type": "name", + "api_path": { + "v1": "billing_details[someOtherValue]" + } + } + ] + }] + """.data(using: .utf8)! + let formSpec = try! JSONSerialization.jsonObject(with: updatedSpecJson) + + let result = sut.loadFrom(formSpec) + XCTAssertFalse(result) + let epsUpdated = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(epsUpdated.fields.count, 5) + XCTAssertEqual( + epsUpdated.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + } + + func testLoadNotValidJsonFailsGracefully() throws { + let e = expectation(description: "Loads form specs file") + let sut = FormSpecProvider() + let paymentMethodType = "eps" + sut.load { loaded in + XCTAssertTrue(loaded) + e.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + + let eps = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(eps.fields.count, 5) + XCTAssertEqual( + eps.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + + let updatedSpecJson = + """ + NOT VALID JSON + """.data(using: .utf8)! + + let result = sut.loadFrom(updatedSpecJson) + XCTAssertFalse(result) + let epsUpdated = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(epsUpdated.fields.count, 5) + XCTAssertEqual( + epsUpdated.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + } + + func testLoadJsonDoesOverwrites() throws { + let e = expectation(description: "Loads form specs file") + let sut = FormSpecProvider() + let paymentMethodType = "eps" + sut.load { loaded in + XCTAssertTrue(loaded) + e.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + let eps = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(eps.fields.count, 5) + XCTAssertEqual( + eps.fields.first, + .name(FormSpec.NameFieldSpec(apiPath: ["v1": "billing_details[name]"], translationId: nil)) + ) + + let updatedSpecJson = + """ + [{ + "type": "eps", + "async": false, + "fields": [ + { + "type": "name", + "api_path": { + "v1": "billing_details[someOtherValue]" + } + } + ], + "next_action_spec": { + "confirm_response_status_specs": { + "requires_action": { + "type": "redirect_to_url" + } + }, + "post_confirm_handling_pi_status_specs": { + "succeeded": { + "type": "finished" + }, + "requires_action": { + "type": "finished" + } + } + } + }] + """.data(using: .utf8)! + let formSpec = try! JSONSerialization.jsonObject(with: updatedSpecJson) + + let result = sut.loadFrom(formSpec) + XCTAssert(result) + + // Validate ability to override LPM behavior of next actions + let epsUpdated = try XCTUnwrap(sut.formSpec(for: paymentMethodType)) + XCTAssertEqual(epsUpdated.fields.count, 1) + XCTAssertEqual( + epsUpdated.fields.first, + .name( + FormSpec.NameFieldSpec( + apiPath: ["v1": "billing_details[someOtherValue]"], + translationId: nil + ) + ) + ) + } +} diff --git a/Stripe/StripeiOSTests/FraudDetectionDataTest.swift b/Stripe/StripeiOSTests/FraudDetectionDataTest.swift new file mode 100644 index 00000000..3b4ba93d --- /dev/null +++ b/Stripe/StripeiOSTests/FraudDetectionDataTest.swift @@ -0,0 +1,31 @@ +// +// FraudDetectionDataTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class FraudDetectionDataTest: XCTestCase { + func testResetsSIDIfExpired() { + FraudDetectionData.shared.sidCreationDate = Date(timeInterval: -30 * 60 - 1, since: Date()) + FraudDetectionData.shared.resetSIDIfExpired() + XCTAssertNil(FraudDetectionData.shared.sid) + } + + func testSIDNotExpired() { + // Test resets sid if expired + FraudDetectionData.shared.sid = "123" + FraudDetectionData.shared.sidCreationDate = Date() + FraudDetectionData.shared.resetSIDIfExpired() + XCTAssertNotNil(FraudDetectionData.shared.sid) + } +} diff --git a/Stripe/StripeiOSTests/HostedSurfaceTest.swift b/Stripe/StripeiOSTests/HostedSurfaceTest.swift new file mode 100644 index 00000000..0fbe10b0 --- /dev/null +++ b/Stripe/StripeiOSTests/HostedSurfaceTest.swift @@ -0,0 +1,83 @@ +// +// HostedSurfaceTest.swift +// StripeiOSTests +// +// Created by Nick Porter on 12/20/23. +// + +import Foundation +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class HostedSurfaceTest: XCTestCase { + + // Test the initializer + func testHostedSurfaceInitializer() { + let paymentSheetConfig = PaymentSheetFormFactoryConfig.paymentSheet(.init()) + + let hostedSurfaceForPaymentSheet = HostedSurface(config: paymentSheetConfig) + XCTAssertEqual(hostedSurfaceForPaymentSheet, .paymentSheet) + + let customerSheetConfig = PaymentSheetFormFactoryConfig.customerSheet(.init()) + + let hostedSurfaceForCustomerSheet = HostedSurface(config: customerSheetConfig) + XCTAssertEqual(hostedSurfaceForCustomerSheet, .customerSheet) + } + + // Test analyticEvent function for every event in CardBrandChoiceEvents + func testPaymentSheetAnalyticEvents() { + let hostedSurface = HostedSurface.paymentSheet + testAnalyticEvents(for: hostedSurface) + } + + func testCustomerSheetAnalyticEvents() { + let hostedSurface = HostedSurface.customerSheet + testAnalyticEvents(for: hostedSurface) + } + + private func testAnalyticEvents(for hostedSurface: HostedSurface) { + let events: [HostedSurface.CardBrandChoiceEvents] = [ + .displayCardBrandDropdownIndicator, + .openCardBrandDropdown, + .closeCardBrandDropDown, + .openCardBrandEditScreen, + .updateCardBrand, + .updateCardBrandFailed, + .closeEditScreen, + ] + + let expectedEventsPaymentSheet: [HostedSurface.CardBrandChoiceEvents: STPAnalyticEvent] = [ + .displayCardBrandDropdownIndicator: .paymentSheetDisplayCardBrandDropdownIndicator, + .openCardBrandDropdown: .paymentSheetOpenCardBrandDropdown, + .closeCardBrandDropDown: .paymentSheetCloseCardBrandDropDown, + .openCardBrandEditScreen: .paymentSheetOpenCardBrandEditScreen, + .updateCardBrand: .paymentSheetUpdateCardBrand, + .updateCardBrandFailed: .paymentSheetUpdateCardBrandFailed, + .closeEditScreen: .paymentSheetClosesEditScreen, + ] + + let expectedEventsCustomerSheet: [HostedSurface.CardBrandChoiceEvents: STPAnalyticEvent] = [ + .displayCardBrandDropdownIndicator: .customerSheetDisplayCardBrandDropdownIndicator, + .openCardBrandDropdown: .customerSheetOpenCardBrandDropdown, + .closeCardBrandDropDown: .customerSheetCloseCardBrandDropDown, + .openCardBrandEditScreen: .customerSheetOpenCardBrandEditScreen, + .updateCardBrand: .customerSheetUpdateCardBrand, + .updateCardBrandFailed: .customerSheetUpdateCardBrandFailed, + .closeEditScreen: .customerSheetClosesEditScreen, + ] + + for event in events { + let analyticEvent = hostedSurface.analyticEvent(for: event) + // assert that the event is the expected one + switch hostedSurface { + case .paymentSheet: + XCTAssertEqual(analyticEvent, expectedEventsPaymentSheet[event]) + case .customerSheet: + XCTAssertEqual(analyticEvent, expectedEventsCustomerSheet[event]) + } + } + } +} diff --git a/Stripe/StripeiOSTests/ImageTest.swift b/Stripe/StripeiOSTests/ImageTest.swift new file mode 100644 index 00000000..3c31b62e --- /dev/null +++ b/Stripe/StripeiOSTests/ImageTest.swift @@ -0,0 +1,27 @@ +// +// ImageTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/19/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class ImageTest: XCTestCase { + func testAllImagesExist() throws { + for image in Image.allCases { + let image = UIImage( + named: image.rawValue, + in: StripePaymentSheetBundleLocator.resourcesBundle, + compatibleWith: nil + ) + XCTAssertNotNil(image) + } + } +} diff --git a/Stripe/StripeiOSTests/Info.plist b/Stripe/StripeiOSTests/Info.plist new file mode 100644 index 00000000..ba72822e --- /dev/null +++ b/Stripe/StripeiOSTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift new file mode 100644 index 00000000..90d1e10a --- /dev/null +++ b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift @@ -0,0 +1,170 @@ +// +// LinkInlineSignupElementSnapshotTests.swift +// StripeiOS Tests +// + +import iOSSnapshotTestCase +@_spi(STP) import StripeCoreTestUtils +@_spi(STP) import StripeUICore +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class LinkInlineSignupElementSnapshotTests: STPSnapshotTestCase { + + // MARK: Normal mode + + func testDefaultState() { + let sut = makeSUT() + verify(sut) + } + + // WARNING: If this tests fails, see go/link-signup-consent-action-log to determine if a new consent_action is needed. + func testExpandedState() { + let sut = makeSUT(saveCheckboxChecked: true, userTypedEmailAddress: "user@example.com") + verify(sut) + } + + // WARNING: If this tests fails, see go/link-signup-consent-action-log to determine if a new consent_action is needed. + func testExpandedState_nonUS() { + let sut = makeSUT( + saveCheckboxChecked: true, + userTypedEmailAddress: "user@example.com", + country: "CA" + ) + verify(sut) + } + + func testExpandedState_nonUS_preFilled() { + let sut = makeSUT( + saveCheckboxChecked: true, + userTypedEmailAddress: "user@example.com", + country: "CA", + preFillName: "Jane Diaz", + preFillPhone: "+13105551234" + ) + verify(sut) + } + + // MARK: Textfield only mode + + func testDefaultState_textFieldsOnly() { + let sut = makeSUT(showCheckbox: false) + verify(sut) + } + + // WARNING: If this tests fails, see go/link-signup-consent-action-log to determine if a new consent_action is needed. + func testExpandedState_textFieldsOnly() { + let sut = makeSUT(saveCheckboxChecked: true, userTypedEmailAddress: "user@example.com", showCheckbox: false) + verify(sut) + } + + // WARNING: If this tests fails, see go/link-signup-consent-action-log to determine if a new consent_action is needed. + func testExpandedState_nonUS_textFieldsOnly() { + let sut = makeSUT( + saveCheckboxChecked: true, + userTypedEmailAddress: "user@example.com", + country: "CA", + showCheckbox: false + ) + verify(sut) + } + + // WARNING: If this tests fails, see go/link-signup-consent-action-log to determine if a new consent_action is needed. + func testExpandedState_nonUS_preFilled_textFieldsOnly() { + // In textFieldsOnly mode, the phone number should *not* be prefilled. + let sut = makeSUT( + saveCheckboxChecked: true, + linkAccountEmailAddress: "user@example.com", + country: "CA", + preFillName: "Jane Diaz", + preFillPhone: "+13105551234", + showCheckbox: false + ) + verify(sut) + } + + func verify( + _ element: LinkInlineSignupElement, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + element.view.autosizeHeight(width: 340) + STPSnapshotVerifyView(element.view, identifier: identifier, file: file, line: line) + } + +} + +extension LinkInlineSignupElementSnapshotTests { + + struct MockAccountService: LinkAccountServiceProtocol { + func lookupAccount( + withEmail email: String?, + completion: @escaping (Result) -> Void + ) { + completion( + .success( + PaymentSheetLinkAccount( + email: "user@example.com", + session: nil, + publishableKey: nil + ) + ) + ) + } + + func hasEmailLoggedOut(email: String) -> Bool { + // TODO(porter): Determine if we want to implement this in tests + return false + } + } + + func makeSUT( + saveCheckboxChecked: Bool = false, + linkAccountEmailAddress: String? = nil, + userTypedEmailAddress: String? = nil, + country: String = "US", + preFillName: String? = nil, + preFillPhone: String? = nil, + showCheckbox: Bool = true + ) -> LinkInlineSignupElement { + var configuration = PaymentSheet.Configuration() + configuration.merchantDisplayName = "[Merchant]" + configuration.defaultBillingDetails.name = preFillName + configuration.defaultBillingDetails.phone = preFillPhone + + var linkAccount: PaymentSheetLinkAccount? + + if let linkAccountEmailAddress { + linkAccount = PaymentSheetLinkAccount(email: linkAccountEmailAddress, session: nil, publishableKey: nil) + } + + let viewModel = LinkInlineSignupViewModel( + configuration: configuration, + showCheckbox: showCheckbox, + accountService: MockAccountService(), + linkAccount: linkAccount, + country: country + ) + + viewModel.saveCheckboxChecked = saveCheckboxChecked + // Won't trigger the "email address prefilled" path, because it wasn't there when initialized + if let userTypedEmailAddress { + viewModel.emailAddress = userTypedEmailAddress + } + + if userTypedEmailAddress != nil || linkAccountEmailAddress != nil { + // Wait for account to load + let expectation = notNullExpectation(for: viewModel, keyPath: \.linkAccount) + wait(for: [expectation], timeout: 10) + } + + return .init(viewModel: viewModel) + } + +} diff --git a/Stripe/StripeiOSTests/LinkLegalTermsViewSnapshotTests.swift b/Stripe/StripeiOSTests/LinkLegalTermsViewSnapshotTests.swift new file mode 100644 index 00000000..4f4417d0 --- /dev/null +++ b/Stripe/StripeiOSTests/LinkLegalTermsViewSnapshotTests.swift @@ -0,0 +1,102 @@ +// +// LinkLegalTermsViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 1/26/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class LinkLegalTermsViewSnapshotTests: STPSnapshotTestCase { + + func testDefault() { + let sut = makeSUT() + verify(sut) + } + + func testCentered() { + let sut = makeSUT(textAlignment: .center) + verify(sut) + } + + func testColorCustomization() { + let sut = makeSUT() + sut.textColor = .orange + sut.tintColor = .purple + verify(sut) + } + + func testLocalization_de() { + performLocalizedSnapshotTest(forLanguage: "de") + } + func testLocalization_es() { + performLocalizedSnapshotTest(forLanguage: "es") + } + func testLocalization_el_GR() { + performLocalizedSnapshotTest(forLanguage: "el-GR") + } + func testLocalization_it() { + performLocalizedSnapshotTest(forLanguage: "it") + } + func testLocalization_ja() { + performLocalizedSnapshotTest(forLanguage: "ja") + } + func testLocalization_ko() { + performLocalizedSnapshotTest(forLanguage: "ko") + } + func testLocalization_zh_hans() { + performLocalizedSnapshotTest(forLanguage: "zh-Hans") + } + +} + +// MARK: - Helpers + +extension LinkLegalTermsViewSnapshotTests { + + func verify( + _ view: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + view.autosizeHeight(width: 250) + STPSnapshotVerifyView(view, identifier: identifier, file: file, line: line) + } + + func performLocalizedSnapshotTest( + forLanguage language: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + STPLocalizationUtils.overrideLanguage(to: language) + let sut = makeSUT() + STPLocalizationUtils.overrideLanguage(to: nil) + verify(sut, identifier: language, file: file, line: line) + } + +} + +// MARK: - Factory + +extension LinkLegalTermsViewSnapshotTests { + + func makeSUT() -> LinkLegalTermsView { + return LinkLegalTermsView() + } + + func makeSUT(textAlignment: NSTextAlignment) -> LinkLegalTermsView { + return LinkLegalTermsView(textAlignment: textAlignment) + } + +} diff --git a/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift new file mode 100644 index 00000000..23759abe --- /dev/null +++ b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift @@ -0,0 +1,216 @@ +// +// LinkSignupViewModelTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 1/21/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +import StripeCoreTestUtils +@_spi(STP) import StripeUICore +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class LinkInlineSignupViewModelTests: STPNetworkStubbingTestCase { + + // Should be ~4x the debounce time for best results. + let accountLookupTimeout: TimeInterval = 4 + + func test_defaults() { + let sut = makeSUT(country: "US", showCheckbox: true) + + XCTAssertFalse(sut.shouldShowEmailField) + XCTAssertFalse(sut.shouldShowPhoneField) + XCTAssertFalse(sut.shouldShowNameField) + XCTAssertFalse(sut.shouldShowLegalTerms) + } + + func test_shouldShowEmailFieldWhenCheckboxIsChecked() { + let sut = makeSUT(country: "US", showCheckbox: true) + + sut.saveCheckboxChecked = true + XCTAssertTrue(sut.shouldShowEmailField) + + sut.saveCheckboxChecked = false + XCTAssertFalse(sut.shouldShowEmailField) + } + + func test_shouldShowRegistrationFieldsWhenEmailIsProvided() { + let sut = makeSUT(country: "US", showCheckbox: true) + + sut.saveCheckboxChecked = true + sut.emailAddress = "user@example.com" + + // Wait for async change on `shouldShowPhoneField`. + let showPhoneFieldExpectation = expectation( + for: sut, + keyPath: \.shouldShowPhoneField, + equalsToValue: true + ) + wait(for: [showPhoneFieldExpectation], timeout: accountLookupTimeout) + + XCTAssertFalse(sut.shouldShowNameField, "Should not show name field for US customers") + XCTAssertTrue( + sut.shouldShowLegalTerms, + "Should show legal terms when creating a new account" + ) + + sut.emailAddress = nil + + // Wait for async change on `shouldShowPhoneField`. + let hidePhoneFieldExpectation = expectation( + for: sut, + keyPath: \.shouldShowPhoneField, + equalsToValue: false + ) + wait(for: [hidePhoneFieldExpectation], timeout: accountLookupTimeout) + XCTAssertFalse(sut.shouldShowNameField) + sut.saveCheckboxChecked = false + XCTAssertFalse(sut.shouldShowLegalTerms) + } + + func test_shouldShowNameField_nonUSCustomers() { + let sut = makeSUT(country: "CA", showCheckbox: true, hasAccountObject: true) + sut.saveCheckboxChecked = true + XCTAssertTrue(sut.shouldShowNameField, "Should show name field for non-US customers") + } + + func test_shouldShowLegalText() { + let sut = makeSUT(country: "US", showCheckbox: true, hasAccountObject: false) + sut.saveCheckboxChecked = false + XCTAssertFalse(sut.shouldShowLegalTerms) + sut.saveCheckboxChecked = true + XCTAssertTrue(sut.shouldShowLegalTerms) + } + + func test_action_returnsNilUnlessPhoneRequirementIsFulfilled() { + let sut = makeSUT(country: "US", showCheckbox: true, hasAccountObject: true) + + sut.saveCheckboxChecked = true + XCTAssertNil(sut.action) + + sut.phoneNumber = PhoneNumber(number: "5555555555", countryCode: "US") + XCTAssertNotNil(sut.action) + } + + func test_action_returnsNilUnlessNameRequirementIsFulfilled() { + // Non-US customers require providing a name + let sut = makeSUT(country: "CA", showCheckbox: true, hasAccountObject: true) + + sut.saveCheckboxChecked = true + sut.phoneNumber = PhoneNumber(number: "5555555555", countryCode: "CA") + XCTAssertNil(sut.action, "`action` must be nil unless a name is provided") + + sut.legalName = "Jane Doe" + XCTAssertNotNil(sut.action) + } + + func test_action_returnsContinueWithoutLinkIfCheckboxIsNotChecked() { + let sut = makeSUT(country: "US", showCheckbox: true) + + sut.saveCheckboxChecked = false + XCTAssertEqual(sut.action, .continueWithoutLink) + } + + func test_action_returnsContinueWithoutLinkIfLookupFails() { + let sut = makeSUT(country: "US", showCheckbox: true, shouldFailLookup: true) + + sut.saveCheckboxChecked = true + sut.emailAddress = "user@example.com" + + // Wait for lookup to fail + let lookupFailedExpectation = expectation( + for: sut, + keyPath: \.lookupFailed, + equalsToValue: true + ) + wait(for: [lookupFailedExpectation], timeout: accountLookupTimeout) + + XCTAssertEqual(sut.action, .continueWithoutLink) + } + + func test_consentAction_checkbox() { + let sut = makeSUT(country: "US", showCheckbox: true, hasAccountObject: false) + XCTAssertEqual(sut.consentAction, .checkbox_v0) + } + + func test_consentAction_checkbox_prefillEmail() { + let sut = makeSUT(country: "US", showCheckbox: true, hasAccountObject: true) + XCTAssertEqual(sut.consentAction, .checkbox_v0_0) + } + + func test_consentAction_checkbox_prefillEmailAndPhone() { + let sut = makeSUT(country: "US", showCheckbox: true, hasAccountObject: true) + sut.phoneNumber = PhoneNumber(number: "555555555", countryCode: "1") + sut.phoneNumberWasPrefilled = true + XCTAssertEqual(sut.consentAction, .checkbox_v0_1) + } + + func test_consentAction_implied() { + let sut = makeSUT(country: "US", showCheckbox: false, hasAccountObject: false) + XCTAssertEqual(sut.consentAction, .implied_v0) + } + + func test_consentAction_implied_prefillEmail() { + let sut = makeSUT(country: "US", showCheckbox: false, hasAccountObject: true) + XCTAssertEqual(sut.consentAction, .implied_v0_0) + } +} + +extension LinkInlineSignupViewModelTests { + + struct MockAccountService: LinkAccountServiceProtocol { + let shouldFailLookup: Bool + + func lookupAccount( + withEmail email: String?, + completion: @escaping (Result) -> Void + ) { + if shouldFailLookup { + completion(.failure(NSError.stp_genericConnectionError())) + } else { + completion( + .success( + PaymentSheetLinkAccount( + email: "user@example.com", + session: nil, + publishableKey: nil + ) + ) + ) + } + } + + func hasEmailLoggedOut(email: String) -> Bool { + // TODO(porter): Determine if we want to implement this in tests + return false + } + } + + func makeSUT( + country: String, + showCheckbox: Bool, + hasAccountObject: Bool = false, + shouldFailLookup: Bool = false + ) -> LinkInlineSignupViewModel { + let linkAccount: PaymentSheetLinkAccount? = hasAccountObject + ? PaymentSheetLinkAccount(email: "user@example.com", session: nil, publishableKey: nil) + : nil + + return LinkInlineSignupViewModel( + configuration: .init(), + showCheckbox: showCheckbox, + accountService: MockAccountService(shouldFailLookup: shouldFailLookup), + linkAccount: linkAccount, + country: country + ) + } + +} diff --git a/Stripe/StripeiOSTests/MKPlacemark+PaymentSheetTests.swift b/Stripe/StripeiOSTests/MKPlacemark+PaymentSheetTests.swift new file mode 100644 index 00000000..711e3863 --- /dev/null +++ b/Stripe/StripeiOSTests/MKPlacemark+PaymentSheetTests.swift @@ -0,0 +1,209 @@ +// +// MKPlacemark+PaymentSheetTests.swift +// StripeiOS Tests +// +// Created by Nick Porter on 6/13/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Contacts +import MapKit +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class MKPlacemark_PaymentSheetTests: XCTestCase { + + // All address dictionaries are based on an actual placemark of an `MKLocalSearchCompletion` + + func testAsAddress_UnitedStates() { + // Search string used to generate address dictionary: "4 Pennsylvania Pl" + let addressDictionary = + [ + CNPostalAddressStreetKey: "4 Pennsylvania Plaza", + CNPostalAddressStateKey: "NY", + CNPostalAddressCountryKey: "United States", + CNPostalAddressISOCountryCodeKey: "US", + CNPostalAddressCityKey: "New York", + "SubThoroughfare": "4", + CNPostalAddressPostalCodeKey: "10001", + "Thoroughfare": "Pennsylvania Plaza", + CNPostalAddressSubLocalityKey: "Manhattan", + CNPostalAddressSubAdministrativeAreaKey: "New York County", + "Name": "4 Pennsylvania Plaza", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "New York", + country: "US", + line1: "4 Pennsylvania Plaza", + line2: nil, + postalCode: "10001", + state: "NY" + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + + func testAsAddress_Canada() { + // Search string used to generate address dictionary: "40 Bay St To" + let addressDictionary = + [ + CNPostalAddressStreetKey: "40 Bay St", + CNPostalAddressStateKey: "ON", + CNPostalAddressISOCountryCodeKey: "CA", + CNPostalAddressCountryKey: "Canada", + CNPostalAddressCityKey: "Toronto", + "SubThoroughfare": "40", + CNPostalAddressPostalCodeKey: "M5J 2X2", + "Thoroughfare": "Bay St", + CNPostalAddressSubLocalityKey: "Downtown Toronto", + CNPostalAddressSubAdministrativeAreaKey: "SubAdministrativeArea", + "Name": "40 Bay St", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "Toronto", + country: "CA", + line1: "40 Bay St", + line2: nil, + postalCode: "M5J 2X2", + state: "ON" + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + + func testAsAddress_Germany() { + // Search string used to generate address dictionary: "Rüsternallee 14" + let addressDictionary = + [ + CNPostalAddressStreetKey: "Rüsternallee 14", + CNPostalAddressISOCountryCodeKey: "DE", + CNPostalAddressCountryKey: "Germany", + CNPostalAddressCityKey: "Berlin", + CNPostalAddressPostalCodeKey: "14050", + "SubThoroughfare": "14", + "Thoroughfare": "Rüsternallee", + CNPostalAddressSubLocalityKey: "Charlottenburg-Wilmersdorf", + CNPostalAddressSubAdministrativeAreaKey: "Berlin", + "Name": "Rüsternallee 14", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "Berlin", + country: "DE", + line1: "Rüsternallee 14", + line2: nil, + postalCode: "14050", + state: nil + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + + func testAsAddress_Brazil() { + // Search string used to generate address dictionary: "Avenida Paulista 500" + let addressDictionary = + [ + CNPostalAddressStreetKey: "Avenida Paulista, 500", + CNPostalAddressStateKey: "SP", + CNPostalAddressISOCountryCodeKey: "BR", + CNPostalAddressCountryKey: "Brazil", + CNPostalAddressCityKey: "Paulínia", + "SubThoroughfare": "500", + CNPostalAddressPostalCodeKey: "13145-089", + "Thoroughfare": "Avenida Paulista", + CNPostalAddressSubLocalityKey: "Jardim Planalto", + "Name": "Avenida Paulista, 500", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "Paulínia", + country: "BR", + line1: "Avenida Paulista, 500", + line2: nil, + postalCode: "13145-089", + state: "SP" + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + + func testAsAddress_Japan() { + // Search string used to generate address dictionary: "Nagatacho 2" + let addressDictionary = + [ + CNPostalAddressStreetKey: "Nagatacho 2-Chōme", + CNPostalAddressStateKey: "Tokyo", + CNPostalAddressISOCountryCodeKey: "JP", + CNPostalAddressCountryKey: "Japan", + CNPostalAddressCityKey: "Chiyoda", + "Thoroughfare": "Nagatacho 2-Chōme", + CNPostalAddressSubLocalityKey: "Nagatacho", + "Name": "Nagatacho 2-Chōme", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "Chiyoda", + country: "JP", + line1: "Nagatacho 2-Chōme", + line2: nil, + postalCode: nil, + state: "Tokyo" + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + + func testAsAddress_Australia() { + // Search string used to generate address dictionary: "488 George St Syd" + let addressDictionary = + [ + CNPostalAddressStreetKey: "488 George St", + CNPostalAddressStateKey: "NSW", + CNPostalAddressISOCountryCodeKey: "AU", + CNPostalAddressCountryKey: "Australia", + CNPostalAddressCityKey: "Sydney", + CNPostalAddressPostalCodeKey: "2000", + "SubThoroughfare": "488", + "Thoroughfare": "George St", + CNPostalAddressSubAdministrativeAreaKey: "Sydney", + "Name": "488 George St", + ] as [String: Any] + let placemark = MKPlacemark( + coordinate: CLLocationCoordinate2D(), + addressDictionary: addressDictionary + ) + let expectedAddress = PaymentSheet.Address( + city: "Sydney", + country: "AU", + line1: "488 George St", + line2: nil, + postalCode: "2000", + state: "NSW" + ) + + XCTAssertEqual(placemark.asAddress, expectedAddress) + } + +} diff --git a/Stripe/StripeiOSTests/NSArray+StripeTest.swift b/Stripe/StripeiOSTests/NSArray+StripeTest.swift new file mode 100644 index 00000000..8e12190e --- /dev/null +++ b/Stripe/StripeiOSTests/NSArray+StripeTest.swift @@ -0,0 +1,67 @@ +// +// NSArray+StripeTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 1/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +import XCTest + +class Array_StripeTest: XCTestCase { + func test_arrayByRemovingNulls_removesNullsDeeply() { + let array: [Any] = [ + "id", + NSNull(), // null in root + [ + "user": "user_123", + "country": NSNull(), // null in dictionary + "nicknames": ["john", "johnny", NSNull()], + "profiles": [ + "facebook": "fb_123", + "twitter": NSNull(), + ], + ], + [ + NSNull(), // null in array + [ + "id": "fee_123", + "frequency": NSNull(), + ], + ["payment", NSNull()], + ], + ] + + let expected: [Any] = [ + "id", + [ + "user": "user_123", + "nicknames": ["john", "johnny"], + "profiles": [ + "facebook": "fb_123" + ], + ], + [ + [ + "id": "fee_123" + ], ["payment"], + ], + ] + + let result = array.stp_arrayByRemovingNulls() + + XCTAssertEqual(result as NSArray, expected as NSArray) + } + + func test_arrayByRemovingNulls_keepsEmptyLeaves() { + let array = [NSNull()] + let result = array.stp_arrayByRemovingNulls() + + XCTAssertEqual(result as NSArray, [] as NSArray) + } +} diff --git a/Stripe/StripeiOSTests/NSDecimalNumber+StripeTest.swift b/Stripe/StripeiOSTests/NSDecimalNumber+StripeTest.swift new file mode 100644 index 00000000..33ae8d48 --- /dev/null +++ b/Stripe/StripeiOSTests/NSDecimalNumber+StripeTest.swift @@ -0,0 +1,103 @@ +// +// NSDecimalNumber+StripeTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +import XCTest + +class NSDecimalNumberStripeTest: XCTestCase { + // an incomplete list of 2 decimal point currencies + private let twoDecimalPointCurrencies = [ + "usd", + "dkk", + "eur", + "aud", + "sek", + "sgd", + + // Special cases: + "cop", + "pkr", + "lak", + "rsd", + ] + + private let noDecimalPointCurrencies = [ + "bif", + "clp", + "djf", + "gnf", + "jpy", + "kmf", + "krw", + "mga", + "pyg", + "rwf", + "vnd", + "vuv", + "xaf", + "xof", + "xpf", + ] + + private let threeDecimalCurrencies = [ + "bhd", + "jod", + "kwd", + "omr", + "tnd", + ] + + func testDecimalAmount_twoDecimal() { + for twoDecimalPointCurrency in twoDecimalPointCurrencies { + let decimalNumber = NSDecimalNumber.stp_decimalNumber(withAmount: 92123, currency: twoDecimalPointCurrency) + XCTAssertEqual(decimalNumber, NSDecimalNumber(string: "921.23")) + } + } + + func testDecimalAmount_noDecimal() { + for currency in noDecimalPointCurrencies { + let decimalNumber = NSDecimalNumber.stp_decimalNumber(withAmount: 92123, currency: currency) + XCTAssertEqual(decimalNumber, NSDecimalNumber(string: "92123")) + } + } + + func testDecimalAmount_threeDecimal() { + for currency in threeDecimalCurrencies { + let decimalNumber = NSDecimalNumber.stp_decimalNumber(withAmount: 92123, currency: currency) + XCTAssertEqual(decimalNumber, NSDecimalNumber(string: "92.123")) + } + } + + func testAmount_twoDecimal() { + for twoDecimalPointCurrency in twoDecimalPointCurrencies { + let amount = NSDecimalNumber(value: 1000.12) + let decimalNumber = amount.stp_amount(withCurrency: twoDecimalPointCurrency) + XCTAssertEqual(decimalNumber, 100012) + } + } + + func testAmount_noDecimal() { + for noDecimalPointCurrency in noDecimalPointCurrencies { + let amount = NSDecimalNumber(value: 1000.12) + let decimalNumber = amount.stp_amount(withCurrency: noDecimalPointCurrency) + XCTAssertEqual(decimalNumber, 1000) + } + } + + func testAmount_threeDecimal() { + for threeDecimalPointCurrency in threeDecimalCurrencies { + let amount = NSDecimalNumber(value: 1000.12) + let decimalNumber = amount.stp_amount(withCurrency: threeDecimalPointCurrency) + XCTAssertEqual(decimalNumber, 1000120) + } + } +} diff --git a/Stripe/StripeiOSTests/NSDictionary+StripeTest.swift b/Stripe/StripeiOSTests/NSDictionary+StripeTest.swift new file mode 100644 index 00000000..84287033 --- /dev/null +++ b/Stripe/StripeiOSTests/NSDictionary+StripeTest.swift @@ -0,0 +1,254 @@ +// +// Dictionary+StripeTest.swift +// StripeiOS Tests +// +// Created by Joey Dong on 7/24/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +import XCTest + +class Dictionary_StripeTest: XCTestCase { + // MARK: - dictionaryByRemovingNullsValidatingRequiredFields + func test_dictionaryByRemovingNulls_removesNullsDeeply() { + let dictionary = + [ + "id": "card_123", + "tokenization_method": NSNull(), // null in root + "metadata": [ + "user": "user_123", + "country": NSNull(), // null in dictionary + "nicknames": ["john", "johnny", NSNull()], + "profiles": [ + "facebook": "fb_123", + "twitter": NSNull(), + ], + ], + "fees": [ + NSNull(), // null in array + [ + "id": "fee_123", + "frequency": NSNull(), + ], + ["payment", NSNull()], + ], + ] as [AnyHashable: Any] + + let expected = + [ + "id": "card_123", + "metadata": [ + "user": "user_123", + "nicknames": ["john", "johnny"], + "profiles": [ + "facebook": "fb_123" + ], + ], + "fees": [ + [ + "id": "fee_123" + ], ["payment"], + ], + ] as [AnyHashable: Any] + + let result = dictionary.stp_dictionaryByRemovingNulls() + XCTAssertEqual(result as NSDictionary, expected as NSDictionary) + } + + func test_dictionaryByRemovingNullsValidatingRequiredFields_keepsEmptyLeaves() { + let dictionary = + [ + "id": NSNull() + ] as [AnyHashable: Any] + let result = dictionary.stp_dictionaryByRemovingNulls() + + XCTAssertEqual(result as NSDictionary, [:] as NSDictionary) + } + + // MARK: - dictionaryByRemovingNonStrings + func test_dictionaryByRemovingNonStrings_basicCases() { + // Empty dictionary + var dictionary = [:] as [AnyHashable: Any] + var expected = [:] as [AnyHashable: Any] + var result = dictionary.stp_dictionaryByRemovingNonStrings() + XCTAssertEqual(result as NSDictionary, expected as NSDictionary) + + // Regular case + dictionary = + [ + "user": "user_123", + "nicknames": "John, Johnny", + ] + expected = + [ + "user": "user_123", + "nicknames": "John, Johnny", + ] + result = dictionary.stp_dictionaryByRemovingNonStrings() + XCTAssertEqual(result as NSDictionary, expected as NSDictionary) + + // Strips non-NSString keys and values + dictionary = + [ + "user": "user_123", + "nicknames": "John, Johnny", + "profiles": NSNull(), + NSNull(): "San Francisco, CA", + "age": NSNumber(value: 21), + NSNumber(value: 21): "age", + "fees": [ + "plan": "monthly" + ], + "visits": ["january", "february"], + ] + expected = + [ + "user": "user_123", + "nicknames": "John, Johnny", + ] + result = dictionary.stp_dictionaryByRemovingNonStrings() + XCTAssertEqual(result as NSDictionary, expected as NSDictionary) + + // Strips non-NSString keys and values + dictionary = + [ + "user": "user_123", + "nicknames": "John, Johnny", + "profiles": NSNull(), + NSNull(): NSNull(), + "age": NSNumber(value: 21), + NSNumber(value: 21): NSNumber(value: 21), + "fees": [ + "plan": "monthly" + ], + "visits": ["january", "february"], + ] + expected = + [ + "user": "user_123", + "nicknames": "John, Johnny", + ] + result = dictionary.stp_dictionaryByRemovingNonStrings() + XCTAssertEqual(result as NSDictionary, expected as NSDictionary) + } + + // MARK: - Getters + func testArrayForKey() { + let dict = + [ + "a": ["foo"] + ] as [AnyHashable: Any] + + XCTAssertEqual(dict.stp_array(forKey: "a") as! [String], ["foo"]) + XCTAssertNil(dict.stp_array(forKey: "b")) + } + + func testBoolForKey() { + let dict = + [ + "a": NSNumber(value: 1), + "b": NSNumber(value: 0), + "c": "true", + "d": "false", + "e": "1", + "f": "foo", + ] as [AnyHashable: Any] + + XCTAssertTrue(dict.stp_bool(forKey: "a", or: false)) + XCTAssertFalse(dict.stp_bool(forKey: "b", or: true)) + XCTAssertTrue(dict.stp_bool(forKey: "c", or: false)) + XCTAssertFalse(dict.stp_bool(forKey: "d", or: true)) + XCTAssertTrue(dict.stp_bool(forKey: "e", or: false)) + XCTAssertFalse(dict.stp_bool(forKey: "f", or: false)) + } + + func testIntForKey() { + let dict = + [ + "a": NSNumber(value: 1), + "b": NSNumber(value: -1), + "c": "1", + "d": "-1", + "e": "10.0", + "f": "10.5", + "g": NSNumber(value: 10.0), + "h": NSNumber(value: 10.5), + "i": "foo", + ] as [AnyHashable: Any] + + XCTAssertEqual(dict.stp_int(forKey: "a", or: 0), 1) + XCTAssertEqual(dict.stp_int(forKey: "b", or: 0), -1) + XCTAssertEqual(dict.stp_int(forKey: "c", or: 0), 1) + XCTAssertEqual(dict.stp_int(forKey: "d", or: 0), -1) + XCTAssertEqual(dict.stp_int(forKey: "e", or: 0), 10) + XCTAssertEqual(dict.stp_int(forKey: "f", or: 0), 10) + XCTAssertEqual(dict.stp_int(forKey: "g", or: 0), 10) + XCTAssertEqual(dict.stp_int(forKey: "h", or: 0), 10) + XCTAssertEqual(dict.stp_int(forKey: "i", or: 0), 0) + } + + func testDateForKey() { + let dict = + [ + "a": NSNumber(value: 0), + "b": "0", + ] as [AnyHashable: Any] + let expectedDate = Date(timeIntervalSince1970: 0) + + XCTAssertEqual(dict.stp_date(forKey: "a"), expectedDate) + XCTAssertEqual(dict.stp_date(forKey: "b"), expectedDate) + XCTAssertNil(dict.stp_date(forKey: "c")) + } + + func testDictionaryForKey() { + let dict = + [ + "a": [ + "foo": "bar" + ], + ] as [AnyHashable: Any] + + XCTAssertEqual( + dict.stp_dictionary(forKey: "a")! as NSDictionary, + [ + "foo": "bar" + ] as NSDictionary + ) + XCTAssertNil(dict.stp_dictionary(forKey: "b")) + } + + func testNumberForKey() { + let dict = + [ + "a": NSNumber(value: 1) + ] as [AnyHashable: Any] + + XCTAssertEqual(dict.stp_number(forKey: "a"), NSNumber(value: 1)) + XCTAssertNil(dict.stp_number(forKey: "b")) + } + + func testStringForKey() { + let dict = + [ + "a": "foo" + ] as [AnyHashable: Any] + XCTAssertEqual(dict.stp_string(forKey: "a"), "foo") + XCTAssertNil(dict.stp_string(forKey: "b")) + } + + func testURLForKey() { + let dict = + [ + "a": "https://example.com", + "b": "not a url", + ] as [AnyHashable: Any] + XCTAssertEqual(dict.stp_url(forKey: "a"), URL(string: "https://example.com")) + XCTAssertNil(dict.stp_url(forKey: "b")) + XCTAssertNil(dict.stp_url(forKey: "c")) + } +} diff --git a/Stripe/StripeiOSTests/NSLocale+STPSwizzling.swift b/Stripe/StripeiOSTests/NSLocale+STPSwizzling.swift new file mode 100644 index 00000000..bff5b323 --- /dev/null +++ b/Stripe/StripeiOSTests/NSLocale+STPSwizzling.swift @@ -0,0 +1,96 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +import Foundation +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +// +// NSLocale+STPSwizzling.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 7/17/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import ObjectiveC + +class STPLocaleSwizzling { + static var stpLocaleOverride: NSLocale? + static var hasSwizzled: Bool = false + +} + +extension NSLocale { + class func swizzleIfNeeded() { + if !STPLocaleSwizzling.hasSwizzled { + self.stp_swizzleClassMethod(#selector(getter: current), withReplacement: #selector(stp_current)) + self.stp_swizzleClassMethod(#selector(getter: autoupdatingCurrent), withReplacement: #selector(stp_autoUpdatingCurrent)) + self.stp_swizzleClassMethod(#selector(getter: system), withReplacement: #selector(stp_system)) + STPLocaleSwizzling.hasSwizzled = true + } + } + + class func stp_withLocale(as locale: NSLocale?, perform block: @escaping () -> Void) { + swizzleIfNeeded() + let currentLocale = NSLocale.current as NSLocale + self.stp_setCurrentLocale(locale) + block() + self.stp_resetCurrentLocale() + assert((currentLocale as Locale == NSLocale.current), "Failed to reset locale.") + } + + class func stp_setCurrentLocale(_ locale: NSLocale?) { + swizzleIfNeeded() + STPLocaleSwizzling.stpLocaleOverride = locale + } + + class func stp_resetCurrentLocale() { + swizzleIfNeeded() + self.stp_setCurrentLocale(nil) + } + + @objc class func stp_current() -> NSLocale { + return STPLocaleSwizzling.stpLocaleOverride ?? self.stp_current() + } + + @objc class func stp_autoUpdatingCurrent() -> NSLocale { + return STPLocaleSwizzling.stpLocaleOverride ?? self.stp_autoUpdatingCurrent() + } + + @objc class func stp_system() -> NSLocale { + return STPLocaleSwizzling.stpLocaleOverride ?? self.stp_system() + } +} + +extension NSObject { + class func stp_swizzleClassMethod(_ original: Selector, withReplacement replacement: Selector) { + let `class`: AnyClass? = object_getClass(self) + let originalMethod = class_getClassMethod(self, original) + let replacementMethod = class_getClassMethod(self, replacement) + + var addedMethod = false + if let replacementMethod { + addedMethod = class_addMethod( + `class`, + original, + method_getImplementation(replacementMethod), + method_getTypeEncoding(replacementMethod)) + } + if addedMethod { + if let originalMethod { + class_replaceMethod( + `class`, + replacement, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)) + } + } else { + if let originalMethod, let replacementMethod { + method_exchangeImplementations(originalMethod, replacementMethod) + } + } + } +} diff --git a/Stripe/StripeiOSTests/NSString+StripeTest.swift b/Stripe/StripeiOSTests/NSString+StripeTest.swift new file mode 100644 index 00000000..eeba071c --- /dev/null +++ b/Stripe/StripeiOSTests/NSString+StripeTest.swift @@ -0,0 +1,104 @@ +// +// NSString+StripeTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 3/22/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +import XCTest + +class NSString_StripeTest: XCTestCase { + + func testIsBlank() { + XCTAssertTrue("".isBlank) + XCTAssertTrue(" ".isBlank) + XCTAssertTrue("\t\t\t".isBlank) + XCTAssertFalse("a".isBlank) + XCTAssertFalse(" a ".isBlank) + } + + func testSafeSubstringToIndex() { + XCTAssertEqual("foo".stp_safeSubstring(to: 0), "") + XCTAssertEqual("foo".stp_safeSubstring(to: 500), "foo") + XCTAssertEqual("foo".stp_safeSubstring(to: 1), "f") + XCTAssertEqual("foo".stp_safeSubstring(to: -1), "") + XCTAssertEqual("foo".stp_safeSubstring(to: -100), "") + XCTAssertEqual("".stp_safeSubstring(to: 0), "") + XCTAssertEqual("".stp_safeSubstring(to: 1), "") + } + + func testSafeSubstringFromIndex() { + XCTAssertEqual("foo".stp_safeSubstring(from: 0), "foo") + XCTAssertEqual("foo".stp_safeSubstring(from: 1), "oo") + XCTAssertEqual("foo".stp_safeSubstring(from: 3), "") + XCTAssertEqual("foo".stp_safeSubstring(from: -1), "foo") + XCTAssertEqual("foo".stp_safeSubstring(from: -100), "foo") + XCTAssertEqual("".stp_safeSubstring(from: 0), "") + XCTAssertEqual("".stp_safeSubstring(from: 1), "") + } + + func testStringByRemovingSuffix() { + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: "bar"), "foo") + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: "baz"), "foobar") + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: nil), "foobar") + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: "foobar"), "") + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: ""), "foobar") + XCTAssertEqual("foobar".stp_string(byRemovingSuffix: "oba"), "foobar") + + XCTAssertEqual("foobar☺¿".stp_string(byRemovingSuffix: "bar☺¿"), "foo") + XCTAssertEqual("foobar☺¿".stp_string(byRemovingSuffix: "bar¿"), "foobar☺¿") + + XCTAssertEqual("foobar\u{202C}".stp_string(byRemovingSuffix: "bar"), "foobar\u{202C}") + XCTAssertEqual("foobar\u{202C}".stp_string(byRemovingSuffix: "bar\u{202C}"), "foo") + + // e + \u0041 => é + XCTAssertEqual("foobare\u{0301}".stp_string(byRemovingSuffix: "bare"), "foobare\u{0301}") + XCTAssertEqual("foobare\u{0301}".stp_string(byRemovingSuffix: "bare\u{0301}"), "foo") + XCTAssertEqual("foobare".stp_string(byRemovingSuffix: "bare\u{0301}"), "foobare") + + } + + func testLocalizedAmountDisplayString() { + XCTAssertEqual(String.localizedAmountDisplayString(for: 1099, currency: "USD"), "$10.99") + XCTAssertEqual( + String.localizedAmountDisplayString( + for: 1099, + currency: "USD", + locale: Locale(identifier: "fr_FR") + ), + "10,99 $US" + ) + XCTAssertEqual( + String.localizedAmountDisplayString( + for: 1099, + currency: "USD", + locale: Locale(identifier: "zh_HANT") + ), + "US$10.99" + ) + + XCTAssertEqual( + String.localizedAmountDisplayString( + for: 1099, + currency: "ZZZ", + locale: Locale(identifier: "z") + ), + "ZZZ 10.99" + ) + + XCTAssertEqual( + String.localizedAmountDisplayString( + for: 1000, + currency: "IDR", + locale: Locale(identifier: "en_US") + ), + "IDR 10" + ) + } +} diff --git a/Stripe/StripeiOSTests/NSURLComponents_StripeTest.swift b/Stripe/StripeiOSTests/NSURLComponents_StripeTest.swift new file mode 100644 index 00000000..86da11a4 --- /dev/null +++ b/Stripe/StripeiOSTests/NSURLComponents_StripeTest.swift @@ -0,0 +1,40 @@ +// +// NSURLComponents_StripeTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/24/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +import XCTest + +class NSURLComponents_StripeTest: XCTestCase { + func testCaseInsensitiveSchemeComparison() { + let lhs = NSURLComponents(string: "com.bar.foo://host")! + let rhs = NSURLComponents(string: "COM.BAR.FOO://HOST")! + XCTAssert(lhs.stp_matchesURLComponents(lhs)) // sanity + XCTAssert(lhs.stp_matchesURLComponents(rhs)) + XCTAssert(rhs.stp_matchesURLComponents(lhs)) + } + + func testMatchesURLsWithQueryString() { + // e.g. STPSourceFunctionalTest passes "https://shop.example.com/crtABC" for the return_url, + // but the Source object returned by the API comes has "https://shop.example.com/crtABC?redirect_merchant_name=xctest" + let expectedComponents = NSURLComponents( + string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest" + )! + let components = NSURLComponents(string: "https://shop.example.com/crtABC")! + XCTAssertTrue(components.stp_matchesURLComponents(expectedComponents)) + } + + func testMatchesURLWithNilParameters() { + let nil1 = NSURLComponents(string: "")! + let nil2 = NSURLComponents(string: "")! + XCTAssert(nil1.stp_matchesURLComponents(nil2)) + } +} diff --git a/Stripe/StripeiOSTests/OneTimeCodeTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/OneTimeCodeTextFieldSnapshotTests.swift new file mode 100644 index 00000000..a965a083 --- /dev/null +++ b/Stripe/StripeiOSTests/OneTimeCodeTextFieldSnapshotTests.swift @@ -0,0 +1,63 @@ +// +// OneTimeCodeTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 11/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class OneTimeCodeTextFieldSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let field = OneTimeCodeTextField( + configuration: OneTimeCodeTextField.Configuration( + numberOfDigits: 6 + ), + theme: .default + ) + verify(field) + } + + func testFilled() { + let field = OneTimeCodeTextField( + configuration: OneTimeCodeTextField.Configuration( + numberOfDigits: 6 + ), + theme: .default + ) + field.value = "123456" + verify(field) + } + + func testDisabled() { + let field = OneTimeCodeTextField( + configuration: OneTimeCodeTextField.Configuration( + numberOfDigits: 6 + ), + theme: .default + ) + field.value = "123456" + field.isEnabled = false + verify(field) + } + + func verify( + _ view: UIView, + file: StaticString = #filePath, + line: UInt = #line + ) { + view.autosizeHeight(width: 300) + STPSnapshotVerifyView(view, file: file, line: line) + } +} diff --git a/Stripe/StripeiOSTests/OneTimeCodeTextFieldTests.swift b/Stripe/StripeiOSTests/OneTimeCodeTextFieldTests.swift new file mode 100644 index 00000000..51002390 --- /dev/null +++ b/Stripe/StripeiOSTests/OneTimeCodeTextFieldTests.swift @@ -0,0 +1,327 @@ +// +// OneTimeCodeTextFieldTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 11/5/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class OneTimeCodeTextFieldTests: XCTestCase { + + func test_isComplete() { + let field = makeSUT() + + field.value = "12345" + XCTAssertFalse(field.isComplete) + + field.value = "123456" + XCTAssertTrue(field.isComplete) + } + + func test_insertText() { + let field = makeSUT() + + field.insertText("1") + XCTAssertEqual(field.value, "1") + + field.insertText("2") + XCTAssertEqual(field.value, "12") + XCTAssertEqual(field.selectedTextRange?.start, field.endOfDocument) + XCTAssertEqual(field.selectedTextRange?.end, field.endOfDocument) + } + + func test_insertText_shouldNotInsertBeyondNumberOfDigits() { + let field = makeSUT(numberOfDigits: 4) + field.value = "123" + field.selectedTextRange = field.textRange( + from: field.endOfDocument, + to: field.endOfDocument + ) + field.insertText("45") + XCTAssertEqual(field.value, "1234") + } + + func test_insertText_shouldIgnoreInvalidCharacters() { + let field = makeSUT() + field.insertText("123-456") + XCTAssertEqual(field.value, "123456") + } + + func test_deleteBackward() throws { + let field = makeSUT(value: "12") + + field.selectedTextRange = field.textRange( + from: try XCTUnwrap(field.position(from: field.endOfDocument, in: .left, offset: 1)), + to: field.endOfDocument + ) + field.deleteBackward() + XCTAssertEqual(field.value, "1") + + field.selectedTextRange = field.textRange( + from: try XCTUnwrap(field.position(from: field.endOfDocument, in: .left, offset: 1)), + to: field.endOfDocument + ) + field.deleteBackward() + XCTAssertEqual(field.value, "") + + // Delete while empty + field.selectedTextRange = field.textRange( + from: field.beginningOfDocument, + to: field.endOfDocument + ) + field.deleteBackward() + XCTAssertEqual(field.value, "") + } + +// TODO(RUN_MOBILESDK-1848): This test is broken on iOS 16, as it invokes the pasteboard permission dialog +// func test_paste() { +// UIPasteboard.general.string = "123-456" +// +// let field = makeSUT() +// field.paste(nil) +// XCTAssertEqual(field.value, "123456") +// } + + // MARK: - UITextInput conformance + + func test_beginningOfDocument() throws { + let field = makeSUT(value: "123456") + + let position = try XCTUnwrap( + field.beginningOfDocument as? OneTimeCodeTextField.TextPosition + ) + XCTAssertEqual(position.index, 0) + } + + func test_endOfDocument() throws { + let field = makeSUT(value: "123456") + + let position = try XCTUnwrap(field.endOfDocument as? OneTimeCodeTextField.TextPosition) + XCTAssertEqual(position.index, 6) + } + + func test_textInRange() { + let field = makeSUT(value: "123456") + + let result = field.text( + in: OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(3) + ) + ) + + XCTAssertEqual(result, "123") + } + + func test_textInRange_emptyRange() { + let field = makeSUT(value: "123456") + + let result = field.text( + in: OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(0) + ) + ) + + XCTAssertNil(result) + } + + func test_positionFromOffset() { + let field = makeSUT(value: "123456") + + XCTAssertEqual( + field.position(from: field.beginningOfDocument, offset: 3), + OneTimeCodeTextField.TextPosition(3) + ) + + XCTAssertNil( + field.position(from: field.beginningOfDocument, offset: 10), + "Should return nil when offsetting to an out of bounds position" + ) + + XCTAssertNil( + field.position(from: field.beginningOfDocument, offset: -1), + "Should return nil when offsetting to an out of bounds position" + ) + } + + func test_positionInDirection() { + let field = makeSUT(value: "123456") + + XCTAssertEqual( + field.position(from: field.beginningOfDocument, in: .right, offset: 1), + OneTimeCodeTextField.TextPosition(1) + ) + + XCTAssertEqual( + field.position(from: field.endOfDocument, in: .left, offset: 1), + OneTimeCodeTextField.TextPosition(5) + ) + + // Y axis + XCTAssertEqual( + field.position(from: field.beginningOfDocument, in: .up, offset: 1), + field.beginningOfDocument + ) + + XCTAssertEqual( + field.position(from: field.beginningOfDocument, in: .down, offset: 1), + field.endOfDocument + ) + } + + func test_compare() { + let field = makeSUT(value: "123456") + + XCTAssertEqual( + field.compare(field.beginningOfDocument, to: field.beginningOfDocument), + .orderedSame + ) + + XCTAssertEqual( + field.compare(field.beginningOfDocument, to: field.endOfDocument), + .orderedAscending + ) + + XCTAssertEqual( + field.compare(field.endOfDocument, to: field.beginningOfDocument), + .orderedDescending + ) + } + + func test_offsetToPosition() { + let field = makeSUT(value: "123456") + + XCTAssertEqual(field.offset(from: field.beginningOfDocument, to: field.endOfDocument), 6) + XCTAssertEqual(field.offset(from: field.endOfDocument, to: field.beginningOfDocument), -6) + XCTAssertEqual( + field.offset(from: field.beginningOfDocument, to: OneTimeCodeTextField.TextPosition(3)), + 3 + ) + } + + func test_positionFarthestInDirection() throws { + let field = makeSUT(value: "123456") + + let position = try XCTUnwrap( + OneTimeCodeTextField.TextRange( + start: field.beginningOfDocument, + end: field.endOfDocument + ) + ) + + XCTAssertEqual( + field.position(within: position, farthestIn: .left), + field.beginningOfDocument + ) + + XCTAssertEqual( + field.position(within: position, farthestIn: .right), + field.endOfDocument + ) + + // Y axis + XCTAssertEqual( + field.position(within: position, farthestIn: .up), + field.beginningOfDocument + ) + + XCTAssertEqual( + field.position(within: position, farthestIn: .down), + field.endOfDocument + ) + } + + func test_characterRangeByExtendingInDirection() throws { + let field = makeSUT(value: "123456") + + let position = OneTimeCodeTextField.TextPosition(3) + + XCTAssertEqual( + field.characterRange(byExtending: position, in: .left), + OneTimeCodeTextField.TextRange(start: field.beginningOfDocument, end: position) + ) + + XCTAssertEqual( + field.characterRange(byExtending: position, in: .right), + OneTimeCodeTextField.TextRange(start: position, end: field.endOfDocument) + ) + + // Y axis + XCTAssertNil(field.characterRange(byExtending: position, in: .up)) + XCTAssertNil(field.characterRange(byExtending: position, in: .down)) + } + + func test_firstRectForRange_singleDigit() { + let sut = makeSUT(value: "123456") + + // A [0,1] text range + let range = OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(1) + ) + let rect = sut.firstRect(for: range) + XCTAssertEqual(rect.minX, 0, accuracy: 0.2) + XCTAssertEqual(rect.minY, 0, accuracy: 0.2) + XCTAssertEqual(rect.width, 46.0, accuracy: 0.2) + XCTAssertEqual(rect.height, 60, accuracy: 0.2) + } + + func test_firstRectForRange_multipleDigits() { + let sut = makeSUT(value: "123456") + + // A [0,3] Text range + let range = OneTimeCodeTextField.TextRange( + start: OneTimeCodeTextField.TextPosition(0), + end: OneTimeCodeTextField.TextPosition(3) + ) + let rect = sut.firstRect(for: range) + XCTAssertEqual(rect.minX, 0, accuracy: 0.2) + XCTAssertEqual(rect.minY, 0, accuracy: 0.2) + XCTAssertEqual(rect.width, 150, accuracy: 0.2) + XCTAssertEqual(rect.height, 60, accuracy: 0.2) + } + + func test_caretRectForPosition() { + let sut = makeSUT() + let frame = sut.caretRect(for: OneTimeCodeTextField.TextPosition(1)) + XCTAssertEqual(frame.minX, 74, accuracy: 0.2) + XCTAssertEqual(frame.minY, 20.47, accuracy: 0.2) + XCTAssertEqual(frame.width, 2, accuracy: 0.2) + XCTAssertEqual(frame.height, 19.04, accuracy: 0.2) + } + +} + +// MARK: - Factory methods + +extension OneTimeCodeTextFieldTests { + + fileprivate func makeSUT(numberOfDigits: Int = 6) -> OneTimeCodeTextField { + let sut = OneTimeCodeTextField( + configuration: OneTimeCodeTextField.Configuration( + numberOfDigits: numberOfDigits + ), + theme: .default + ) + sut.frame = CGRect(x: 0, y: 0, width: 320, height: 60) + sut.layoutIfNeeded() + return sut + } + + fileprivate func makeSUT(value: String) -> OneTimeCodeTextField { + let sut = makeSUT() + sut.value = value + return sut + } + +} diff --git a/Stripe/StripeiOSTests/OperationDebouncerTests.swift b/Stripe/StripeiOSTests/OperationDebouncerTests.swift new file mode 100644 index 00000000..7a6f3ae0 --- /dev/null +++ b/Stripe/StripeiOSTests/OperationDebouncerTests.swift @@ -0,0 +1,45 @@ +// +// OperationDebouncerTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 1/23/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class OperationDebouncerTests: XCTestCase { + + func testEnqueueShouldDebounce() { + let sut = makeSUT() + + let expectation = self.expectation(description: "Should execute the block just once") + expectation.assertForOverFulfill = true + + // Call `enqueue(block:)` 3 times + for _ in 0..<3 { + sut.enqueue { + expectation.fulfill() + } + } + + Thread.sleep(forTimeInterval: 1) + + wait(for: [expectation], timeout: 1) + } + +} + +extension OperationDebouncerTests { + + func makeSUT() -> OperationDebouncer { + return OperationDebouncer(debounceTime: .milliseconds(500)) + } + +} diff --git a/Stripe/StripeiOSTests/PKPayment+StripeTest.swift b/Stripe/StripeiOSTests/PKPayment+StripeTest.swift new file mode 100644 index 00000000..c71eee52 --- /dev/null +++ b/Stripe/StripeiOSTests/PKPayment+StripeTest.swift @@ -0,0 +1,36 @@ +// +// PKPayment+StripeTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 7/6/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +import PassKit +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class PKPayment_StripeTest: XCTestCase { + func testIsSimulated() { + let payment = PKPayment() + let paymentToken = PKPaymentToken() + + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wundeclared-selector" + paymentToken.perform(Selector(("setTransactionIdentifier:")), with: "Simulated Identifier") + payment.perform(#selector(setter: STPPaymentMethodCardParams.token), with: paymentToken) + // #pragma clang diagnostic pop + + XCTAssertTrue(payment.stp_isSimulated()) + } + + func testTransactionIdentifier() { + let identifier = PKPayment.stp_testTransactionIdentifier() + XCTAssertTrue(identifier.contains("ApplePayStubs~4242424242424242~0~USD~")) + } +} diff --git a/Stripe/StripeiOSTests/PayWithLinkButtonSnapshotTests.swift b/Stripe/StripeiOSTests/PayWithLinkButtonSnapshotTests.swift new file mode 100644 index 00000000..7792c569 --- /dev/null +++ b/Stripe/StripeiOSTests/PayWithLinkButtonSnapshotTests.swift @@ -0,0 +1,102 @@ +// +// PayWithLinkButtonSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 11/17/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class PayWithLinkButtonSnapshotTests: STPSnapshotTestCase { + + private let emailAddress = "customer@example.com" + private let longEmailAddress = "long.customer.name@example.com" + + func testDefault() { + let sut = makeSUT() + sut.linkAccount = makeAccountStub(email: emailAddress, isRegistered: false) + verify(sut) + + sut.isHighlighted = true + verify(sut, identifier: "Highlighted") + } + + func testDefault_rounded() { + let sut = makeSUT() + sut.cornerRadius = 16 + sut.linkAccount = makeAccountStub(email: emailAddress, isRegistered: false) + verify(sut) + } + + func testDisabled() { + let sut = makeSUT() + sut.isEnabled = false + verify(sut) + } + + func testRegistered() { + let sut = makeSUT() + sut.linkAccount = makeAccountStub(email: emailAddress, isRegistered: true) + verify(sut) + } + + func testRegistered_rounded() { + let sut = makeSUT() + sut.cornerRadius = 16 + sut.linkAccount = makeAccountStub(email: emailAddress, isRegistered: true) + verify(sut) + } + + func testRegistered_square() { + let sut = makeSUT() + sut.cornerRadius = 0 + sut.linkAccount = makeAccountStub(email: emailAddress, isRegistered: true) + verify(sut) + } + + func testRegistered_withLongEmailAddress() { + let sut = makeSUT() + sut.linkAccount = makeAccountStub(email: longEmailAddress, isRegistered: true) + verify(sut) + } + + func verify( + _ sut: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + sut.autosizeHeight(width: 300) + STPSnapshotVerifyView(sut, identifier: identifier, file: file, line: line) + } + +} + +extension PayWithLinkButtonSnapshotTests { + + fileprivate struct LinkAccountStub: PaymentSheetLinkAccountInfoProtocol { + let email: String + let isRegistered: Bool + } + + fileprivate func makeAccountStub(email: String, isRegistered: Bool) -> LinkAccountStub { + return LinkAccountStub( + email: email, + isRegistered: isRegistered + ) + } + + fileprivate func makeSUT() -> PayWithLinkButton { + return PayWithLinkButton() + } + +} diff --git a/Stripe/StripeiOSTests/PaymentAnalyticTest.swift b/Stripe/StripeiOSTests/PaymentAnalyticTest.swift new file mode 100644 index 00000000..b0ead140 --- /dev/null +++ b/Stripe/StripeiOSTests/PaymentAnalyticTest.swift @@ -0,0 +1,30 @@ +// +// PaymentAnalyticTest.swift +// StripeiOS Tests +// +// Created by Mel Ludowise on 5/26/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +final class PaymentAnalyticTest: XCTestCase { + + func testParams() { + let analytic = GenericPaymentAnalytic( + event: .cardScanCancelled, + paymentConfiguration: STPPaymentConfiguration(), + additionalParams: [:] + ) + + XCTAssertNotNil(analytic.params["apple_pay_enabled"] as? NSNumber) + XCTAssertNotNil(analytic.params["ocr_type"] as? String) + } +} diff --git a/Stripe/StripeiOSTests/PaymentTypeCellSnapshotTests.swift b/Stripe/StripeiOSTests/PaymentTypeCellSnapshotTests.swift new file mode 100644 index 00000000..aa6d1bf5 --- /dev/null +++ b/Stripe/StripeiOSTests/PaymentTypeCellSnapshotTests.swift @@ -0,0 +1,76 @@ +// +// PaymentTypeCellSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 12/17/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class PaymentTypeCellSnapshotTests: STPSnapshotTestCase { + + func testCardUnselected() { + let cell = PaymentMethodTypeCollectionView.PaymentTypeCell() + cell.paymentMethodType = .stripe(.card) + cell.frame = CGRect( + origin: .zero, + size: CGSize( + width: PaymentMethodTypeCollectionView.cellHeight, + height: PaymentMethodTypeCollectionView.cellHeight + ) + ) + STPSnapshotVerifyView(cell) + } + + func testCardSelected() { + let cell = PaymentMethodTypeCollectionView.PaymentTypeCell() + cell.paymentMethodType = .stripe(.card) + cell.frame = CGRect( + origin: .zero, + size: CGSize( + width: PaymentMethodTypeCollectionView.cellHeight, + height: PaymentMethodTypeCollectionView.cellHeight + ) + ) + cell.isSelected = true + STPSnapshotVerifyView(cell) + } + + func testCardUnselected_forceDarkMode() { + let cell = PaymentMethodTypeCollectionView.PaymentTypeCell() + cell.overrideUserInterfaceStyle = .dark + cell.paymentMethodType = .stripe(.card) + cell.frame = CGRect( + origin: .zero, + size: CGSize( + width: PaymentMethodTypeCollectionView.cellHeight, + height: PaymentMethodTypeCollectionView.cellHeight + ) + ) + STPSnapshotVerifyView(cell) + } + + func testCardSelected_forceDarkMode() { + let cell = PaymentMethodTypeCollectionView.PaymentTypeCell() + cell.appearance.colors.componentBackground = .black + cell.overrideUserInterfaceStyle = .dark + cell.paymentMethodType = .stripe(.card) + cell.frame = CGRect( + origin: .zero, + size: CGSize( + width: PaymentMethodTypeCollectionView.cellHeight, + height: PaymentMethodTypeCollectionView.cellHeight + ) + ) + cell.isSelected = true + STPSnapshotVerifyView(cell) + } +} diff --git a/Stripe/StripeiOSTests/Resources/Images.xcassets/Contents.json b/Stripe/StripeiOSTests/Resources/Images.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Stripe/StripeiOSTests/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Stripe/StripeiOSTests/Resources/MockFiles/paymentIntentResponse.json b/Stripe/StripeiOSTests/Resources/MockFiles/paymentIntentResponse.json new file mode 100644 index 00000000..53200cdf --- /dev/null +++ b/Stripe/StripeiOSTests/Resources/MockFiles/paymentIntentResponse.json @@ -0,0 +1,53 @@ +{ + "id": "pi_3LN3c2L123456789", + "object": "payment_intent", + "amount": 5099, + "amount_details": { + "tip": {} + }, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "client_secret": "pi_123456_secret_654321", + "confirmation_method": "automatic", + "created": 1658187870, + "currency": "usd", + "description": null, + "last_payment_error": null, + "livemode": false, + "next_action": , + "payment_method": , + "payment_method_options": { + "us_bank_account": { + "verification_method": "automatic" + } + }, + "payment_method_types": [ + "card", + "afterpay_clearpay", + "klarna", + "us_bank_account", + "affirm", + "blik" + ], + "processing": null, + "receipt_email": null, + "setup_future_usage": null, + "shipping": { + "address": { + "city": "San Francisco", + "country": "US", + "line1": "510 Townsend St", + "line2": null, + "postal_code": "94102", + "state": "California" + }, + "carrier": null, + "name": "John Doe", + "phone": null, + "tracking_number": null + }, + "source": null, + "status": +} diff --git a/Stripe/StripeiOSTests/Resources/stp_test_upload_image.jpeg b/Stripe/StripeiOSTests/Resources/stp_test_upload_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bb7e0e73e748c3d72c5609783209296b3907eaac GIT binary patch literal 3340 zcmeHKTWl0%82$da?4|n9g@TAN4pSl=znF(%eKMk5F+`oQ?jv}_Zj3GvlEJC`#v-+%r& z=ii5KytCdH(Abyl%>p3+J@^A}5{|5L^+AAK4weEy1Xwa3SY%iNedLxJ?$lPFtSukybYZI0UG9(kyawQ=vlFUi1 zij-2Mb^v;u03D1*y>Y6hOvdRM?oZ)OMt7GS$5KSms0eD&%xglyEQ_vciIR{IVMT{) zsfA(9;quy$Zlw92j~(YZy_n|Lx8)K!E2EY4o-tcnJ9g(lVQjdND)JpGxfQPBmaVep zsGM6KF)E6i<^ypBxqn;axuC=uPV*abtGSG6YiOudNboq?E)FXF-P!4}c$Ma-U$R=Q z3RPJ!?IBT0rBY%dDJGL~tcX`e4M%n3My2_hLbp~a*t+HDros7&YTn%JqXph? zP4*wX`-PU0u`zMe9x%<3H2*J)Fjbi|i~8Vbx9VtV-d~k?LXJ!Fz_nH3MRX=kTk~lx zqm^78^#tk()Dx&DP*0$q!2d6SnJ!B+u(hgU4+Y*&(7w84I%cJ0TAVB-puMX%$M{Vm zBH%3nZ9H@9V-t_V0p|Pz(Oo}Y>AK%L0kH^nj)W#WP%%Qsh<6HZ!)B5t0fJ$8AQa88 zp>QPHa03<`ZUlrX@O!{@-n*jWpt&d}^NYM9^Dl-#m&Po%kP|H(Q1r!Op& z^Y+$g!`z$Z-8{eL_C<@AENyLDw!A&1WV%;n@9636TRU*qy7e3G-dHGVgF_|#o{F>i z-l{t~_TWPgZ`=OJqdT5>@~Nkvd3M+C=k`AT!iz7x{K~8Q4jepm__f!Mym9o+w@$tN z&b#lu|G|eJo%#5aPtQ)AoBZ;tufO^ByYJ8caPgN*mw)~3_bY$=>HES~cghwXr=0!G zR}6ho3_~+v-xr~(zVR5tE|5aAI@g5NO|$vrmPk|A&OQ52Mw{jS3v=@J>4v!rTPIsC z`qlzx*Jo_&e{xncHsk9v$l&*d4+=M$qHw8b1cMe3Y={jInnpN4#7DFS4@Ck6iehk! khS*T;*>w}|bNpf;-Z{9DCirKfW6%LtuP)xZ1H6g905i&OlmGw# literal 0 HcmV?d00001 diff --git a/Stripe/StripeiOSTests/RotatingCardBrandsViewSnapshotTests.swift b/Stripe/StripeiOSTests/RotatingCardBrandsViewSnapshotTests.swift new file mode 100644 index 00000000..0f68bef7 --- /dev/null +++ b/Stripe/StripeiOSTests/RotatingCardBrandsViewSnapshotTests.swift @@ -0,0 +1,32 @@ +// +// RotatingCardBrandsViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 6/27/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +@_spi(STP) import StripePayments +import XCTest + +@testable import Stripe +@testable @_spi(STP) import StripePaymentSheet + +class RotatingCardBrandsViewSnapshotTests: STPSnapshotTestCase { + func testAllCardBrands() { + let rotatingCardBrandsView = RotatingCardBrandsView() + rotatingCardBrandsView.cardBrands = RotatingCardBrandsView.orderedCardBrands(from: STPCardBrand.allCases) + rotatingCardBrandsView.autosizeHeight(width: 140) + STPSnapshotVerifyView(rotatingCardBrandsView) + } + + func testSingleCardBrand() { + let rotatingCardBrandsView = RotatingCardBrandsView() + rotatingCardBrandsView.cardBrands = [.visa] + rotatingCardBrandsView.autosizeHeight(width: 140) + STPSnapshotVerifyView(rotatingCardBrandsView) + } + +} diff --git a/Stripe/StripeiOSTests/RotatingCardBrandsViewTests.swift b/Stripe/StripeiOSTests/RotatingCardBrandsViewTests.swift new file mode 100644 index 00000000..b52ff221 --- /dev/null +++ b/Stripe/StripeiOSTests/RotatingCardBrandsViewTests.swift @@ -0,0 +1,42 @@ +// +// RotatingCardBrandsViewTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 6/27/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripePayments +import XCTest + +@testable import Stripe +@testable @_spi(STP) import StripePaymentSheet + +class RotatingCardBrandsViewTests: XCTestCase { + + func testOrdering() { + XCTAssertEqual([.visa, + .mastercard, + .amex, + .discover, + .dinersClub, + .JCB, + .unionPay, + ], RotatingCardBrandsView.orderedCardBrands(from: STPCardBrand.allCases)) + } + + func testRotatesOnMoreThreeOrMoreBrands() { + let rotatingCardBrandsView = RotatingCardBrandsView() + rotatingCardBrandsView.cardBrands = [.visa] + XCTAssertTrue(rotatingCardBrandsView.rotatingCardBrandView.isHidden) + rotatingCardBrandsView.cardBrands = [.visa, .mastercard] + XCTAssertTrue(rotatingCardBrandsView.rotatingCardBrandView.isHidden) + rotatingCardBrandsView.cardBrands = [.visa, .mastercard, .amex] + XCTAssertTrue(rotatingCardBrandsView.rotatingCardBrandView.isHidden) + rotatingCardBrandsView.cardBrands = [.visa, .mastercard, .amex, .dinersClub] + XCTAssertFalse(rotatingCardBrandsView.rotatingCardBrandView.isHidden) + rotatingCardBrandsView.cardBrands = [.visa, .mastercard, .amex, .dinersClub, .JCB] + XCTAssertFalse(rotatingCardBrandsView.rotatingCardBrandView.isHidden) + } + +} diff --git a/Stripe/StripeiOSTests/STPAPIClient+LinkAccountSessionTest.swift b/Stripe/StripeiOSTests/STPAPIClient+LinkAccountSessionTest.swift new file mode 100644 index 00000000..42232763 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAPIClient+LinkAccountSessionTest.swift @@ -0,0 +1,23 @@ +// +// STPAPIClient+LinkAccountSessionTest.swift +// StripeiOSTests +// +// Created by Yuki Tokuhiro on 4/26/23. +// + +@testable @_spi(STP) import StripePayments +import XCTest + +final class STPAPIClient_LinkAccountSessionTest: XCTestCase { + + func testCreateLinkAccountSessionForDeferredIntent() { + let e = expectation(description: "create link account session") + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + apiClient.createLinkAccountSessionForDeferredIntent(sessionId: "mobile_test_\(UUID().uuidString)", amount: nil, currency: nil, onBehalfOf: nil) { linkAccountSession, error in + XCTAssertNil(error) + XCTAssertNotNil(linkAccountSession) + e.fulfill() + } + waitForExpectations(timeout: 10) + } +} diff --git a/Stripe/StripeiOSTests/STPAPIClientNetworkBridgeTest.swift b/Stripe/StripeiOSTests/STPAPIClientNetworkBridgeTest.swift new file mode 100644 index 00000000..e6bb42aa --- /dev/null +++ b/Stripe/StripeiOSTests/STPAPIClientNetworkBridgeTest.swift @@ -0,0 +1,412 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPAPIClientNetworkBridgeTest.m +// StripeiOS +// +// Created by David Estes on 9/23/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import PassKit +import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@_spi(STP) import StripePayments +import StripePaymentsTestUtils +import XCTest + +class StripeAPIBridgeNetworkTest: STPNetworkStubbingTestCase { + var client: STPAPIClient! + + override func setUp() { + super.setUp() + client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + } + + // MARK: Bank Account + func testCreateTokenWithBankAccount() { + let exp = expectation(description: "Request complete") + let params = STPBankAccountParams() + params.accountNumber = "000123456789" + params.routingNumber = "110000000" + params.country = "US" + + client?.createToken(withBankAccount: params) { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: PII + + func testCreateTokenWithPII() { + let exp = expectation(description: "Create token") + + client?.createToken(withPersonalIDNumber: "123456789") { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateTokenWithSSNLast4() { + let exp = expectation(description: "Create SSN") + + client?.createToken(withSSNLast4: "1234") { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Connect Accounts + + func testCreateConnectAccount() { + let exp = expectation(description: "Create connect account") + let companyParams = STPConnectAccountCompanyParams() + companyParams.name = "Company" + let params = STPConnectAccountParams(company: companyParams) + client?.createToken(withConnectAccount: params) { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Upload + + func testUploadFile() { + let exp = expectation(description: "Upload file") + let image = UIImage( + named: "stp_test_upload_image.jpeg", + in: Bundle(for: StripeAPIBridgeNetworkTest.self), + compatibleWith: nil)! + + client?.uploadImage(image, purpose: .disputeEvidence) { file, error in + XCTAssertNotNil(file) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Credit Cards + + func testCardToken() { + let exp = expectation(description: "Create card token") + let params = STPCardParams() + params.number = "4242424242424242" + params.expYear = 42 + params.expMonth = 12 + params.cvc = "123" + + client?.createToken(withCard: params) { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCVCUpdate() { + let exp = expectation(description: "CVC Update") + + client?.createToken(forCVCUpdate: "123") { token, error in + XCTAssertNotNil(token) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Sources + + func testCreateRetrieveAndPollSource() { + let exp = expectation(description: "Upload file") + let expR = expectation(description: "Retrieve source") + let expP = expectation(description: "Poll source") + + let card = STPCardParams() + card.number = "4242424242424242" + card.expYear = 42 + card.expMonth = 12 + card.cvc = "123" + + let params = STPSourceParams.cardParams(withCard: card) + + client.createSource(with: params) { [self] source, error in + guard let source = source else { + XCTFail() + return + } + XCTAssertNil(error) + exp.fulfill() + + client?.retrieveSource(withId: source.stripeID, clientSecret: source.clientSecret!) { source2, error2 in + XCTAssertNotNil(source2) + XCTAssertNil(error2) + expR.fulfill() + } + + client?.startPollingSource(withId: source.stripeID, clientSecret: source.clientSecret!, timeout: 10) { [self] source2, error2 in + XCTAssertNotNil(source2) + XCTAssertNil(error2) + client?.stopPollingSource(withId: source.stripeID) + expP.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Payment Intents + + func testRetrievePaymentIntent() { + let exp = expectation(description: "Fetch") + let exp2 = expectation(description: "Fetch with expansion") + + let testClient = STPTestingAPIClient.shared() + testClient.createPaymentIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + client?.retrievePaymentIntent(withClientSecret: clientSecret!) { pi, error2 in + XCTAssertNotNil(pi) + XCTAssertNil(error2) + exp.fulfill() + } + + client?.retrievePaymentIntent(withClientSecret: clientSecret!, expand: ["metadata"]) { pi, error2 in + XCTAssertNotNil(pi) + XCTAssertNil(error2) + exp2.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmPaymentIntent() { + let exp = expectation(description: "Confirm") + let exp2 = expectation(description: "Confirm with expansion") + let testClient = STPTestingAPIClient.shared() + + let card = STPPaymentMethodCardParams() + card.number = "4242424242424242" + card.expYear = NSNumber(value: 42) + card.expMonth = NSNumber(value: 12) + card.cvc = "123" + + testClient.createPaymentIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + params.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + client?.confirmPaymentIntent(with: params) { pi, error2 in + XCTAssertNotNil(pi) + XCTAssertNil(error2) + exp.fulfill() + } + } + + testClient.createPaymentIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + params.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + client?.confirmPaymentIntent(with: params) { pi, error2 in + XCTAssertNotNil(pi) + XCTAssertNil(error2) + exp2.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRefreshPaymentIntent() { + let exp = expectation(description: "Refresh") + + let testClient = STPTestingAPIClient.shared() + testClient.createPaymentIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + client?.refreshPaymentIntent(withClientSecret: clientSecret!) { pi, error2 in + XCTAssertNotNil(pi) + XCTAssertNil(error2) + exp.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Setup Intents + + func testRetrieveSetupIntent() { + let exp = expectation(description: "Fetch") + + let testClient = STPTestingAPIClient.shared() + testClient.createSetupIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + client?.retrieveSetupIntent(withClientSecret: clientSecret!) { si, error2 in + XCTAssertNotNil(si) + XCTAssertNil(error2) + exp.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmSetupIntent() { + let exp = expectation(description: "Confirm") + let testClient = STPTestingAPIClient.shared() + + let card = STPPaymentMethodCardParams() + card.number = "4242424242424242" + card.expYear = NSNumber(value: 42) + card.expMonth = NSNumber(value: 12) + card.cvc = "123" + + testClient.createSetupIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + let params = STPSetupIntentConfirmParams(clientSecret: clientSecret!) + params.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + client?.confirmSetupIntent(with: params) { si, error2 in + XCTAssertNotNil(si) + XCTAssertNil(error2) + exp.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRefreshSetupIntent() { + let exp = expectation(description: "Refresh") + + let testClient = STPTestingAPIClient.shared() + testClient.createSetupIntent(withParams: nil) { [self] clientSecret, error in + XCTAssertNil(error) + + client?.refreshSetupIntent(withClientSecret: clientSecret!) { si, error2 in + XCTAssertNotNil(si) + XCTAssertNil(error2) + exp.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Payment Methods + + func testCreatePaymentMethod() { + let exp = expectation(description: "Create PaymentMethod") + + let card = STPPaymentMethodCardParams() + card.number = "4242424242424242" + card.expYear = NSNumber(value: 42) + card.expMonth = NSNumber(value: 12) + card.cvc = "123" + + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + client?.createPaymentMethod(with: params) { pm, error in + XCTAssertNotNil(pm) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: Radar + + func testCreateRadarSession() { + let exp = expectation(description: "Create session") + + // Set fake SID/MUID to make this test replicable + FraudDetectionData.shared.sid = "123" + FraudDetectionData.shared.muid = "123" + FraudDetectionData.shared.sidCreationDate = Date() + + client?.createRadarSession { session, error in + XCTAssertNotNil(session) + XCTAssertNil(error) + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: ApplePay + + func testCreateApplePayToken() { + let exp = expectation(description: "CreateToken") + let exp2 = expectation(description: "CreateSource") + let exp3 = expectation(description: "CreatePM") + let payment = STPFixtures.applePayPayment() + client?.createToken(with: payment) { token, error in + // The certificate used to sign our fake Apple Pay test payment is invalid, which makes sense. + // Expect an error. + XCTAssertNil(token) + XCTAssertNotNil(error) + exp.fulfill() + } + + client?.createSource(with: payment) { source, error in + XCTAssertNil(source) + XCTAssertNotNil(error) + exp2.fulfill() + } + + client?.createPaymentMethod(with: payment) { pm, error in + XCTAssertNil(pm) + XCTAssertNotNil(error) + exp3.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testPKPaymentError() { + let exp = expectation(description: "Upload file") + let params = STPCardParams() + params.number = "4242424242424242" + params.expYear = 20 + params.expMonth = 12 + params.cvc = "123" + + client?.createToken(withCard: params) { token, error in + XCTAssertNil(token) + XCTAssertNotNil(error) + XCTAssertNotNil(STPAPIClient.pkPaymentError(forStripeError: error)) + + exp.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} + +// These are a little redundant with the existing +// API tests, but it's a good way to test that the +// bridge works correctly. diff --git a/Stripe/StripeiOSTests/STPAPIClientStubbedTest.swift b/Stripe/StripeiOSTests/STPAPIClientStubbedTest.swift new file mode 100644 index 00000000..73546eb1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAPIClientStubbedTest.swift @@ -0,0 +1,311 @@ +// +// STPAPIClientStubbedTest.swift +// StripeiOS Tests +// +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet + +class STPAPIClientStubbedTest: APIStubbedTestCase { + func testCreatePaymentMethodWithAdditionalPaymentUserAgentValues() { + let sut = stubbedAPIClient() + stub { urlRequest in + guard let queryItems = urlRequest.queryItems else { + return false + } + XCTAssertTrue(queryItems.contains(where: { item in + // The additional payment user agent values "foo" and "bar" should be in the payment_user_agent field + item.name == "payment_user_agent" && item.value!.hasSuffix("%3B%20foo%3B%20bar") + })) + return true + } response: { _ in + return .init() + } + let e = expectation(description: "") + sut.createPaymentMethod(with: ._testValidCardValue(), additionalPaymentUserAgentValues: ["foo", "bar"]) { _, _ in + e.fulfill() + } + waitForExpectations(timeout: 10) + } + + func testSetupIntent_LinkAccountSessionForUSBankAccount() { + let sut = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains( + "/setup_intents/seti_12345/link_account_sessions" + ) ?? false + } response: { urlRequest in + guard let data = urlRequest.httpBodyOrBodyStream, + let body = String(data: data, encoding: .utf8) + else { + return HTTPStubsResponse( + data: "".data(using: .utf8)!, + statusCode: 400, + headers: nil + ) + } + XCTAssert(body.contains("client_secret=si_client_secret_123")) + XCTAssert( + body.contains( + "payment_method_data%5Bbilling_details%5D%5Bemail%5D=test%40example.com" + ) + ) + XCTAssert( + body.contains("payment_method_data%5Bbilling_details%5D%5Bname%5D=Test%20Tester") + ) + XCTAssert(body.contains("payment_method_data%5Btype%5D=us_bank_account")) + + let jsonText = """ + { + "id": "xxxxx", + "object": "link_account_session", + "client_secret": "las_client_secret_123456", + "linked_accounts": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/linked_accounts" + }, + "livemode": false + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let expectCallback = expectation(description: "bindings serialize/deserialize") + sut.createLinkAccountSession( + setupIntentID: "seti_12345", + clientSecret: "si_client_secret_123", + paymentMethodType: .USBankAccount, + customerName: "Test Tester", + customerEmailAddress: "test@example.com" + ) { intent, _ in + guard let intent = intent else { + XCTFail("Intent was null") + return + } + XCTAssertEqual(intent.clientSecret, "las_client_secret_123456") + expectCallback.fulfill() + } + + wait(for: [expectCallback], timeout: 2.0) + } + + func testPaymentIntent_LinkAccountSessionForUSBankAccount() { + let sut = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains( + "/payment_intents/pi_12345/link_account_sessions" + ) ?? false + } response: { urlRequest in + guard let data = urlRequest.httpBodyOrBodyStream, + let body = String(data: data, encoding: .utf8) + else { + return HTTPStubsResponse( + data: "".data(using: .utf8)!, + statusCode: 400, + headers: nil + ) + } + XCTAssert(body.contains("client_secret=si_client_secret_123")) + XCTAssert( + body.contains( + "payment_method_data%5Bbilling_details%5D%5Bemail%5D=test%40example.com" + ) + ) + XCTAssert( + body.contains("payment_method_data%5Bbilling_details%5D%5Bname%5D=Test%20Tester") + ) + XCTAssert(body.contains("payment_method_data%5Btype%5D=us_bank_account")) + + let jsonText = """ + { + "id": "las_12345", + "object": "link_account_session", + "client_secret": "las_client_secret_654321", + "linked_accounts": { + "object": "list", + "data": [ + + ], + "has_more": false, + "total_count": 0, + "url": "/v1/linked_accounts" + }, + "livemode": false + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let expectCallback = expectation(description: "bindings serialize/deserialize") + sut.createLinkAccountSession( + paymentIntentID: "pi_12345", + clientSecret: "si_client_secret_123", + paymentMethodType: .USBankAccount, + customerName: "Test Tester", + customerEmailAddress: "test@example.com" + ) { intent, _ in + guard let intent = intent else { + XCTFail("Intent was null") + return + } + XCTAssertEqual(intent.clientSecret, "las_client_secret_654321") + expectCallback.fulfill() + } + + wait(for: [expectCallback], timeout: 2.0) + } + + func testSetupIntent_LinkAccountSessionAttach() { + let sut = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains( + "/setup_intents/seti_12345/link_account_sessions/las_123456/attach" + ) ?? false + } response: { urlRequest in + guard let data = urlRequest.httpBodyOrBodyStream, + let body = String(data: data, encoding: .utf8) + else { + return HTTPStubsResponse( + data: "".data(using: .utf8)!, + statusCode: 400, + headers: nil + ) + } + XCTAssert(body.contains("client_secret=si_client_secret_123")) + + let jsonText = """ + { + "id": "seti_12345", + "object": "setup_intent", + "cancellation_reason": null, + "client_secret": "seti_abc_secret_def", + "created": 1647000000, + "description": null, + "last_setup_error": null, + "livemode": false, + "next_action": null, + "payment_method": "pm_abcdefg", + "payment_method_options": { + "us_bank_account": { + "verification_method": "instant" + } + }, + "payment_method_types": [ + "us_bank_account" + ], + "status": "requires_confirmation", + "usage": "off_session" + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let expectCallback = expectation(description: "bindings serialize/deserialize") + sut.attachLinkAccountSession( + setupIntentID: "seti_12345", + linkAccountSessionID: "las_123456", + clientSecret: "si_client_secret_123" + ) { intent, _ in + guard let intent = intent else { + XCTFail("Intent was null") + return + } + XCTAssertEqual(intent.paymentMethodID, "pm_abcdefg") + expectCallback.fulfill() + } + + wait(for: [expectCallback], timeout: 2.0) + } + + func testPaymentIntent_LinkAccountSessionAttach() { + let sut = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains( + "/payment_intents/pi_12345/link_account_sessions/las_123456/attach" + ) ?? false + } response: { urlRequest in + guard let data = urlRequest.httpBodyOrBodyStream, + let body = String(data: data, encoding: .utf8) + else { + return HTTPStubsResponse( + data: "".data(using: .utf8)!, + statusCode: 400, + headers: nil + ) + } + XCTAssert(body.contains("client_secret=pi_client_secret_123")) + + let jsonText = """ + { + "id": "pi_12345", + "object": "payment_intent", + "amount": 100, + "currency": "usd", + "cancellation_reason": null, + "client_secret": "seti_abc_secret_def", + "created": 1647000000, + "description": null, + "last_setup_error": null, + "livemode": false, + "next_action": null, + "payment_method": "pm_abcdefg", + "payment_method_options": { + "us_bank_account": { + "verification_method": "instant" + } + }, + "payment_method_types": [ + "us_bank_account" + ], + "status": "requires_payment_method" + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let expectCallback = expectation(description: "bindings serialize/deserialize") + sut.attachLinkAccountSession( + paymentIntentID: "pi_12345", + linkAccountSessionID: "las_123456", + clientSecret: "pi_client_secret_123" + ) { intent, _ in + guard let intent = intent else { + XCTFail("Intent was null") + return + } + XCTAssertEqual(intent.paymentMethodId, "pm_abcdefg") + expectCallback.fulfill() + } + + wait(for: [expectCallback], timeout: 2.0) + } +} diff --git a/Stripe/StripeiOSTests/STPAPIClientTest.swift b/Stripe/StripeiOSTests/STPAPIClientTest.swift new file mode 100644 index 00000000..656381cf --- /dev/null +++ b/Stripe/StripeiOSTests/STPAPIClientTest.swift @@ -0,0 +1,149 @@ +// +// STPAPIClientTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 12/19/14. +// Copyright (c) 2014 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAPIClientTest: XCTestCase { + func testSharedClient() { + XCTAssert(STPAPIClient.shared === STPAPIClient.shared) + } + + func testSetDefaultPublishableKey() { + let clientInitializedBefore = STPAPIClient() + StripeAPI.defaultPublishableKey = "test" + let clientInitializedAfter = STPAPIClient() + let sharedClient = STPAPIClient.shared + XCTAssertEqual(clientInitializedBefore.publishableKey, "test") + XCTAssertEqual(clientInitializedAfter.publishableKey, "test") + + // Setting the STPAPIClient instance overrides Stripe.defaultPublishableKey... + sharedClient.publishableKey = "test2" + XCTAssertEqual(sharedClient.publishableKey, "test2") + + // ...while Stripe.defaultPublishableKey remains the same + XCTAssertEqual(StripeAPI.defaultPublishableKey, "test") + } + + func testInitWithPublishableKey() { + let sut = STPAPIClient(publishableKey: "pk_foo") + let authHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ).allHTTPHeaderFields?["Authorization"] + XCTAssertEqual(authHeader, "Bearer pk_foo") + } + + func testSetPublishableKey() { + let sut = STPAPIClient(publishableKey: "pk_foo") + var authHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ).allHTTPHeaderFields?["Authorization"] + XCTAssertEqual(authHeader, "Bearer pk_foo") + sut.publishableKey = "pk_bar" + authHeader = + sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ) + .allHTTPHeaderFields?["Authorization"] + XCTAssertEqual(authHeader, "Bearer pk_bar") + } + + func testEphemeralKeyOverwritesHeader() { + let sut = STPAPIClient(publishableKey: "pk_foo") + let ephemeralKey = STPFixtures.ephemeralKey() + let additionalHeaders = sut.authorizationHeader(using: ephemeralKey) + let authHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: additionalHeaders + ).allHTTPHeaderFields?["Authorization"] + XCTAssertEqual(authHeader, "Bearer " + (ephemeralKey.secret)) + } + + func testSetStripeAccount() { + let sut = STPAPIClient(publishableKey: "pk_foo") + var accountHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ).allHTTPHeaderFields?["Stripe-Account"] + XCTAssertNil(accountHeader) + sut.stripeAccount = "acct_123" + accountHeader = + sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ) + .allHTTPHeaderFields?["Stripe-Account"] + XCTAssertEqual(accountHeader, "acct_123") + } + + func testInitWithConfiguration() { + let config = STPPaymentConfiguration() + config.publishableKey = "pk_123" + config.stripeAccount = "acct_123" + + let sut = STPAPIClient(configuration: config) + XCTAssertEqual(sut.publishableKey, config.publishableKey) + XCTAssertEqual(sut.stripeAccount, config.stripeAccount) + + let accountHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ).allHTTPHeaderFields?["Stripe-Account"] + XCTAssertEqual(accountHeader, "acct_123") + } + + private struct MockUAUsageClass: STPAnalyticsProtocol { + static let stp_analyticsIdentifier = "MockUAUsageClass" + } + + func testPaymentUserAgent() { + STPAnalyticsClient.sharedClient.productUsage = .init() + STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: MockUAUsageClass.self) + var params: [String: Any] = [:] + params = STPAPIClient.paramsAddingPaymentUserAgent(params) + XCTAssertEqual(params["payment_user_agent"] as! String, "stripe-ios/\(StripeAPIConfiguration.STPSDKVersion); variant.legacy; MockUAUsageClass") + + params = STPAPIClient.paramsAddingPaymentUserAgent(params, additionalValues: ["foo"]) + XCTAssertEqual(params["payment_user_agent"] as! String, "stripe-ios/\(StripeAPIConfiguration.STPSDKVersion); variant.legacy; MockUAUsageClass; foo") + } + + func testSetAppInfo() { + let sut = STPAPIClient(publishableKey: "pk_foo") + sut.appInfo = STPAppInfo( + name: "MyAwesomeLibrary", + partnerId: "pp_partner_1234", + version: "1.2.34", + url: "https://myawesomelibrary.info" + ) + let userAgentHeader = sut.configuredRequest( + for: URL(string: "https://www.stripe.com")!, + additionalHeaders: [:] + ).allHTTPHeaderFields?["X-Stripe-User-Agent"] + var userAgentHeaderDict: [AnyHashable: Any]? + do { + if let data = userAgentHeader?.data(using: .utf8) { + userAgentHeaderDict = + try JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any] + } + } catch { + } + XCTAssertEqual(userAgentHeaderDict?["name"] as! String, "MyAwesomeLibrary") + XCTAssertEqual(userAgentHeaderDict?["partner_id"] as! String, "pp_partner_1234") + XCTAssertEqual(userAgentHeaderDict?["version"] as! String, "1.2.34") + XCTAssertEqual(userAgentHeaderDict?["url"] as! String, "https://myawesomelibrary.info") + } +} diff --git a/Stripe/StripeiOSTests/STPAPISettingsObjCBridgeTest.m b/Stripe/StripeiOSTests/STPAPISettingsObjCBridgeTest.m new file mode 100644 index 00000000..046c294a --- /dev/null +++ b/Stripe/StripeiOSTests/STPAPISettingsObjCBridgeTest.m @@ -0,0 +1,84 @@ +// +// STPObjcBridgeTest.m +// StripeiOS Tests +// +// Created by David Estes on 9/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +@import Stripe; +@import XCTest; +@import PassKit; +@import StripePaymentsObjcTestUtils; + +@interface StripeAPIBridgeTest : XCTestCase + +@end + +@implementation StripeAPIBridgeTest + +- (void)testStripeAPIBridge { + NSString *testKey = @"pk_test_123"; + StripeAPI.defaultPublishableKey = testKey; + XCTAssertEqualObjects(StripeAPI.defaultPublishableKey, testKey); + StripeAPI.defaultPublishableKey = nil; + + StripeAPI.advancedFraudSignalsEnabled = NO; + XCTAssertFalse(StripeAPI.advancedFraudSignalsEnabled); + StripeAPI.advancedFraudSignalsEnabled = YES; + + + StripeAPI.maxRetries = 2; + XCTAssertEqual(StripeAPI.maxRetries, 2); + StripeAPI.maxRetries = 3; + + // Check that this at least doesn't crash + [StripeAPI handleStripeURLCallbackWithURL:[NSURL URLWithString:@"https://example.com"]]; + + StripeAPI.jcbPaymentNetworkSupported = YES; + XCTAssertTrue(StripeAPI.jcbPaymentNetworkSupported); + StripeAPI.jcbPaymentNetworkSupported = NO; + + StripeAPI.additionalEnabledApplePayNetworks = @[PKPaymentNetworkJCB]; + XCTAssertTrue([StripeAPI.additionalEnabledApplePayNetworks containsObject:PKPaymentNetworkJCB]); + StripeAPI.additionalEnabledApplePayNetworks = @[]; + + PKPaymentRequest *request = [StripeAPI paymentRequestWithMerchantIdentifier:@"test" country:@"US" currency:@"USD"]; + request.paymentSummaryItems = @[[PKPaymentSummaryItem summaryItemWithLabel:@"bar" amount:[NSDecimalNumber decimalNumberWithString:@"1.00"]]]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated" + PKPaymentRequest *request2 = [StripeAPI paymentRequestWithMerchantIdentifier:@"test"]; + request2.paymentSummaryItems = @[[PKPaymentSummaryItem summaryItemWithLabel:@"bar" amount:[NSDecimalNumber decimalNumberWithString:@"1.00"]]]; + #pragma clang diagnostic pop + + XCTAssertTrue([StripeAPI canSubmitPaymentRequest:request]); + XCTAssertTrue([StripeAPI canSubmitPaymentRequest:request2]); + + XCTAssertTrue([StripeAPI deviceSupportsApplePay]); +} + +- (void)testSTPAPIClientBridgeKeys { + NSString *testKey = @"pk_test_123"; + StripeAPI.defaultPublishableKey = testKey; + XCTAssertEqualObjects(testKey, StripeAPI.defaultPublishableKey); + StripeAPI.defaultPublishableKey = nil; +} + +- (void)testSTPAPIClientBridgeSettings { + STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:@"pk_test_123"]; + STPPaymentConfiguration *config = [[STPPaymentConfiguration alloc] init]; + client.configuration = config; + XCTAssertEqual(config, client.configuration); + + NSString *stripeAccount = @"acct_123"; + client.stripeAccount = stripeAccount; + XCTAssertEqualObjects(stripeAccount, client.stripeAccount); + + STPAppInfo *appInfo = [[STPAppInfo alloc] initWithName:@"test" partnerId:@"abc123" version:@"1.0" url:@"https://example.com"]; + client.appInfo = appInfo; + XCTAssertEqualObjects(appInfo.name, client.appInfo.name); + + XCTAssertNotNil(STPAPIClient.apiVersion); +} + +@end diff --git a/Stripe/StripeiOSTests/STPAUBECSDebitFormViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPAUBECSDebitFormViewSnapshotTests.swift new file mode 100644 index 00000000..649193e1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAUBECSDebitFormViewSnapshotTests.swift @@ -0,0 +1,115 @@ +// +// STPAUBECSDebitFormViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAUBECSDebitFormViewSnapshotTests: STPSnapshotTestCase { + + func testDefaultAppearance() { + let view = _newFormView() + _size(toFit: view) + STPSnapshotVerifyView(view, identifier: "STPAUBECSDebitFormView.defaultAppearance") + } + + func testNoDataCustomization() { + let view = _newFormView() + + _applyCustomization(view) + + _size(toFit: view) + + STPSnapshotVerifyView(view, identifier: "STPAUBECSDebitFormView.noDataCustomization") + } + + func testWithDataAppearance() { + let view = _newFormView() + view.nameTextField().text = "Jenny Rosen" + view.emailTextField().text = "jrosen@example.com" + view.bsbNumberTextField().text = "111111" + view.accountNumberTextField().text = "123456" + _size(toFit: view) + + STPSnapshotVerifyView(view, identifier: "STPAUBECSDebitFormView.withDataAppearance") + } + + func testWithDataCustomization() { + let view = _newFormView() + view.nameTextField().text = "Jenny Rosen" + view.emailTextField().text = "jrosen@example.com" + view.bsbNumberTextField().text = "111111" + view.accountNumberTextField().text = "123456" + _applyCustomization(view) + _size(toFit: view) + + STPSnapshotVerifyView(view, identifier: "STPAUBECSDebitFormView.withDataAppearance") + } + + func testInvalidBSBAndEmailAppearance() { + let view = _newFormView() + view.nameTextField().text = "Jenny Rosen" + view.emailTextField().text = "jrosen" + view.bsbNumberTextField().text = "666666" + view.accountNumberTextField().text = "123456" + _size(toFit: view) + + STPSnapshotVerifyView( + view, + identifier: "STPAUBECSDebitFormView.invalidBSBAndEmailAppearance" + ) + } + + func testInvalidBSBAndEmailCustomization() { + let view = _newFormView() + view.nameTextField().text = "Jenny Rosen" + view.emailTextField().text = "jrosen" + view.bsbNumberTextField().text = "666666" + view.accountNumberTextField().text = "123456" + _applyCustomization(view) + _size(toFit: view) + + STPSnapshotVerifyView( + view, + identifier: "STPAUBECSDebitFormView.invalidBSBAndEmailCustomization" + ) + } + + // MARK: - Helpers + func _newFormView() -> STPAUBECSDebitFormView { + let formView = STPAUBECSDebitFormView(companyName: "Snapshotter") + formView.frame = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 600.0) + return formView + } + + func _applyCustomization(_ view: STPAUBECSDebitFormView?) { + view?.formFont = UIFont.boldSystemFont(ofSize: 12.0) + view?.formTextColor = UIColor.blue + view?.formTextErrorColor = UIColor.orange + view?.formPlaceholderColor = UIColor.black + view?.formCursorColor = UIColor.red + view?.formBackgroundColor = UIColor( + red: 255.0 / 255.0, + green: 45.0 / 255.0, + blue: 85.0 / 255.0, + alpha: 1.0 + ) + } + + func _size(toFit view: STPAUBECSDebitFormView?) { + var adjustedFrame = view?.frame + adjustedFrame?.size.height = + view?.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height ?? 0.0 + view?.frame = adjustedFrame! + } +} diff --git a/Stripe/StripeiOSTests/STPAUBECSFormViewModelTests.swift b/Stripe/StripeiOSTests/STPAUBECSFormViewModelTests.swift new file mode 100644 index 00000000..829f50c6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAUBECSFormViewModelTests.swift @@ -0,0 +1,584 @@ +// +// STPAUBECSFormViewModelTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAUBECSFormViewModelTests: XCTestCase { + func testBECSDebitParams() { + do { + // Test empty data + let model = STPAUBECSFormViewModel() + XCTAssertNil(model.becsDebitParams, "params with no data should be nil") + } + + do { + // Test complete/valid data + let model = STPAUBECSFormViewModel() + model.accountNumber = "123456" + model.bsbNumber = "111-111" + + let params = model.becsDebitParams + XCTAssertNotNil(params, "Failed to create BECS Debit params") + XCTAssertEqual(params?.accountNumber, "123456") + XCTAssertEqual(params?.bsbNumber, "111111") + } + + do { + // Test complete/valid data w/o formatting + let model = STPAUBECSFormViewModel() + model.accountNumber = "123456" + model.bsbNumber = "111111" + + let params = model.becsDebitParams + XCTAssertNotNil(params, "Failed to create BECS Debit params") + XCTAssertEqual(params?.accountNumber, "123456") + XCTAssertEqual(params?.bsbNumber, "111111") + } + + do { + // Test complete/valid accountNumber, incomplete bsb number + let model = STPAUBECSFormViewModel() + model.accountNumber = "123456" + model.bsbNumber = "111-" + + let params = model.becsDebitParams + XCTAssertNil(params, "Should not create params with incomplete bsb number") + } + + do { + // Test incomplete accountNumber, complete/valid bsb number + let model = STPAUBECSFormViewModel() + model.accountNumber = "1234" + model.bsbNumber = "111-111" + + let params = model.becsDebitParams + XCTAssertNil(params, "Should not create params with incomplete account number") + } + + do { + // Test invalid accountNumber, complete/valid bsb number + let model = STPAUBECSFormViewModel() + model.accountNumber = "12345678910" + model.bsbNumber = "111-111" + + let params = model.becsDebitParams + XCTAssertNil(params, "Should not create params with invalid account number") + } + + do { + // Test complete/valid accountNumber, invalid bsb number + let model = STPAUBECSFormViewModel() + model.accountNumber = "123456" + model.bsbNumber = "666-666" + + let params = model.becsDebitParams + XCTAssertNil(params, "Should not create params with incomplete bsb number") + } + } + + func testPaymentMethodParams() { + do { + /// Test empty + let model = STPAUBECSFormViewModel() + XCTAssertNil(model.paymentMethodParams, "params with no data should be nil") + } + + do { + /// name: + + /// email: + + /// bsb: + (formatting) + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "123456" + model.bsbNumber = "111-111" + + let params = model.paymentMethodParams + XCTAssertNotNil(params, "Failed to create BECS Debit params") + XCTAssertEqual(params?.billingDetails?.name, "Jenny Rosen") + XCTAssertEqual(params?.billingDetails?.email, "jrosen@example.com") + XCTAssertEqual(params?.auBECSDebit?.accountNumber, "123456") + XCTAssertEqual(params?.auBECSDebit?.bsbNumber, "111111") + } + + do { + /// name: + + /// email: + + /// bsb: + + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "123456" + model.bsbNumber = "111111" + + let params = model.paymentMethodParams + XCTAssertNotNil(params, "Failed to create BECS Debit params") + XCTAssertEqual(params?.billingDetails?.name, "Jenny Rosen") + XCTAssertEqual(params?.billingDetails?.email, "jrosen@example.com") + XCTAssertEqual(params?.auBECSDebit?.accountNumber, "123456") + XCTAssertEqual(params?.auBECSDebit?.bsbNumber, "111111") + } + + do { + /// name: + + /// email: + + /// bsb: x (incomplete) + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "123456" + model.bsbNumber = "111-" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create params with incomplete bsb number") + } + + do { + /// name: + + /// email: + + /// bsb: + + /// account: x (incomplete) + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "1234" + model.bsbNumber = "111-111" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create params with incomplete account number") + } + + do { + /// name: + + /// email: + + /// bsb: + + /// account: x + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "12345678910" + model.bsbNumber = "111-111" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create params with invalid account number") + } + + do { + /// name: + + /// email: + + /// bsb: x + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrosen@example.com" + model.accountNumber = "123456" + model.bsbNumber = "666-666" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create params with incomplete bsb number") + } + + do { + /// name: x + /// email: + + /// bsb: + (formatting) + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "" + model.email = "jrosen@example.com" + model.accountNumber = "123456" + model.bsbNumber = "111-111" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create payment method params without name.") + } + + do { + /// name: + + /// email: x + /// bsb: + (formatting) + /// account: + + let model = STPAUBECSFormViewModel() + model.name = "Jenny Rosen" + model.email = "jrose" + model.accountNumber = "123456" + model.bsbNumber = "111-111" + + let params = model.paymentMethodParams + XCTAssertNil(params, "Should not create payment method params with invalid email.") + } + } + + func testBSBLabelForInput() { + do { + // empty test + let model = STPAUBECSFormViewModel() + var isErrorString = true + var bsbLabel = model.bsbLabel( + forInput: "", + editing: false, + isErrorString: &isErrorString + ) + XCTAssertFalse(isErrorString, "Empty input shouldn't be an error.") + XCTAssertNil(bsbLabel, "No bsb label for empty input.") + + isErrorString = true + bsbLabel = model.bsbLabel(forInput: nil, editing: true, isErrorString: &isErrorString) + XCTAssertFalse(isErrorString, "nil input shouldn't be an error.") + XCTAssertNil(bsbLabel, "No bsb label for nil input.") + } + + do { + // invalid test + let model = STPAUBECSFormViewModel() + var isErrorString = false + var bsbLabel = model.bsbLabel( + forInput: "666-666", + editing: false, + isErrorString: &isErrorString + ) + XCTAssertTrue(isErrorString, "Invalid input should be an error.") + XCTAssertEqual(bsbLabel, "The BSB you entered is invalid.") + + isErrorString = false + bsbLabel = model.bsbLabel( + forInput: "666-666", + editing: true, + isErrorString: &isErrorString + ) + XCTAssertTrue(isErrorString, "Invalid input should be an error (editing).") + XCTAssertEqual(bsbLabel, "The BSB you entered is invalid.") + } + + do { + // incomplete test + let model = STPAUBECSFormViewModel() + var isErrorString = false + var bsbLabel = model.bsbLabel( + forInput: "111-11", + editing: false, + isErrorString: &isErrorString + ) + XCTAssertTrue(isErrorString, "Incomplete input should be an error when not editing.") + XCTAssertEqual(bsbLabel, "The BSB you entered is incomplete.") + + isErrorString = true + bsbLabel = model.bsbLabel( + forInput: "111-11", + editing: true, + isErrorString: &isErrorString + ) + XCTAssertFalse(isErrorString, "Incomplete input should not be an error when editing.") + XCTAssertEqual(bsbLabel, "St George Bank (division of Westpac Bank)") + } + + do { + // valid test + let model = STPAUBECSFormViewModel() + var isErrorString = true + var bsbLabel = model.bsbLabel( + forInput: "111-111", + editing: false, + isErrorString: &isErrorString + ) + XCTAssertFalse(isErrorString, "Complete input should be not an error when not editing.") + XCTAssertEqual(bsbLabel, "St George Bank (division of Westpac Bank)") + + isErrorString = true + bsbLabel = model.bsbLabel( + forInput: "111-111", + editing: true, + isErrorString: &isErrorString + ) + XCTAssertFalse(isErrorString, "Complete input should not be an error when editing.") + XCTAssertEqual(bsbLabel, "St George Bank (division of Westpac Bank)") + } + } + + func testIsInputValid() { + do { + // name + let model = STPAUBECSFormViewModel() + XCTAssertTrue( + model.isInputValid("", for: .name, editing: false), + "Name should always be valid." + ) + XCTAssertTrue( + model.isInputValid("Jen", for: .name, editing: true), + "Name should always be valid." + ) + } + + do { + // email + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isInputValid("jrosen", for: .email, editing: false), + "Partial email is invalid when not editing." + ) + XCTAssertTrue( + model.isInputValid("jrosen", for: .email, editing: true), + "Partial email is valid when editing." + ) + + XCTAssertTrue( + model.isInputValid("", for: .email, editing: false), + "Empty email is always valid." + ) + XCTAssertTrue( + model.isInputValid("", for: .email, editing: true), + "Empty email is always valid." + ) + + XCTAssertTrue( + model.isInputValid("jrosen@example.com", for: .email, editing: false), + "Valid email." + ) + XCTAssertTrue( + model.isInputValid("jrosen@example.com", for: .email, editing: true), + "Valid email." + ) + } + + do { + // bsb + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isInputValid("111-1", for: .BSBNumber, editing: false), + "Partial bsb is invalid when not editing." + ) + XCTAssertTrue( + model.isInputValid("111-1", for: .BSBNumber, editing: true), + "Partial bsb is valid when editing." + ) + + XCTAssertTrue( + model.isInputValid("", for: .BSBNumber, editing: false), + "Empty bsb is always valid." + ) + XCTAssertTrue( + model.isInputValid("", for: .BSBNumber, editing: true), + "Empty bsb is always valid." + ) + + XCTAssertTrue( + model.isInputValid("111-111", for: .BSBNumber, editing: false), + "Valid bsb." + ) + XCTAssertTrue( + model.isInputValid("111-111", for: .BSBNumber, editing: true), + "Valid bsb." + ) + + XCTAssertFalse( + model.isInputValid("666-6", for: .BSBNumber, editing: false), + "Invalid partial bsb is always invalid." + ) + XCTAssertFalse( + model.isInputValid("666-6", for: .BSBNumber, editing: true), + "Invalid partial bsb is always invalid." + ) + + XCTAssertFalse( + model.isInputValid("666-666", for: .BSBNumber, editing: false), + "Invalid full bsb is always invalid." + ) + XCTAssertFalse( + model.isInputValid("666-666", for: .BSBNumber, editing: true), + "Invalid full bsb is always invalid." + ) + } + + do { + // account + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isInputValid("1234", for: .accountNumber, editing: false), + "Partial account number is invalid when not editing." + ) + XCTAssertTrue( + model.isInputValid("1234", for: .accountNumber, editing: true), + "Partial account number is valid when editing." + ) + + XCTAssertTrue( + model.isInputValid("", for: .accountNumber, editing: false), + "Empty account number is always valid." + ) + XCTAssertTrue( + model.isInputValid("", for: .accountNumber, editing: true), + "Empty account number is always valid." + ) + + XCTAssertTrue( + model.isInputValid("12345", for: .accountNumber, editing: false), + "Valid account number." + ) + XCTAssertTrue( + model.isInputValid("12345", for: .accountNumber, editing: true), + "Valid account number." + ) + + XCTAssertFalse( + model.isInputValid("12345678910", for: .accountNumber, editing: false), + "Invalid account number is always invalid." + ) + XCTAssertFalse( + model.isInputValid("12345678910", for: .accountNumber, editing: true), + "Invalid account number is always invalid." + ) + } + } + + func testIsFieldComplete() { + do { + // name + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .name, editing: false), + "Empty name is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .name, editing: true), + "Empty name is not complete." + ) + + XCTAssertTrue( + model.isFieldComplete(withInput: "Jen", in: .name, editing: false), + "Non-empty name is complete." + ) + XCTAssertTrue( + model.isFieldComplete(withInput: "Jenny Rosen", in: .name, editing: true), + "Non-empty name is complete." + ) + } + + do { + // email + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isFieldComplete(withInput: "jrosen", in: .email, editing: false), + "Partial email is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "jrosen", in: .email, editing: true), + "Partial email is not complete." + ) + + XCTAssertTrue( + model.isFieldComplete(withInput: "jrosen@example.com", in: .email, editing: false), + "Full email is complete." + ) + XCTAssertTrue( + model.isFieldComplete(withInput: "jrosen@example.com", in: .email, editing: true), + "Full email is complete." + ) + } + + do { + // bsb + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isFieldComplete(withInput: "111-1", in: .BSBNumber, editing: false), + "Partial bsb is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "111-1", in: .BSBNumber, editing: true), + "Partial bsb is not complete." + ) + + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .BSBNumber, editing: false), + "Empty bsb is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .BSBNumber, editing: true), + "Empty bsb is not complete." + ) + + XCTAssertTrue( + model.isFieldComplete(withInput: "111-111", in: .BSBNumber, editing: false), + "Full bsb is complete." + ) + XCTAssertTrue( + model.isFieldComplete(withInput: "111-111", in: .BSBNumber, editing: true), + "Full bsb is complete." + ) + + XCTAssertFalse( + model.isFieldComplete(withInput: "666-6", in: .BSBNumber, editing: false), + "Invalid partial bsb is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "666-6", in: .BSBNumber, editing: true), + "Invalid partial bsb is not complete." + ) + + XCTAssertFalse( + model.isFieldComplete(withInput: "666-666", in: .BSBNumber, editing: false), + "Invalid full bsb is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "666-666", in: .BSBNumber, editing: true), + "Invalid full bsb is not complete." + ) + } + + do { + // account + let model = STPAUBECSFormViewModel() + XCTAssertFalse( + model.isFieldComplete(withInput: "1234", in: .accountNumber, editing: false), + "Partial account number is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "1234", in: .accountNumber, editing: true), + "Partial account number is not complete." + ) + + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .accountNumber, editing: false), + "Empty account number is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "", in: .accountNumber, editing: true), + "Empty account number is not complete." + ) + + XCTAssertTrue( + model.isFieldComplete(withInput: "12345", in: .accountNumber, editing: false), + "Min length account number is complete when not editing." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "12345", in: .accountNumber, editing: true), + "Min length account number is not complete when editing." + ) + + XCTAssertTrue( + model.isFieldComplete(withInput: "123456789", in: .accountNumber, editing: true), + "Max length account number is complete when editing." + ) + + XCTAssertFalse( + model.isFieldComplete(withInput: "12345678910", in: .accountNumber, editing: false), + "Invalid account number is not complete." + ) + XCTAssertFalse( + model.isFieldComplete(withInput: "12345678910", in: .accountNumber, editing: true), + "Invalid account number is not complete." + ) + } + } +} diff --git a/Stripe/StripeiOSTests/STPAddCardViewControllerLocalizationSnapshotTests.swift b/Stripe/StripeiOSTests/STPAddCardViewControllerLocalizationSnapshotTests.swift new file mode 100644 index 00000000..9ebaf74d --- /dev/null +++ b/Stripe/StripeiOSTests/STPAddCardViewControllerLocalizationSnapshotTests.swift @@ -0,0 +1,101 @@ +// +// STPAddCardViewControllerLocalizationSnapshotTests.swift +// StripeiOS Tests +// +// Created by Brian Dorfman on 10/17/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAddCardViewControllerLocalizationSnapshotTests: STPSnapshotTestCase { + func performSnapshotTest(forLanguage language: String?, delivery: Bool) { + let config = STPPaymentConfiguration() + config.companyName = "Test Company" + config.requiredBillingAddressFields = .full + config.shippingType = delivery ? .delivery : .shipping + config.cardScanningEnabled = true + STPLocalizationUtils.overrideLanguage(to: language) + + let addCardVC = STPAddCardViewController( + configuration: config, + theme: STPTheme.defaultTheme + ) + addCardVC.shippingAddress = STPAddress() + addCardVC.shippingAddress?.line1 = "1" // trigger "use shipping address" button + + let viewToTest = stp_preparedAndSizedViewForSnapshotTest(from: addCardVC)! + + if delivery { + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "INVALID" + STPSnapshotVerifyView(viewToTest, identifier: "delivery") + } else { + // This method rejects nil or empty country codes to stop strange looking behavior + // when scrolling to the top "unset" position in the picker, so put in + // an invalid country code instead to test seeing the "Country" placeholder + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "INVALID" + STPSnapshotVerifyView(viewToTest, identifier: "no_country") + + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "US" + STPSnapshotVerifyView(viewToTest, identifier: "US") + + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "GB" + STPSnapshotVerifyView(viewToTest, identifier: "GB") + + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "CA" + STPSnapshotVerifyView(viewToTest, identifier: "CA") + + addCardVC.addressViewModel.addressFieldTableViewCountryCode = "MX" + STPSnapshotVerifyView(viewToTest, identifier: "MX") + } + + STPLocalizationUtils.overrideLanguage(to: nil) + } + + func testGerman() { + performSnapshotTest(forLanguage: "de", delivery: false) + performSnapshotTest(forLanguage: "de", delivery: true) + } + + func testEnglish() { + performSnapshotTest(forLanguage: "en", delivery: false) + performSnapshotTest(forLanguage: "en", delivery: true) + } + + func testSpanish() { + performSnapshotTest(forLanguage: "es", delivery: false) + performSnapshotTest(forLanguage: "es", delivery: true) + } + + func testFrench() { + performSnapshotTest(forLanguage: "fr", delivery: false) + performSnapshotTest(forLanguage: "fr", delivery: true) + } + + func testItalian() { + performSnapshotTest(forLanguage: "it", delivery: false) + performSnapshotTest(forLanguage: "it", delivery: true) + } + + func testJapanese() { + performSnapshotTest(forLanguage: "ja", delivery: false) + performSnapshotTest(forLanguage: "ja", delivery: true) + } + + func testDutch() { + performSnapshotTest(forLanguage: "nl", delivery: false) + performSnapshotTest(forLanguage: "nl", delivery: true) + } + + func testChinese() { + performSnapshotTest(forLanguage: "zh-Hans", delivery: false) + performSnapshotTest(forLanguage: "zh-Hans", delivery: true) + } +} diff --git a/Stripe/StripeiOSTests/STPAddCardViewControllerTest.swift b/Stripe/StripeiOSTests/STPAddCardViewControllerTest.swift new file mode 100644 index 00000000..16b50807 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAddCardViewControllerTest.swift @@ -0,0 +1,290 @@ +// +// STPAddCardViewControllerTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 7/5/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class MockDelegate: NSObject, STPAddCardViewControllerDelegate { + func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) { + + } + + var addCardViewControllerDidCreatePaymentMethodBlock: + (STPAddCardViewController, STPPaymentMethod, STPErrorBlock) -> Void = { _, _, _ in } + func addCardViewController( + _ addCardViewController: STPAddCardViewController, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + completion: @escaping STPErrorBlock + ) { + addCardViewControllerDidCreatePaymentMethodBlock( + addCardViewController, + paymentMethod, + completion + ) + } +} + +class STPAddCardViewControllerTest: APIStubbedTestCase { + + func paymentMethodAPIFilter( + expectedCardParams: STPPaymentMethodCardParams, + urlRequest: URLRequest + ) -> Bool { + if urlRequest.url?.absoluteString.contains("payment_methods") ?? false { + let cardNumber = urlRequest.queryItems?.first(where: { item in + item.name == "card[number]" + }) + XCTAssertEqual(cardNumber!.value, expectedCardParams.number) + return true + } + return false + } + + func buildAddCardViewController() -> STPAddCardViewController? { + let config = STPPaymentConfiguration() + let theme = STPTheme.defaultTheme + let vc = STPAddCardViewController( + configuration: config, + theme: theme + ) + XCTAssertNotNil(vc.view) + return vc + } + + func testPrefilledBillingAddress_removeAddress() { + let config = STPPaymentConfiguration() + config.requiredBillingAddressFields = .postalCode + let sut = STPAddCardViewController( + configuration: config, + theme: STPTheme.defaultTheme + ) + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "Harare" + address.postalCode = "10002" + // Zimbabwe does not require zip codes, while the default locale for tests (US) does + address.country = "ZW" + // Sanity checks + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "ZW")) + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "US")) + + let prefilledInfo = STPUserInformation() + prefilledInfo.billingAddress = address + sut.prefilledInformation = prefilledInfo + + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + } + + func testPrefilledBillingAddress_viewDidLoadHappensBeforeSettingAddress() { + let config = STPPaymentConfiguration() + config.requiredBillingAddressFields = .full + let sut = STPAddCardViewController( + configuration: config, + theme: STPTheme.defaultTheme + ) + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + + let address = STPAddress() + address.name = "John Smith Doe" + address.line1 = "55 John St" + address.city = "Harare" + address.postalCode = "10002" + + let prefilledInfo = STPUserInformation() + prefilledInfo.billingAddress = address + sut.prefilledInformation = prefilledInfo + + let nameCell = sut.addressViewModel.addressCells.first { $0.type == .name }! + XCTAssertEqual( nameCell.contents, "John Smith Doe") + + let line1Cell = sut.addressViewModel.addressCells.first { $0.type == .line1 }! + XCTAssertEqual( line1Cell.contents, "55 John St") + + let cityCell = sut.addressViewModel.addressCells.first { $0.type == .city }! + XCTAssertEqual( cityCell.contents, "Harare") + + let zipCell = sut.addressViewModel.addressCells.first { $0.type == .zip }! + XCTAssertEqual( zipCell.contents, "10002") + } + + func testPrefilledBillingAddress_addAddress() { + // Zimbabwe does not require zip codes, while the default locale for tests (US) does + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_ZW")) { + // Sanity checks + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "ZW")) + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "US")) + let config = STPPaymentConfiguration() + config.requiredBillingAddressFields = .postalCode + let sut = STPAddCardViewController( + configuration: config, + theme: STPTheme.defaultTheme + ) + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let prefilledInfo = STPUserInformation() + prefilledInfo.billingAddress = address + sut.prefilledInformation = prefilledInfo + + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + } + } + + func testNextWithCreatePaymentMethodError() { + let sut = buildAddCardViewController()! + let expectedCardParams = STPFixtures.paymentMethodCardParams() + sut.paymentCell?.paymentField!.perform(NSSelectorFromString("setCardParams:"), with: expectedCardParams) + + let exp = expectation(description: "createPaymentMethodWithCard network request") + stub { urlRequest in + return self.paymentMethodAPIFilter( + expectedCardParams: expectedCardParams, + urlRequest: urlRequest + ) + } response: { _ in + XCTAssertTrue(sut.loading) + let paymentMethod = ["error": "intentionally_invalid"] + defer { + exp.fulfill() + } + return HTTPStubsResponse(jsonObject: paymentMethod, statusCode: 200, headers: nil) + } + sut.apiClient = stubbedAPIClient() + // tap next button + let nextButton = sut.navigationItem.rightBarButtonItem + _ = nextButton?.target?.perform(nextButton?.action, with: nextButton) + + waitForExpectations(timeout: 2, handler: nil) + + // It takes a few more spins on the runloop before we get a response from + // the HTTP stubs, so we'll wait 0.5 seconds before checking the loading indicator. + let loadExp = expectation(description: "loading has stopped") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + XCTAssertFalse(sut.loading) + loadExp.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + } + + func testNextWithCreatePaymentMethodSuccessAndDidCreatePaymentMethodError() { + let sut = buildAddCardViewController()! + let createPaymentMethodExp = expectation(description: "createPaymentMethodWithCard") + + let expectedCardParams = STPFixtures.paymentMethodCardParams() + let expectedPaymentMethod = STPFixtures.paymentMethod() + let expectedPaymentMethodData = STPFixtures.paymentMethodJSON() + + stub { urlRequest in + return self.paymentMethodAPIFilter( + expectedCardParams: expectedCardParams, + urlRequest: urlRequest + ) + } response: { _ in + XCTAssertTrue(sut.loading) + defer { + createPaymentMethodExp.fulfill() + } + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodData, + statusCode: 200, + headers: nil + ) + } + + let mockDelegate = MockDelegate() + sut.apiClient = stubbedAPIClient() + sut.delegate = mockDelegate + sut.paymentCell?.paymentField!.perform(NSSelectorFromString("setCardParams:"), with: expectedCardParams) + + let didCreatePaymentMethodExp = expectation(description: "didCreatePaymentMethod") + + mockDelegate.addCardViewControllerDidCreatePaymentMethodBlock = { + (_, paymentMethod, completion) in + XCTAssertTrue(sut.loading) + let error = NSError.stp_genericFailedToParseResponseError() + XCTAssertEqual(paymentMethod.stripeId, expectedPaymentMethod.stripeId) + completion(error) + XCTAssertFalse(sut.loading) + didCreatePaymentMethodExp.fulfill() + } + + // tap next button + let nextButton = sut.navigationItem.rightBarButtonItem + _ = nextButton?.target?.perform(nextButton?.action, with: nextButton) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testNextWithCreateTokenSuccessAndDidCreateTokenSuccess() { + let sut = buildAddCardViewController()! + + let createPaymentMethodExp = expectation(description: "createPaymentMethodWithCard") + + let expectedCardParams = STPFixtures.paymentMethodCardParams() + let expectedPaymentMethod = STPFixtures.paymentMethod() + let expectedPaymentMethodData = STPFixtures.paymentMethodJSON() + + stub { urlRequest in + return self.paymentMethodAPIFilter( + expectedCardParams: expectedCardParams, + urlRequest: urlRequest + ) + } response: { _ in + XCTAssertTrue(sut.loading) + defer { + createPaymentMethodExp.fulfill() + } + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodData, + statusCode: 200, + headers: nil + ) + } + + let mockDelegate = MockDelegate() + sut.apiClient = stubbedAPIClient() + sut.delegate = mockDelegate + sut.paymentCell?.paymentField!.perform(NSSelectorFromString("setCardParams:"), with: expectedCardParams) + + let didCreatePaymentMethodExp = expectation(description: "didCreatePaymentMethod") + mockDelegate.addCardViewControllerDidCreatePaymentMethodBlock = { + (_, paymentMethod, completion) in + XCTAssertTrue(sut.loading) + XCTAssertEqual(paymentMethod.stripeId, expectedPaymentMethod.stripeId) + completion(nil) + XCTAssertFalse(sut.loading) + didCreatePaymentMethodExp.fulfill() + } + + // tap next button + let nextButton = sut.navigationItem.rightBarButtonItem + _ = nextButton?.target?.perform(nextButton?.action, with: nextButton) + + waitForExpectations(timeout: 2, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPAddressTests.swift b/Stripe/StripeiOSTests/STPAddressTests.swift new file mode 100644 index 00000000..9817de51 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAddressTests.swift @@ -0,0 +1,523 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPAddressTests.m +// Stripe +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import Contacts +import PassKit + +class STPAddressTests: XCTestCase { + func testInitWithPKContact_complete() { + let contact = PKContact() + do { + var name = PersonNameComponents() + name.givenName = "John" + name.familyName = "Doe" + contact.name = name + + contact.emailAddress = "foo@example.com" + contact.phoneNumber = CNPhoneNumber(stringValue: "888-555-1212") + + let address = CNMutablePostalAddress() + address.street = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.isoCountryCode = "US" + address.country = "United States" + contact.postalAddress = address + } + + let address = STPAddress(pkContact: contact) + XCTAssertEqual("John Doe", address.name) + XCTAssertEqual("8885551212", address.phone) + XCTAssertEqual("foo@example.com", address.email) + XCTAssertEqual("55 John St", address.line1) + XCTAssertEqual("New York", address.city) + XCTAssertEqual("NY", address.state) + XCTAssertEqual("10002", address.postalCode) + XCTAssertEqual("US", address.country) + } + + func testInitWithPKContact_partial() { + let contact = PKContact() + do { + var name = PersonNameComponents() + name.givenName = "John" + contact.name = name + + let address = CNMutablePostalAddress() + address.state = "VA" + contact.postalAddress = address + } + + let address = STPAddress(pkContact: contact) + XCTAssertEqual("John", address.name) + XCTAssertNil(address.phone) + XCTAssertNil(address.email) + XCTAssertNil(address.line1) + XCTAssertNil(address.city) + XCTAssertEqual("VA", address.state) + XCTAssertNil(address.postalCode) + XCTAssertNil(address.country) + } + + func testInitWithCNContact_complete() { + let contact = CNMutableContact() + do { + contact.givenName = "John" + contact.familyName = "Doe" + + contact.emailAddresses = [ + CNLabeledValue( + label: CNLabelHome, + value: "foo@example.com"), + CNLabeledValue( + label: CNLabelWork, + value: "bar@example.com"), + ] + + contact.phoneNumbers = [ + CNLabeledValue( + label: CNLabelHome, + value: CNPhoneNumber(stringValue: "888-555-1212")), + CNLabeledValue( + label: CNLabelWork, + value: CNPhoneNumber(stringValue: "555-555-5555")), + ] + + let address = CNMutablePostalAddress() + address.street = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.isoCountryCode = "US" + address.country = "United States" + contact.postalAddresses = [ + CNLabeledValue( + label: CNLabelHome, + value: address), + ] + } + + let address = STPAddress(cnContact: contact) + XCTAssertEqual("John Doe", address.name) + XCTAssertEqual("8885551212", address.phone) + XCTAssertEqual("foo@example.com", address.email) + XCTAssertEqual("55 John St", address.line1) + XCTAssertEqual("New York", address.city) + XCTAssertEqual("NY", address.state) + XCTAssertEqual("10002", address.postalCode) + XCTAssertEqual("US", address.country) + } + + func testInitWithCNContact_partial() { + let contact = CNMutableContact() + do { + contact.givenName = "John" + + let address = CNMutablePostalAddress() + address.state = "VA" + contact.postalAddresses = [ + CNLabeledValue( + label: CNLabelHome, + value: address), + ] + } + + let address = STPAddress(cnContact: contact) + XCTAssertEqual("John", address.name) + XCTAssertNil(address.phone) + XCTAssertNil(address.email) + XCTAssertNil(address.line1) + XCTAssertNil(address.city) + XCTAssertEqual("VA", address.state) + XCTAssertNil(address.postalCode) + XCTAssertNil(address.country) + } + + func testPKContactValue() { + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let contact = address.pkContactValue() + XCTAssertEqual(contact.name?.givenName, "John") + XCTAssertEqual(contact.name?.familyName, "Smith Doe") + XCTAssertEqual(contact.phoneNumber?.stringValue, "8885551212") + XCTAssertEqual(contact.emailAddress, "foo@example.com") + let postalAddress = contact.postalAddress + XCTAssertEqual(postalAddress?.street, "55 John St") + XCTAssertEqual(postalAddress?.city, "New York") + XCTAssertEqual(postalAddress?.state, "NY") + XCTAssertEqual(postalAddress?.postalCode, "10002") + XCTAssertEqual(postalAddress?.country, "US") + } + + func testContainsRequiredFieldsNone() { + let address = STPAddress() + XCTAssertTrue(address.containsRequiredFields(.none)) + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + address.phone = "8885551212" + address.email = "foo@example.com" + address.name = "John Doe" + XCTAssertTrue(address.containsRequiredFields(.none)) + address.country = "UK" + XCTAssertTrue(address.containsRequiredFields(.none)) + } + + func testContainsRequiredFieldsZip() { + let address = STPAddress() + + // nil country is treated as generic postal requirement + XCTAssertFalse(address.containsRequiredFields(.postalCode)) + address.country = "IE" // should pass for country which doesn't require zip/postal + XCTAssertTrue(address.containsRequiredFields(.postalCode)) + address.country = "US" + XCTAssertFalse(address.containsRequiredFields(.postalCode)) + address.postalCode = "10002" + XCTAssertTrue(address.containsRequiredFields(.postalCode)) + address.postalCode = "ABCDE" + XCTAssertFalse(address.containsRequiredFields(.postalCode)) + address.country = "UK" // should pass for alphanumeric countries + XCTAssertTrue(address.containsRequiredFields(.postalCode)) + address.country = nil // nil treated as alphanumeric + XCTAssertTrue(address.containsRequiredFields(.postalCode)) + } + + func testContainsRequiredFieldsFull() { + let address = STPAddress() + + /// Required fields for full are: + /// line1, city, country, state (US only) and a valid postal code (based on country) + + XCTAssertFalse(address.containsRequiredFields(.full)) + address.country = "US" + address.line1 = "55 John St" + + // Fail on partial + XCTAssertFalse(address.containsRequiredFields(.full)) + + address.city = "New York" + + // For US fail if missing state or zip + XCTAssertFalse(address.containsRequiredFields(.full)) + address.state = "NY" + XCTAssertFalse(address.containsRequiredFields(.full)) + address.postalCode = "ABCDE" + XCTAssertFalse(address.containsRequiredFields(.full)) + // postal must be numeric for US + address.postalCode = "10002" + XCTAssertTrue(address.containsRequiredFields(.full)) + address.phone = "8885551212" + address.email = "foo@example.com" + address.name = "John Doe" + // Name/phone/email should have no effect + XCTAssertTrue(address.containsRequiredFields(.full)) + + // Non US countries don't require state + address.country = "UK" + XCTAssertTrue(address.containsRequiredFields(.full)) + address.state = nil + XCTAssertTrue(address.containsRequiredFields(.full)) + // alphanumeric postal ok in some countries + address.postalCode = "ABCDE" + XCTAssertTrue(address.containsRequiredFields(.full)) + // UK requires ZIP + address.postalCode = nil + XCTAssertFalse(address.containsRequiredFields(.full)) + + address.country = "IE" // Doesn't require postal or state, but allows them + XCTAssertTrue(address.containsRequiredFields(.full)) + address.postalCode = "ABCDE" + XCTAssertTrue(address.containsRequiredFields(.full)) + address.state = "Test" + XCTAssertTrue(address.containsRequiredFields(.full)) + } + + func testContainsRequiredFieldsName() { + let address = STPAddress() + + XCTAssertFalse(address.containsRequiredFields(.name)) + address.name = "Jane Doe" + XCTAssertTrue(address.containsRequiredFields(.name)) + } + + func testContainsContentForBillingAddressFields() { + let address = STPAddress() + + // Empty address should return false for everything + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertFalse(address.containsContent(for: .postalCode)) + XCTAssertFalse(address.containsContent(for: .full)) + XCTAssertFalse(address.containsContent(for: .name)) + + // 1+ characters in postalCode will return true for .PostalCode && .Full + address.postalCode = "0" + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertTrue(address.containsContent(for: .postalCode)) + XCTAssertTrue(address.containsContent(for: .full)) + // empty string returns false + address.postalCode = "" + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertFalse(address.containsContent(for: .postalCode)) + XCTAssertFalse(address.containsContent(for: .full)) + address.postalCode = nil + + // 1+ characters in name will return true for .Name + address.name = "Jane Doe" + XCTAssertTrue(address.containsContent(for: .name)) + // empty string returns false + address.name = "" + XCTAssertFalse(address.containsContent(for: .name)) + address.name = nil + + // Test every other property that contributes to the full address, ensuring it returns True for .Full only + // This is *not* refactoring-safe, but I think it's better than a bunch of duplicated code + for propertyName in ["line1", "line2", "city", "state", "country"] { + for testValue in ["a", "0", "Foo Bar"] { + address.setValue(testValue, forKey: propertyName) + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertFalse(address.containsContent(for: .postalCode)) + XCTAssertTrue(address.containsContent(for: .full)) + XCTAssertFalse(address.containsContent(for: .name)) + address.setValue(nil, forKey: propertyName) + } + + // Make sure that empty string is treated like nil, and returns false for these properties + address.setValue("", forKey: propertyName) + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertFalse(address.containsContent(for: .postalCode)) + XCTAssertFalse(address.containsContent(for: .full)) + XCTAssertFalse(address.containsContent(for: .name)) + address.setValue(nil, forKey: propertyName) + } + + // ensure it still returns false for everything since it has been cleared + XCTAssertFalse(address.containsContent(for: .none)) + XCTAssertFalse(address.containsContent(for: .postalCode)) + XCTAssertFalse(address.containsContent(for: .full)) + XCTAssertFalse(address.containsContent(for: .name)) + } + + func testContainsRequiredShippingAddressFields() { + let address = STPAddress() + XCTAssertTrue(address.containsRequiredShippingAddressFields(nil)) + let allFields = Set([ + STPContactField.postalAddress, + STPContactField.emailAddress, + STPContactField.phoneNumber, + STPContactField.name, + ]) + XCTAssertFalse(address.containsRequiredShippingAddressFields(allFields)) + + address.name = "John Smith" + XCTAssertTrue((address.containsRequiredShippingAddressFields(Set([STPContactField.name])))) + XCTAssertFalse((address.containsRequiredShippingAddressFields(Set([STPContactField.emailAddress])))) + + address.email = "john@example.com" + XCTAssertTrue((address.containsRequiredShippingAddressFields(Set([STPContactField.name, STPContactField.emailAddress])))) + XCTAssertFalse((address.containsRequiredShippingAddressFields(allFields))) + + address.phone = "5555555555" + XCTAssertTrue((address.containsRequiredShippingAddressFields(Set([ + STPContactField.name, + STPContactField.emailAddress, + STPContactField.phoneNumber, + ])))) + address.phone = "555" + XCTAssertFalse((address.containsRequiredShippingAddressFields(Set([ + STPContactField.name, + STPContactField.emailAddress, + STPContactField.phoneNumber, + ])))) + XCTAssertFalse((address.containsRequiredShippingAddressFields(allFields))) + address.country = "GB" + XCTAssertTrue((address.containsRequiredShippingAddressFields(Set([STPContactField.name, STPContactField.emailAddress])))) + address.phone = "5555555555" + XCTAssertTrue((address.containsRequiredShippingAddressFields(Set([ + STPContactField.name, + STPContactField.emailAddress, + STPContactField.phoneNumber, + ])))) + + address.country = "US" + address.phone = "5555555555" + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "12345" + XCTAssertTrue(address.containsRequiredShippingAddressFields(allFields)) + } + + func testContainsContentForShippingAddressFields() { + let address = STPAddress() + + // Empty address should return false for everything + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + + // Name + address.name = "Smith" + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + address.name = "" + + // Phone + address.phone = "1" + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + address.phone = "" + + // Email + address.email = "f" + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + address.email = "" + + // Test every property that contributes to the full address + // This is *not* refactoring-safe, but I think it's better than a bunch more duplicated code + for propertyName in ["line1", "line2", "city", "state", "postalCode", "country"] { + for testValue in ["a", "0", "Foo Bar"] { + address.setValue(testValue, forKey: propertyName) + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + address.setValue("", forKey: propertyName) + } + } + + // ensure it still returns false for everything with empty strings + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + + // Try a hybrid address, and make sure some bitwise combinations work + address.name = "a" + address.phone = "1" + address.line1 = "_" + XCTAssertFalse((address.containsContent(forShippingAddressFields: nil))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.name])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber])))) + XCTAssertFalse((address.containsContent(forShippingAddressFields: Set([STPContactField.emailAddress])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.postalAddress])))) + + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.name, STPContactField.emailAddress])))) + XCTAssertTrue((address.containsContent(forShippingAddressFields: Set([STPContactField.phoneNumber, STPContactField.emailAddress])))) + XCTAssertTrue( + (address.containsContent( + forShippingAddressFields: Set([ + STPContactField.postalAddress, + STPContactField.emailAddress, + STPContactField.phoneNumber, + STPContactField.name, + ])))) + + } + + func testShippingInfoForCharge() { + let address = STPFixtures.address() + let method = PKShippingMethod() + method.label = "UPS Ground" + let info = STPAddress.shippingInfoForCharge( + with: address, + shippingMethod: method) as NSDictionary? + let expected: NSDictionary = [ + "address": [ + "city": address.city, + "country": address.country, + "line1": address.line1, + "line2": address.line2, + "postal_code": address.postalCode, + "state": address.state, + ], + "name": address.name as Any, + "phone": address.phone as Any, + "carrier": method.label, + ] + XCTAssertEqual(expected, info) + } + + // MARK: STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertNil(STPAddress.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let address = STPAddress() + + let mapping = STPAddress.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(address.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual(mapping.values.count, mapping.values.count) + } + + // MARK: NSCopying Tests + + func testCopyWithZone() { + let address = STPFixtures.address() + let copiedAddress = address.copy() as! STPAddress + + XCTAssertNotEqual(address, copiedAddress, "should be different objects") + + // The property names we expect to *not* be equal objects + let notEqualProperties = [ + // these include the object's address, so they won't be the same across copies + "debugDescription", + "description", + "hash", + ] + // use runtime inspection to find the list of properties. If a new property is + // added to the fixture, but not the `copyWithZone:` implementation, this should catch it + for property in STPTestUtils.propertyNames(of: address) { + if notEqualProperties.contains(property) { + XCTAssertNotEqual( + address.value(forKey: property) as! NSObject, + copiedAddress.value(forKey: property) as! NSObject) + } else { + XCTAssertEqual( + address.value(forKey: property) as! NSObject, + copiedAddress.value(forKey: property) as! NSObject) + } + } + } +} diff --git a/Stripe/StripeiOSTests/STPAddressViewModelTest.swift b/Stripe/StripeiOSTests/STPAddressViewModelTest.swift new file mode 100644 index 00000000..429368e2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAddressViewModelTest.swift @@ -0,0 +1,205 @@ +// +// STPAddressViewModelTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 10/21/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAddressViewModelTest: XCTestCase { + func testInitWithRequiredBillingFields() { + var sut = STPAddressViewModel(requiredBillingFields: .none, availableCountries: nil) + XCTAssertTrue(sut.addressCells.count == 0) + XCTAssertTrue(sut.isValid) + + sut = STPAddressViewModel(requiredBillingFields: .postalCode, availableCountries: nil) + XCTAssertTrue(sut.addressCells.count == 0) + + sut = STPAddressViewModel(requiredBillingFields: .full, availableCountries: nil) + XCTAssertTrue(sut.addressCells.count == 7) + let types: [STPAddressFieldType] = [ + .name, + .line1, + .line2, + .country, + .zip, + .city, + .state, + ] + for i in 0..(), + availableCountries: nil + ) + XCTAssertTrue(sut.addressCells.count == 0) + + sut = STPAddressViewModel( + requiredShippingFields: Set([.name]), + availableCountries: nil + ) + XCTAssertTrue(sut.addressCells.count == 1) + let cell1 = sut.addressCells[0] + XCTAssertEqual(cell1.type, .name) + + sut = STPAddressViewModel( + requiredShippingFields: Set([.name, .emailAddress]), + availableCountries: nil + ) + XCTAssertTrue(sut.addressCells.count == 2) + var types: [STPAddressFieldType] = [.name, .email] + for i in 0..([ + .postalAddress, .emailAddress, .phoneNumber, + ]), + availableCountries: nil + ) + XCTAssertTrue(sut.addressCells.count == 9) + types = [ + .email, + .name, + .line1, + .line2, + .country, + .zip, + .city, + .state, + .phone, + ] + for i in 0..([ + .postalAddress, + .emailAddress, + .phoneNumber, + ]), + availableCountries: nil + ) + sut.addressCells[0].contents = "foo@example.com" + sut.addressCells[1].contents = "John Smith" + sut.addressCells[2].contents = "55 John St" + sut.addressCells[3].contents = "#3B" + sut.addressCells[4].contents = "US" + sut.addressCells[5].contents = "10002" + sut.addressCells[6].contents = "New York" + sut.addressCells[7].contents = "NY" + sut.addressCells[8].contents = "555-555-5555" + + XCTAssertEqual(sut.address.email, "foo@example.com") + XCTAssertEqual(sut.address.name, "John Smith") + XCTAssertEqual(sut.address.line1, "55 John St") + XCTAssertEqual(sut.address.line2, "#3B") + XCTAssertEqual(sut.address.city, "New York") + XCTAssertEqual(sut.address.state, "NY") + XCTAssertEqual(sut.address.postalCode, "10002") + XCTAssertEqual(sut.address.country, "US") + XCTAssertEqual(sut.address.phone, "555-555-5555") + } + + func testSetAddress() { + let address = STPAddress() + address.email = "foo@example.com" + address.name = "John Smith" + address.line1 = "55 John St" + address.line2 = "#3B" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + address.phone = "555-555-5555" + + let sut = STPAddressViewModel( + requiredShippingFields: Set([ + .postalAddress, + .emailAddress, + .phoneNumber, + ]), + availableCountries: nil + ) + sut.address = address + XCTAssertEqual(sut.addressCells[0].contents, "foo@example.com") + XCTAssertEqual(sut.addressCells[1].contents, "John Smith") + XCTAssertEqual(sut.addressCells[2].contents, "55 John St") + XCTAssertEqual(sut.addressCells[3].contents, "#3B") + XCTAssertEqual(sut.addressCells[4].contents, "US") + XCTAssertEqual(sut.addressCells[4].textField.text, "United States") + XCTAssertEqual(sut.addressCells[5].contents, "10002") + XCTAssertEqual(sut.addressCells[6].contents, "New York") + XCTAssertEqual(sut.addressCells[7].contents, "NY") + XCTAssertEqual(sut.addressCells[8].contents, "555-555-5555") + } + + func testIsValid_Zip() { + let sut = STPAddressViewModel(requiredBillingFields: .postalCode, availableCountries: nil) + + let address = STPAddress() + + address.country = "US" + sut.address = address + // The AddressViewModel shouldn't request any information when requesting ZIPs. + XCTAssertEqual(sut.addressCells.count, 0) + + address.postalCode = "94016" + sut.address = address + XCTAssertTrue(sut.isValid) + + address.country = "MO" // in Macao, postalCode is optional + address.postalCode = nil + sut.address = address + XCTAssertEqual(sut.addressCells.count, 0) + XCTAssertTrue(sut.isValid, "in Macao, postalCode is optional, valid without one") + } + + func testIsValid_Full() { + let sut = STPAddressViewModel(requiredBillingFields: .full, availableCountries: nil) + XCTAssertFalse(sut.isValid) + sut.addressCells[0].contents = "John Smith" + sut.addressCells[1].contents = "55 John St" + sut.addressCells[2].contents = "#3B" + XCTAssertFalse(sut.isValid) + sut.addressCells[3].contents = "10002" + sut.addressCells[4].contents = "New York" + sut.addressCells[5].contents = "NY" + sut.addressCells[6].contents = "US" + XCTAssertTrue(sut.isValid) + } + + func testIsValid_Name() { + let sut = STPAddressViewModel(requiredBillingFields: .name, availableCountries: nil) + + let address = STPAddress() + + address.name = "" + sut.address = address + XCTAssertEqual(sut.addressCells.count, 1) + XCTAssertFalse(sut.isValid) + + address.name = "Jane Doe" + sut.address = address + XCTAssertEqual(sut.addressCells.count, 1) + XCTAssertTrue(sut.isValid) + } +} diff --git a/Stripe/StripeiOSTests/STPAnalyticsClientPaymentsTest.swift b/Stripe/StripeiOSTests/STPAnalyticsClientPaymentsTest.swift new file mode 100644 index 00000000..5ab6e8b5 --- /dev/null +++ b/Stripe/StripeiOSTests/STPAnalyticsClientPaymentsTest.swift @@ -0,0 +1,247 @@ +// +// STPAnalyticsClientPaymentsTest.swift +// StripeiOS Tests +// +// Created by Mel Ludowise on 5/26/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import StripeApplePay +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPAnalyticsClientPaymentsTest: XCTestCase { + private var client: STPAnalyticsClient! + + override func setUp() { + super.setUp() + client = STPAnalyticsClient() + } + + func testAdditionalInfo() { + XCTAssertEqual(client.additionalInfo(), []) + + // Add some info + client.addAdditionalInfo("hello") + client.addAdditionalInfo("i'm additional info") + client.addAdditionalInfo("how are you?") + + XCTAssertEqual(client.additionalInfo(), ["hello", "how are you?", "i'm additional info"]) + + // Clear it + client.clearAdditionalInfo() + XCTAssertEqual(client.additionalInfo(), []) + } + + func testPayloadFromAnalytic() throws { + AnalyticsHelper.shared.generateSessionID() + + client.addAdditionalInfo("test_additional_info") + + let mockAnalytic = MockAnalytic() + let payload = client.payload(from: mockAnalytic) + + XCTAssertEqual(payload.count, 17) + + // Verify event name is included + XCTAssertEqual(payload["event"] as? String, mockAnalytic.event.rawValue) + + // Verify additionalInfo is included + XCTAssertEqual(payload["additional_info"] as? [String], ["test_additional_info"]) + + // Verify all the analytic params are in the payload + XCTAssertEqual(payload["test_param1"] as? Int, 1) + XCTAssertEqual(payload["test_param2"] as? String, "two") + + // Verify productUsage is included + XCTAssertNotNil(payload["product_usage"]) + + // Verify install method is Xcode + XCTAssertEqual(payload["install"] as? String, "X") + + // Verify is_development + XCTAssertTrue(payload["is_development"] as? Bool ?? false) + + // Verify locale + XCTAssertEqual(payload["locale"] as? String, Locale.autoupdatingCurrent.identifier) + } + + // MARK: - Error tests + + enum MockError: Error { + case someErrorCase + } + + func testLogErrorAnalytic() { + let error = MockError.someErrorCase + let errorAnalytic = ErrorAnalytic(event: .luxeSerializeFailure, error: error) + let payload = client.payload(from: errorAnalytic) + + // Verify payload event name is correct + XCTAssertEqual(payload["event"] as? String, STPAnalyticEvent.luxeSerializeFailure.rawValue) + + // Verify error details are included + XCTAssertEqual(payload["error_type"] as? String, "StripeiOS_Tests.STPAnalyticsClientPaymentsTest.MockError") + XCTAssertEqual(payload["error_code"] as? String, "someErrorCase") + + let errorAnalyticWithAdditionalParams = ErrorAnalytic(event: .luxeSerializeFailure, error: error, additionalNonPIIParams: ["additional_param": "value"]) + let payloadWithAdditionalParams = client.payload(from: errorAnalyticWithAdditionalParams) + + // Verify additional params in ErrorAnalytic are included + XCTAssertEqual(payloadWithAdditionalParams["additional_param"] as? String, "value") + } + + // MARK: - Other tests + + func testTokenTypeFromParameters() { + let card = STPFixtures.cardParams() + let cardDict = buildTokenParams(card) + XCTAssertEqual(STPAnalyticsClient.tokenType(fromParameters: cardDict), "card") + + let account = STPFixtures.accountParams() + let accountDict = buildTokenParams(account) + XCTAssertEqual(STPAnalyticsClient.tokenType(fromParameters: accountDict), "account") + + let bank = STPFixtures.bankAccountParams() + let bankDict = buildTokenParams(bank) + XCTAssertEqual(STPAnalyticsClient.tokenType(fromParameters: bankDict), "bank_account") + + let applePay = STPFixtures.applePayPayment() + let applePayDict = addTelemetry(applePay.stp_tokenParameters(apiClient: .shared)) + XCTAssertEqual(STPAnalyticsClient.tokenType(fromParameters: applePayDict), "apple_pay") + } + + // MARK: - Tests various classes report usage + + func testCardTextFieldAddsUsage() { + _ = STPPaymentCardTextField() + XCTAssertTrue( + STPAnalyticsClient.sharedClient.productUsage.contains("STPPaymentCardTextField") + ) + } + + func testPaymentContextAddsUsage() { + let keyManager = STPEphemeralKeyManager( + keyProvider: MockKeyProvider(), + apiVersion: "1", + performsEagerFetching: false + ) + let apiClient = STPAPIClient() + let customerContext = STPCustomerContext.init(keyManager: keyManager, apiClient: apiClient) + let paymentContext = STPPaymentContext(customerContext: customerContext) + XCTAssertTrue(STPAnalyticsClient.sharedClient.productUsage.contains("STPCustomerContext")) + XCTAssertEqual(paymentContext.analyticsLogger.product, "STPPaymentContext") + } + + func testApplePayContextAddsUsage() { + _ = STPApplePayContext(paymentRequest: STPFixtures.applePayRequest(), delegate: nil) + XCTAssertTrue(STPAnalyticsClient.sharedClient.productUsage.contains("STPApplePayContext")) + } + + func testCustomerContextAddsUsage() { + let keyManager = STPEphemeralKeyManager( + keyProvider: MockKeyProvider(), + apiVersion: "1", + performsEagerFetching: false + ) + let apiClient = STPAPIClient() + _ = STPCustomerContext(keyManager: keyManager, apiClient: apiClient) + XCTAssertTrue(STPAnalyticsClient.sharedClient.productUsage.contains("STPCustomerContext")) + } + + func testAddCardVCAddsUsage() { + let addCardVC = STPAddCardViewController() + XCTAssertTrue( + STPAnalyticsClient.sharedClient.productUsage.contains("STPAddCardViewController") + ) + XCTAssertEqual(addCardVC.analyticsLogger.product, "STPAddCardViewController") + } + + func testPaymentOptionsVCAddsUsage() { + let customerContext = Testing_StaticCustomerContext.init( + customer: STPFixtures.customerWithCardTokenAndSourceSources(), + paymentMethods: [] + ) + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let paymentOptionsVC = STPPaymentOptionsViewController(configuration: .shared, theme: .defaultTheme, customerContext: customerContext, delegate: delegate) + XCTAssertTrue( + STPAnalyticsClient.sharedClient.productUsage.contains("STPPaymentOptionsViewController") + ) + XCTAssertEqual(paymentOptionsVC.analyticsLogger.product, "STPPaymentOptionsViewController") + } + + func testBankSelectionVCAddsUsage() { + _ = STPBankSelectionViewController() + XCTAssertTrue( + STPAnalyticsClient.sharedClient.productUsage.contains("STPBankSelectionViewController") + ) + } + + func testShippingVCAddsUsage() { + let config = STPPaymentConfiguration() + config.requiredShippingAddressFields = [STPContactField.postalAddress] + _ = STPShippingAddressViewController( + configuration: config, + theme: .defaultTheme, + currency: nil, + shippingAddress: nil, + selectedShippingMethod: nil, + prefilledInformation: nil + ) + XCTAssertTrue( + STPAnalyticsClient.sharedClient.productUsage.contains( + "STPShippingAddressViewController" + ) + ) + } +} + +// MARK: - Helpers + +extension STPAnalyticsClientPaymentsTest { + fileprivate func buildTokenParams(_ object: T) -> [String: Any] + { + return addTelemetry(STPFormEncoder.dictionary(forObject: object)) + } + + fileprivate func addTelemetry(_ params: [String: Any]) -> [String: Any] { + // STPAPIClient adds these before determining the token type, + // so do the same in the test + return STPTelemetryClient.shared.paramsByAddingTelemetryFields(toParams: params) + } +} + +// MARK: - Mock types + +private struct MockAnalytic: Analytic { + let event = STPAnalyticEvent.sourceCreation + + let params: [String: Any] = [ + "test_param1": 1, + "test_param2": "two", + ] +} + +private struct MockAnalyticsClass1: STPAnalyticsProtocol { + static let stp_analyticsIdentifier = "MockAnalyticsClass1" +} + +private struct MockAnalyticsClass2: STPAnalyticsProtocol { + static let stp_analyticsIdentifier = "MockAnalyticsClass2" +} + +private class MockKeyProvider: NSObject, STPCustomerEphemeralKeyProvider { + func createCustomerKey( + withAPIVersion apiVersion: String, + completion: @escaping STPJSONResponseCompletionBlock + ) { + guard apiVersion == "1" else { return } + + completion(nil, NSError.stp_genericConnectionError()) + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayContextFunctionalTest.swift b/Stripe/StripeiOSTests/STPApplePayContextFunctionalTest.swift new file mode 100644 index 00000000..83c5c034 --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayContextFunctionalTest.swift @@ -0,0 +1,362 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPApplePayContextFunctionalTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/5/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import OHHTTPStubs +@testable import Stripe +@testable import StripeApplePay +@testable import StripeCoreTestUtils +@testable import StripePayments +@testable import StripePaymentsObjcTestUtils +@testable import StripePaymentsTestUtils + +class STPTestApplePayContextDelegate: NSObject, STPApplePayContextDelegate { + func applePayContext(_ context: StripeApplePay.STPApplePayContext, didCreatePaymentMethod paymentMethod: StripePayments.STPPaymentMethod, paymentInformation: PKPayment, completion: @escaping StripeApplePay.STPIntentClientSecretCompletionBlock) { + didCreatePaymentMethodDelegateMethod!(paymentMethod, paymentInformation, completion) + } + + func applePayContext(_ context: StripeApplePay.STPApplePayContext, didCompleteWith status: StripePayments.STPPaymentStatus, error: Error?) { + didCompleteDelegateMethod!(status, error) + } + + var didCompleteDelegateMethod: ((_ status: STPPaymentStatus, _ error: Error?) -> Void)? + var didCreatePaymentMethodDelegateMethod: ((_ paymentMethod: STPPaymentMethod?, _ paymentInformation: PKPayment?, _ completion: @escaping STPIntentClientSecretCompletionBlock) -> Void)? +} + +class STPApplePayContextFunctionalTest: STPNetworkStubbingTestCase { + var apiClient: STPApplePayContextFunctionalTestAPIClient! + var delegate: STPTestApplePayContextDelegate! + var context: STPApplePayContext! + + override func setUp() { + super.setUp() + delegate = STPTestApplePayContextDelegate() + let apiClient = STPApplePayContextFunctionalTestAPIClient(publishableKey: STPTestingDefaultPublishableKey) + apiClient.setupStubs() + apiClient.applePayContext = context + self.apiClient = apiClient + + context = STPApplePayContext(paymentRequest: STPFixtures.applePayRequest(), delegate: delegate) + self.apiClient.applePayContext = context + context?.apiClient = self.apiClient + context?.authorizationController = STPTestPKPaymentAuthorizationController() + } + + override func tearDown() { + HTTPStubs.removeAllStubs() + super.tearDown() + } + + func testCompletesManualConfirmationPaymentIntent() { + var clientSecret: String? + // A manual confirmation PI confirmed server-side... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { paymentMethod, paymentInformation, completion in + XCTAssertNotNil(paymentInformation) + if let stripeId = paymentMethod?.stripeId { + STPTestingAPIClient.shared.createPaymentIntent(withParams: [ + "confirmation_method": "manual", + "payment_method": stripeId, + "confirm": NSNumber(value: true), + ]) { _clientSecret, _ in + XCTAssertNotNil(_clientSecret) + clientSecret = _clientSecret + completion(clientSecret, nil) + } + } + } + + // ...used with ApplePayContext + let context = STPApplePayContext(paymentRequest: STPFixtures.applePayRequest(), delegate: self.delegate)! + context.apiClient = apiClient + _startApplePayForContext(withExpectedStatus: .success) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful PI + apiClient?.retrievePaymentIntent(withClientSecret: clientSecret!) { paymentIntent, paymentIntentRetrieveError in + XCTAssertNil(paymentIntentRetrieveError) + XCTAssert(paymentIntent?.status == .succeeded) + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCompletesAutomaticConfirmationPaymentIntent() { + var clientSecret: String? + // An automatic confirmation PI with the PaymentMethod attached... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { newClientSecret, _ in + clientSecret = newClientSecret + completion(newClientSecret, nil) + } + } + + // ...used with ApplePayContext + _startApplePayForContext(withExpectedStatus: .success) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful PI + apiClient?.retrievePaymentIntent(withClientSecret: clientSecret!) { paymentIntent, paymentIntentRetrieveError in + XCTAssertNil(paymentIntentRetrieveError) + XCTAssert(paymentIntent?.status == .succeeded) + XCTAssertEqual(paymentIntent?.shipping?.name, "Jane Doe") + XCTAssertEqual(paymentIntent?.shipping?.address?.line1, "510 Townsend St") + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCompletesAutomaticConfirmationPaymentIntentManualCapture() { + var clientSecret: String? + // An automatic confirmation PI with the PaymentMethod attached... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + STPTestingAPIClient.shared.createPaymentIntent(withParams: ["capture_method": "manual"]) { newClientSecret, _ in + clientSecret = newClientSecret + completion(newClientSecret, nil) + } + } + + // ...used with ApplePayContext + _startApplePayForContext(withExpectedStatus: .success) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful PI + apiClient?.retrievePaymentIntent(withClientSecret: clientSecret!) { paymentIntent, paymentIntentRetrieveError in + XCTAssertNil(paymentIntentRetrieveError) + XCTAssert(paymentIntent?.status == .requiresCapture) + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCompletesSetupIntent() { + var clientSecret: String? + // An automatic confirmation SI... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + STPTestingAPIClient.shared.createSetupIntent(withParams: nil) { newClientSecret, _ in + clientSecret = newClientSecret + completion(newClientSecret, nil) + } + } + + // ...used with ApplePayContext + _startApplePayForContext(withExpectedStatus: .success) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful PI + apiClient?.retrieveSetupIntent(withClientSecret: clientSecret!) { setupIntent, setupIntentRetrieveError in + XCTAssertNil(setupIntentRetrieveError) + XCTAssert(setupIntent?.status == .succeeded) + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Error tests + + func testBadPaymentIntentClientSecretErrors() { + var clientSecret: String? + // An invalid PaymentIntent client secret... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + DispatchQueue.main.async { + clientSecret = "pi_bad_secret_1234" + completion(clientSecret, nil) + } + } + + // ...used with ApplePayContext + _startApplePayForContext(withExpectedStatus: .failure) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { status, error in + // ...and results in an error + XCTAssertEqual(status, .error) + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + didCallCompletion.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testBadSetupIntentClientSecretErrors() { + var clientSecret: String? + // An invalid SetupIntent client secret... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + DispatchQueue.main.async { + clientSecret = "seti_bad_secret_1234" + completion(clientSecret, nil) + } + } + + // ...used with ApplePayContext + _startApplePayForContext(withExpectedStatus: .failure) + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { status, error in + // ...and results in an error + XCTAssertEqual(status, .error) + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + didCallCompletion.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Cancel tests + + func testCancelBeforeIntentConfirmsCancels() { + // Cancelling Apple Pay *before* the context attempts to confirms the PI/SI... + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + self.context.paymentAuthorizationControllerDidFinish(self.context.authorizationController!) // Simulate cancel before passing PI to the context + // ...should never retrieve the PI (b/c it is cancelled before) + completion("A 'client secret' that triggers an exception if fetched", nil) + } + // Simulate user tapping 'Pay' button in Apple Pay + self.context.paymentAuthorizationController(self.context.authorizationController!, didAuthorizePayment: STPFixtures.simulatorApplePayPayment()) { _ in } + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { status, error in + // ...and results in a 'user cancel' status + XCTAssertEqual(status, .userCancellation) + XCTAssertNil(error) + didCallCompletion.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCancelAfterPaymentIntentConfirmsStillSucceeds() { + // Cancelling Apple Pay *after* the context attempts to confirm the PI... + apiClient?.shouldSimulateCancelAfterConfirmBegins = true + + var clientSecret: String? + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { newClientSecret, _ in + clientSecret = newClientSecret + completion(newClientSecret, nil) + } + } + // Simulate user tapping 'Pay' button in Apple Pay + self.context.paymentAuthorizationController(self.context.authorizationController!, didAuthorizePayment: STPFixtures.simulatorApplePayPayment()) { _ in } + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful PI + apiClient?.retrievePaymentIntent(withClientSecret: clientSecret!) { paymentIntent, paymentIntentRetrieveError in + XCTAssertNil(paymentIntentRetrieveError) + XCTAssert(paymentIntent?.status == .succeeded) + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: 20.0, handler: nil) // give this a longer timeout, it tends to take a while + } + + func testCancelAfterSetupIntentConfirmsStillSucceeds() { + // Cancelling Apple Pay *after* the context attempts to confirm the SI... + apiClient?.shouldSimulateCancelAfterConfirmBegins = true + + var clientSecret: String? + let delegate = self.delegate + delegate?.didCreatePaymentMethodDelegateMethod = { _, _, completion in + STPTestingAPIClient.shared.createSetupIntent(withParams: nil) { newClientSecret, _ in + clientSecret = newClientSecret + completion(newClientSecret, nil) + } + } + // Simulate user tapping 'Pay' button in Apple Pay + self.context.paymentAuthorizationController(self.context.authorizationController!, didAuthorizePayment: STPFixtures.simulatorApplePayPayment()) { _ in } + + // ...calls applePayContext:didCompleteWithStatus:error: + let didCallCompletion = expectation(description: "applePayContext:didCompleteWithStatus: called") + delegate?.didCompleteDelegateMethod = { [self] status, error in + XCTAssertEqual(status, .success) + XCTAssertNil(error) + + // ...and results in a successful SI + apiClient?.retrieveSetupIntent(withClientSecret: clientSecret!) { setupIntent, setupIntentRetrieveError in + XCTAssertNil(setupIntentRetrieveError) + XCTAssert(setupIntent?.status == .succeeded) + didCallCompletion.fulfill() + } + } + + waitForExpectations(timeout: 20.0, handler: nil) // give this a longer timeout, it tends to take a while + } + + // MARK: - Helper + + /// Simulates user tapping 'Pay' button in Apple Pay sheet + func _startApplePayForContext(withExpectedStatus expectedStatus: PKPaymentAuthorizationStatus) { + // When the user taps 'Pay', PKPaymentAuthorizationController calls `didAuthorizePayment:completion:` + // After you call its completion block, it calls `paymentAuthorizationControllerDidFinish:` + let didCallAuthorizePaymentCompletion = expectation(description: "ApplePayContext called completion block of paymentAuthorizationController:didAuthorizePayment:completion:") + if let authorizationController = context?.authorizationController { + context?.paymentAuthorizationController(authorizationController, didAuthorizePayment: STPFixtures.simulatorApplePayPayment(), handler: { [self] result in + XCTAssertEqual(expectedStatus, result.status) + DispatchQueue.main.async(execute: { [self] in + if let authorizationController = context?.authorizationController { + context?.paymentAuthorizationControllerDidFinish(authorizationController) + } + didCallAuthorizePaymentCompletion.fulfill() + }) + }) + } + } +} + +class STPTestPKPaymentAuthorizationController: PKPaymentAuthorizationController { + // Stub dismissViewControllerAnimated: to just call its completion block + override func dismiss(completion: (() -> Void)? = nil) { + completion?() + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayContextFunctionalTestExtras.swift b/Stripe/StripeiOSTests/STPApplePayContextFunctionalTestExtras.swift new file mode 100644 index 00000000..15a7394b --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayContextFunctionalTestExtras.swift @@ -0,0 +1,44 @@ +// +// STPApplePayContextFunctionalTestExtras.swift +// StripeiOS Tests +// +// Created by David Estes on 3/23/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPApplePayContextFunctionalTestAPIClient: STPAPIClient { + @objc var applePayContext: STPApplePayContext? + @objc var shouldSimulateCancelAfterConfirmBegins: Bool = false + + @objc func setupStubs() { + stub { urlRequest in + // Hook SetupIntent or PaymentIntent confirmation + if let urlString = urlRequest.url?.absoluteString, + urlString.contains("_intents/"), + urlString.hasSuffix("/confirm") + { + if self.shouldSimulateCancelAfterConfirmBegins { + self.applePayContext!.paymentAuthorizationControllerDidFinish( + self.applePayContext!.authorizationController! + ) + } + } + // Let everything pass through to the underlying API + return false + } response: { _ in + // This doesn't matter, we're not sending responses for anything. + return HTTPStubsResponse() + } + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayContextTest.swift b/Stripe/StripeiOSTests/STPApplePayContextTest.swift new file mode 100644 index 00000000..28296c1b --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayContextTest.swift @@ -0,0 +1,202 @@ +// +// STPApplePayContextTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 2/20/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripePayments + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPApplePayTestDelegateiOS11: NSObject, STPApplePayContextDelegate { + var didChangeCouponCodeCalled: Bool = false + func applePayContext( + _ context: STPApplePayContext, + didSelectShippingContact contact: PKContact, + handler completion: @escaping (PKPaymentRequestShippingContactUpdate) -> Void + ) { + completion(PKPaymentRequestShippingContactUpdate()) + } + + func applePayContext( + _ context: STPApplePayContext, + didSelect shippingMethod: PKShippingMethod, + handler completion: @escaping (PKPaymentRequestShippingMethodUpdate) -> Void + ) { + completion(PKPaymentRequestShippingMethodUpdate()) + } + + @available(iOS 15.0, *) + func applePayContext( + _ context: STPApplePayContext, + didChangeCouponCode couponCode: String, + handler completion: @escaping (PKPaymentRequestCouponCodeUpdate) -> Void + ) { + didChangeCouponCodeCalled = true + completion(.init(errors: nil, paymentSummaryItems: [], shippingMethods: [])) + } + + func applePayContext( + _ context: STPApplePayContext, + didCompleteWith status: STPPaymentStatus, + error: Error? + ) { + } + + func applePayContext( + _ context: STPApplePayContext, + didCreatePaymentMethod paymentMethod: STPPaymentMethod, + paymentInformation: PKPayment, + completion: STPIntentClientSecretCompletionBlock + ) { + } +} + +class STPApplePayContextTest: XCTestCase { + func testInvalidPaymentRequest() { + // An invalid request (missing payment summary items)... + let request = StripeAPI.paymentRequest( + withMerchantIdentifier: "foo", + country: "US", + currency: "USD" + ) + // ...should cause ApplePayContext to be nil + let applePayContext = STPApplePayContext(paymentRequest: request, delegate: STPApplePayTestDelegateiOS11()) + XCTAssertNil(applePayContext) + } + + // MARK: - STPApplePayTestDelegateiOS11 + func testiOS11ApplePayDelegateMethodsForwarded() { + // With a user that only implements iOS 11 delegate methods... + let delegate = STPApplePayTestDelegateiOS11() + let request = StripeAPI.paymentRequest( + withMerchantIdentifier: "foo", + country: "US", + currency: "USD" + ) + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "bar", amount: NSDecimalNumber(string: "1.00")), + ] + let context = STPApplePayContext(paymentRequest: request, delegate: delegate)! + + // ...the context should respondToSelector appropriately... + XCTAssertTrue( + context.responds( + to: #selector( + PKPaymentAuthorizationControllerDelegate.paymentAuthorizationController( + _: + didSelectShippingContact: + handler: + )) + ) + ) + XCTAssertFalse( + context.responds( + to: #selector( + PKPaymentAuthorizationControllerDelegate.paymentAuthorizationController( + _: + didSelectShippingContact: + completion: + )) + ) + ) + + // ...and forward the PassKit delegate method to its delegate + let vc: PKPaymentAuthorizationController = PKPaymentAuthorizationController() + // 1) ..didSelectShippingContact.. delegate method + let contact = PKContact() + let shippingContactExpectation = expectation( + description: "didSelectShippingContact forwarded" + ) + context.paymentAuthorizationController( + vc, + didSelectShippingContact: contact, + handler: { _ in + shippingContactExpectation.fulfill() + } + ) + + // 2) ..didSelectShippingMethod.. delegate method + let method = PKShippingMethod() + let shippingMethodExpectation = expectation( + description: "didSelectShippingMethod forwarded" + ) + context.paymentAuthorizationController( + vc, + didSelectShippingMethod: method, + handler: { _ in + shippingMethodExpectation.fulfill() + } + ) + + // 3) ..didChangeCouponCode.. delegate method + if #available(iOS 15.0, *) { + XCTAssertFalse(delegate.didChangeCouponCodeCalled) + let couponCodeExpectation = expectation(description: "didChangeCouponCode forwarded") + context.paymentAuthorizationController(vc, didChangeCouponCode: "coupon_123") { _ in + couponCodeExpectation.fulfill() + } + wait(for: [couponCodeExpectation], timeout: 1) + XCTAssertTrue(delegate.didChangeCouponCodeCalled) + } else { + // Fallback on earlier versions + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testConvertsShippingDetails() { + let delegate = STPApplePayTestDelegateiOS11() + let request = StripeAPI.paymentRequest( + withMerchantIdentifier: "foo", + country: "US", + currency: "USD" + ) + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "bar", amount: NSDecimalNumber(string: "1.00")), + ] + let context = STPApplePayContext(paymentRequest: request, delegate: delegate) + + let payment = STPFixtures.simulatorApplePayPayment() + let shipping = PKContact() + shipping.name = PersonNameComponentsFormatter().personNameComponents(from: "Jane Doe") + shipping.phoneNumber = CNPhoneNumber(stringValue: "555-555-5555") + let address = CNMutablePostalAddress() + address.street = "510 Townsend St" + address.city = "San Francisco" + address.state = "CA" + address.isoCountryCode = "US" + address.postalCode = "94105" + shipping.postalAddress = address + payment.perform(#selector(setter: PKPaymentRequest.shippingContact), with: shipping) + + let shippingParams = context!._shippingDetails(from: payment) + XCTAssertNotNil(shippingParams) + XCTAssertEqual(shippingParams?.name, "Jane Doe") + XCTAssertNil(shippingParams?.carrier) + XCTAssertEqual(shippingParams?.phone, "555-555-5555") + XCTAssertNil(shippingParams?.trackingNumber) + + XCTAssertEqual(shippingParams?.address.line1, "510 Townsend St") + XCTAssertNil(shippingParams?.address.line2) + XCTAssertEqual(shippingParams?.address.city, "San Francisco") + XCTAssertEqual(shippingParams?.address.state, "CA") + XCTAssertEqual(shippingParams?.address.country, "US") + XCTAssertEqual(shippingParams?.address.postalCode, "94105") + } + + // Tests stp_tokenParameters in StripeApplePay, not StripePayments + func testStpTokenParameters() { + let applePay = STPFixtures.applePayPayment() + let applePayDict = applePay.stp_tokenParameters(apiClient: .shared) + XCTAssertNotNil(applePayDict["pk_token"]) + XCTAssertEqual((applePayDict["card"] as! NSDictionary)["name"] as! String, "Test Testerson") + XCTAssertEqual(applePayDict["pk_token_instrument_name"] as! String, "Master Charge") + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayFunctionalTest.swift b/Stripe/StripeiOSTests/STPApplePayFunctionalTest.swift new file mode 100644 index 00000000..f11f9123 --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayFunctionalTest.swift @@ -0,0 +1,107 @@ +// +// STPApplePayFunctionalTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 12/21/14. +// Copyright (c) 2014 Stripe, Inc. All rights reserved. +// + +import PassKit +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP)@_spi(StripeApplePayTokenization) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPApplePayFunctionalTest: STPNetworkStubbingTestCase { + + // TODO: regenerate these fixtures with a fresh/real PKPayment + func testCreateTokenWithPaymentClassic() { + let payment = STPFixtures.applePayPayment() + let client = STPAPIClient(publishableKey: "pk_test_vOo1umqsYxSrP5UXfOeL3ecm") + + let expectation = self.expectation(description: "Apple pay token creation") + client.createToken( + with: payment + ) { token, error in + expectation.fulfill() + XCTAssertNil(token, "token should be nil") + guard let error = error else { + XCTFail("error should not be nil") + return + } + + // Since we can't actually generate a new cryptogram in a CI environment, we should just post a blob of expired token data and + // make sure we get the "invalid cert" error. This at least asserts that our blob has been correctly formatted and + // can be decrypted by the backend. + XCTAssert( + ((error as NSError).userInfo[STPError.errorMessageKey] as? NSString)?.range( + of: "certificate used to sign your request is invalid" + ).location != NSNotFound, + "Error is unrelated to cert expiration: \(error)" + ) + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testCreateTokenWithPayment() { + let payment = STPFixtures.applePayPayment() + let client = STPAPIClient(publishableKey: "pk_test_vOo1umqsYxSrP5UXfOeL3ecm") + + let expectation = self.expectation(description: "Apple pay token creation") + StripeAPI.Token.create( + apiClient: client, + payment: payment + ) { result in + expectation.fulfill() + XCTAssertNil(try? result.get(), "token should be nil") + guard case .failure(let error) = result else { + XCTFail("error should not be nil") + return + } + + // Since we can't actually generate a new cryptogram in a CI environment, we should just post a blob of expired token data and + // make sure we get the "invalid cert" error. This at least asserts that our blob has been correctly formatted and + // can be decrypted by the backend. + XCTAssert( + ((error as NSError).userInfo[STPError.errorMessageKey] as? NSString)?.range( + of: "certificate used to sign your request is invalid" + ).location != NSNotFound, + "Error is unrelated to cert expiration: \(error)" + ) + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testCreateSourceWithPayment() { + let payment = STPFixtures.applePayPayment() + let client = STPAPIClient(publishableKey: "pk_test_vOo1umqsYxSrP5UXfOeL3ecm") + + let expectation = self.expectation(description: "Apple pay source creation") + client.createSource( + with: payment + ) { source, error in + expectation.fulfill() + XCTAssertNil(source, "token should be nil") + guard let error = error else { + XCTFail("error should not be nil") + return + } + + // Since we can't actually generate a new cryptogram in a CI environment, we should just post a blob of expired token data and + // make sure we get the "invalid cert" error. This at least asserts that our blob has been correctly formatted and + // can be decrypted by the backend. + XCTAssert( + ((error as NSError).userInfo[STPError.errorMessageKey] as? NSString)?.range( + of: "certificate used to sign your request is invalid" + ).location != NSNotFound, + "Error is unrelated to cert expiration: \(error)" + ) + } + waitForExpectations(timeout: 5.0, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayPaymentOptionTest.swift b/Stripe/StripeiOSTests/STPApplePayPaymentOptionTest.swift new file mode 100644 index 00000000..b0893b29 --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayPaymentOptionTest.swift @@ -0,0 +1,40 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPApplePayPaymentOptionTest.m +// Stripe +// +// Created by Joey Dong on 7/28/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +class STPApplePayPaymentOptionTest: XCTestCase { + // MARK: - STPPaymentOption Tests + + func testImage() { + let applePay = STPApplePayPaymentOption() + XCTAssertNotNil(applePay.image) + } + + func testTemplateImage() { + let applePay = STPApplePayPaymentOption() + XCTAssertNotNil(applePay.templateImage) + } + + func testLabel() { + let applePay = STPApplePayPaymentOption() + XCTAssertEqual(applePay.label, "Apple Pay") + } + + // MARK: - Equality Tests + + func testApplePayEquals() { + let applePay1 = STPApplePayPaymentOption() + let applePay2 = STPApplePayPaymentOption() + + XCTAssertEqual(applePay1, applePay1) + XCTAssertEqual(applePay1, applePay2) + + XCTAssertEqual(applePay1.hash, applePay1.hash) + XCTAssertEqual(applePay1.hash, applePay2.hash) + } +} diff --git a/Stripe/StripeiOSTests/STPApplePayTest.swift b/Stripe/StripeiOSTests/STPApplePayTest.swift new file mode 100644 index 00000000..858a47a9 --- /dev/null +++ b/Stripe/StripeiOSTests/STPApplePayTest.swift @@ -0,0 +1,94 @@ +// +// STPApplePayTest.swift +// StripeiOS Tests +// +// Created by David Estes on 9/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPApplePaySwiftTest: XCTestCase { + func testAdditionalPaymentNetwork() { + XCTAssertFalse(StripeAPI.supportedPKPaymentNetworks().contains(.JCB)) + StripeAPI.additionalEnabledApplePayNetworks = [.JCB] + XCTAssertTrue(StripeAPI.supportedPKPaymentNetworks().contains(.JCB)) + StripeAPI.additionalEnabledApplePayNetworks = [] + } + + func testAdditionalPaymentNetworkCartesBancaires() { + XCTAssertFalse(StripeAPI.supportedPKPaymentNetworks().contains(.cartesBancaires)) + StripeAPI.additionalEnabledApplePayNetworks = [.cartesBancaires] + XCTAssertTrue(StripeAPI.supportedPKPaymentNetworks().contains(.cartesBancaires)) + StripeAPI.additionalEnabledApplePayNetworks = [] + } + + func testAdditionalPaymentNetworksGetPrepended() { + XCTAssertFalse(StripeAPI.supportedPKPaymentNetworks().contains(.cartesBancaires)) + StripeAPI.additionalEnabledApplePayNetworks = [.cartesBancaires] + XCTAssertEqual(StripeAPI.supportedPKPaymentNetworks().first, .cartesBancaires) + StripeAPI.additionalEnabledApplePayNetworks = [] + } + + // Tests stp_tokenParameters in StripePayments, not StripeApplePay + func testStpTokenParameters() { + let applePay = STPFixtures.applePayPayment() + let applePayDict = applePay.stp_tokenParameters(apiClient: .shared) + XCTAssertNotNil(applePayDict["pk_token"]) + XCTAssertEqual((applePayDict["card"] as! NSDictionary)["name"] as! String, "Test Testerson") + XCTAssertEqual(applePayDict["pk_token_instrument_name"] as! String, "Master Charge") + } + + func testPaymentRequestWithMerchantIdentifierCountryCurrency() { + let paymentRequest = StripeAPI.paymentRequest(withMerchantIdentifier: "foo", country: "GB", currency: "GBP") + XCTAssertEqual(paymentRequest.merchantIdentifier, "foo") + let expectedNetworks = Set([ + .amex, + .masterCard, + .visa, + .discover, + .maestro, + ]) + XCTAssertEqual(Set(paymentRequest.supportedNetworks), expectedNetworks) + XCTAssertEqual(paymentRequest.merchantCapabilities, PKMerchantCapability.capability3DS) + XCTAssertEqual(paymentRequest.countryCode, "GB") + XCTAssertEqual(paymentRequest.currencyCode, "GBP") + XCTAssertEqual(paymentRequest.requiredBillingContactFields, Set([.postalAddress])) + } + + func testCanSubmitPaymentRequestReturnsYES() { + let request = PKPaymentRequest() + request.merchantIdentifier = "foo" + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "bar", amount: NSDecimalNumber(string: "1.00")) + ] + + XCTAssertTrue(StripeAPI.canSubmitPaymentRequest(request)) + } + + func testCanSubmitPaymentRequestIfTotalIsZero() { + let request = PKPaymentRequest() + request.merchantIdentifier = "foo" + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "bar", amount: NSDecimalNumber(string: "0.00")) + ] + + XCTAssertTrue(StripeAPI.canSubmitPaymentRequest(request)) + } + + func testCanSubmitPaymentRequestReturnsNOIfMerchantIdentifierIsNil() { + let request = PKPaymentRequest() + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "bar", amount: NSDecimalNumber(string: "1.00")) + ] + + XCTAssertFalse(StripeAPI.canSubmitPaymentRequest(request)) + } +} diff --git a/Stripe/StripeiOSTests/STPBECSDebitAccountNumberValidatorTests.swift b/Stripe/StripeiOSTests/STPBECSDebitAccountNumberValidatorTests.swift new file mode 100644 index 00000000..4e06739c --- /dev/null +++ b/Stripe/StripeiOSTests/STPBECSDebitAccountNumberValidatorTests.swift @@ -0,0 +1,240 @@ +// +// STPBECSDebitAccountNumberValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPBECSDebitAccountNumberValidatorTests: XCTestCase { + func testValidationStateForText() { + let tests = [ + // empty input + [ + "input": "", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.empty.rawValue), + ], + [ + "input": "", + "bsb": "0", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.empty.rawValue), + ], + [ + "input": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.empty.rawValue), + ], + [ + "input": "", + "bsb": "00", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.empty.rawValue), + ], + // incomplete input + [ + "input": "1", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "bsb": "0", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "bsb": "00", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "12345", + "bsb": "06", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + // incomplete input (editing) + [ + "input": "1", + "bsb": "", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "bsb": "0", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "bsb": "00", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "1", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "12345", + "bsb": "06", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + [ + "input": "12345678", + "bsb": "", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.incomplete.rawValue), + ], + // complete + [ + "input": "12345", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + [ + "input": "123456", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + [ + "input": "1234567", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + [ + "input": "12345678", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + [ + "input": "123456789", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + // complete (editing) + [ + "input": "123456789", + "bsb": "", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.complete.rawValue), + ], + // invalid + [ + "input": "12345678910", + "bsb": "", + "editing": NSNumber(value: false), + "expected": NSNumber(value: STPTextValidationState.invalid.rawValue), + ], + // invalid (editing) + [ + "input": "12345678910", + "bsb": "", + "editing": NSNumber(value: true), + "expected": NSNumber(value: STPTextValidationState.invalid.rawValue), + ], + ] + + for test in tests { + let input = (test["input"] as? String)! + let bsb = test["bsb"] as? String + let editing = (test["editing"] as? NSNumber)!.boolValue + let expected = STPTextValidationState( + rawValue: (test["expected"] as! NSNumber).intValue + )! + + XCTAssertEqual( + STPBECSDebitAccountNumberValidator.validationState( + forText: input, + withBSBNumber: bsb, + completeOnMaxLengthOnly: editing + ), + expected + ) + } + } + + func testformattedSanitizedTextFromString() { + let tests = [ + [ + "input": "", + "bsb": "00", + "expected": "", + ], + [ + "input": "1", + "bsb": "00", + "expected": "1", + ], + [ + "input": "--111111--", + "bsb": "00", + "expected": "111111", + ], + [ + "input": "12345678910", + "bsb": "00", + "expected": "123456789", + ], + [ + "input": "", + "bsb": "06", + "expected": "", + ], + [ + "input": "1", + "bsb": "06", + "expected": "1", + ], + [ + "input": "--111111--", + "bsb": "06", + "expected": "111111", + ], + [ + "input": "12345678910", + "bsb": "06", + "expected": "123456789", + ], + ] + + for test in tests { + let input = (test["input"])! + let bsb = test["bsb"] + let expected = test["expected"] + XCTAssertEqual( + STPBECSDebitAccountNumberValidator.formattedSanitizedText( + from: input, + withBSBNumber: bsb + ), + expected + ) + } + } +} diff --git a/Stripe/StripeiOSTests/STPBSBNumberValidatorTests.swift b/Stripe/StripeiOSTests/STPBSBNumberValidatorTests.swift new file mode 100644 index 00000000..efe7b37a --- /dev/null +++ b/Stripe/StripeiOSTests/STPBSBNumberValidatorTests.swift @@ -0,0 +1,92 @@ +// +// STPBSBNumberValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPBSBNumberValidatorTests: XCTestCase { + func testValidationStateForText() { + // Don't use the special test key behavior of treating 00 as a valid BSB. + STPAPIClient.shared.publishableKey = "pk_live_not_a_real_key" + let tests: [(String, STPTextValidationState)] = [ + ("", .empty), + ("1", .incomplete), + ("11", .incomplete), + ("00", .invalid), + ("111111", .complete), + ("111-111", .complete), + ("--111-111--", .complete), + ("1234567", .invalid), + ] + + for test in tests { + XCTAssertEqual(STPBSBNumberValidator.validationState(forText: test.0), test.1) + } + } + + func testformattedSanitizedTextFromString() { + let tests = [ + ["", ""], + ["1", "1"], + ["11", "11"], + ["111", "111-"], + ["111111", "111-111"], + ["--111111--", "111-111"], + ["1234567", "123-456"], + ] + + for test in tests { + XCTAssertEqual(STPBSBNumberValidator.formattedSanitizedText(from: test[0]), test[1]) + } + } + + func testIdentityForText() { + let tests = [ + ["", NSNull()], + ["9", NSNull()], + ["94", NSNull()], + ["941", "Delphi Bank (division of Bendigo and Adelaide Bank)"], + ["942", "Bank of Sydney"], + ["942942", "Bank of Sydney"], + ["40", "Commonwealth Bank of Australia"], + ["942-942", "Bank of Sydney"], + ["942942111", "Bank of Sydney"], + ] + + for test in tests { + if test[1] as! NSObject == NSNull() { + XCTAssertNil(STPBSBNumberValidator.identity(forText: test[0] as! String)) + } else { + XCTAssertEqual( + STPBSBNumberValidator.identity(forText: test[0] as! String), + test[1] as? String + ) + } + } + } + + func testIconForText() { + // Don't use the special test key behavior of treating 00 as a valid BSB. + STPAPIClient.shared.publishableKey = "pk_live_not_a_real_key" + let defaultIcon = STPBSBNumberValidator.icon(forText: nil) + XCTAssertNotNil(defaultIcon, "Nil default icon") + + XCTAssertEqual(defaultIcon, STPBSBNumberValidator.icon(forText: "00")) + + let bankIcon = STPBSBNumberValidator.icon(forText: "11") + XCTAssertNotNil(bankIcon, "Nil icon for bank `11`") + XCTAssertFalse((defaultIcon == bankIcon), "Icon for `11` is same as default") + + XCTAssertEqual(bankIcon, STPBSBNumberValidator.icon(forText: "111-111")) + } +} diff --git a/Stripe/StripeiOSTests/STPBankAccountFunctionalTest.swift b/Stripe/StripeiOSTests/STPBankAccountFunctionalTest.swift new file mode 100644 index 00000000..9f34c476 --- /dev/null +++ b/Stripe/StripeiOSTests/STPBankAccountFunctionalTest.swift @@ -0,0 +1,62 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPBankAccountFunctionalTest.m +// Stripe +// +// Created by Charles Scalesse on 10/2/14. +// +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPBankAccountFunctionalTest: STPNetworkStubbingTestCase { + func testCreateAndRetreiveBankAccountToken() { + let bankAccount = STPBankAccountParams() + bankAccount.accountNumber = "000123456789" + bankAccount.routingNumber = "110000000" + bankAccount.country = "US" + bankAccount.accountHolderName = "Jimmy bob" + bankAccount.accountHolderType = STPBankAccountHolderType.company + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "Bank account creation") + client.createToken( + withBankAccount: bankAccount) { token, error in + expectation.fulfill() + XCTAssertNil(error, "error should be nil") + XCTAssertNotNil(token, "token should not be nil") + + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .bankAccount) + XCTAssertNotNil(token?.bankAccount?.stripeID) + XCTAssertEqual("STRIPE TEST BANK", token?.bankAccount?.bankName) + XCTAssertEqual("6789", token?.bankAccount?.last4) + XCTAssertEqual("Jimmy bob", token?.bankAccount?.accountHolderName) + XCTAssertEqual(token?.bankAccount?.accountHolderType, STPBankAccountHolderType.company) + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testInvalidKey() { + let bankAccount = STPBankAccountParams() + bankAccount.accountNumber = "000123456789" + bankAccount.routingNumber = "110000000" + bankAccount.country = "US" + + let client = STPAPIClient(publishableKey: "not_a_valid_key_asdf") + + let expectation = self.expectation(description: "Bad bank account creation") + + client.createToken( + withBankAccount: bankAccount) { token, error in + expectation.fulfill() + XCTAssertNil(token, "token should be nil") + XCTAssertNotNil(error, "error should not be nil") + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPBankAccountParamsTest.swift b/Stripe/StripeiOSTests/STPBankAccountParamsTest.swift new file mode 100644 index 00000000..847b59d1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPBankAccountParamsTest.swift @@ -0,0 +1,106 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPBankAccountParamsTest.m +// Stripe +// +// Created by Joey Dong on 6/19/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import Stripe +@testable import StripePayments +import XCTest + +class STPBankAccountParamsTest: XCTestCase { + // MARK: - + + func testLast4ReturnsAccountNumberLast4() { + let bankAccountParams = STPBankAccountParams() + bankAccountParams.accountNumber = "000123456789" + XCTAssertEqual(bankAccountParams.last4, "6789") + } + + func testLast4ReturnsNilWhenNoAccountNumberSet() { + let bankAccountParams = STPBankAccountParams() + XCTAssertNil(bankAccountParams.last4) + } + + func testLast4ReturnsNilWhenAccountNumberIsLessThanLength4() { + let bankAccountParams = STPBankAccountParams() + bankAccountParams.accountNumber = "123" + XCTAssertNil(bankAccountParams.last4) + } + + // MARK: - STPBankAccountHolderType Tests + + func testAccountHolderTypeFromString() { + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "individual"), STPBankAccountHolderType.individual) + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "INDIVIDUAL"), STPBankAccountHolderType.individual) + + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "company"), STPBankAccountHolderType.company) + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "COMPANY"), STPBankAccountHolderType.company) + + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "garbage"), STPBankAccountHolderType.individual) + XCTAssertEqual(STPBankAccountParams.accountHolderType(from: "GARBAGE"), STPBankAccountHolderType.individual) + } + + func testStringFromAccountHolderType() { + let values = [ + STPBankAccountHolderType.individual, + STPBankAccountHolderType.company, + ] + + for accountHolderType in values { + let string = STPBankAccountParams.string(from: accountHolderType) + + switch accountHolderType { + case STPBankAccountHolderType.individual: + XCTAssertEqual(string, "individual") + case STPBankAccountHolderType.company: + XCTAssertEqual(string, "company") + default: + break + } + } + } + + // MARK: - Description Tests + + func testDescription() { + let bankAccountParams = STPBankAccountParams() + XCTAssertNotNil(bankAccountParams.description) + } + + // MARK: - STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertEqual(STPBankAccountParams.rootObjectName(), "bank_account") + } + + func testPropertyNamesToFormFieldNamesMapping() { + let bankAccountParams = STPBankAccountParams() + + let mapping = STPBankAccountParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(bankAccountParams.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(!formFieldName.isEmpty) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } + + func testAccountHolderTypeString() { + let bankAccountParams = STPBankAccountParams() + + bankAccountParams.accountHolderType = STPBankAccountHolderType.individual + XCTAssertEqual(bankAccountParams.accountHolderTypeString(), "individual") + + bankAccountParams.accountHolderType = .company + XCTAssertEqual(bankAccountParams.accountHolderTypeString(), "company") + } +} diff --git a/Stripe/StripeiOSTests/STPBankAccountTest.swift b/Stripe/StripeiOSTests/STPBankAccountTest.swift new file mode 100644 index 00000000..8ddb033b --- /dev/null +++ b/Stripe/StripeiOSTests/STPBankAccountTest.swift @@ -0,0 +1,124 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPBankAccountTest.m +// Stripe +// +// Created by Charles Scalesse on 10/2/14. +// +// + +@testable import StripePayments +import XCTest + +class STPBankAccountTest: XCTestCase { + // MARK: - STPBankAccountStatus Tests + + func testStatusFromString() { + XCTAssertEqual(STPBankAccount.status(from: "new"), STPBankAccountStatus.new) + XCTAssertEqual(STPBankAccount.status(from: "NEW"), STPBankAccountStatus.new) + + XCTAssertEqual(STPBankAccount.status(from: "validated"), STPBankAccountStatus.validated) + XCTAssertEqual(STPBankAccount.status(from: "VALIDATED"), STPBankAccountStatus.validated) + + XCTAssertEqual(STPBankAccount.status(from: "verified"), STPBankAccountStatus.verified) + XCTAssertEqual(STPBankAccount.status(from: "VERIFIED"), STPBankAccountStatus.verified) + + XCTAssertEqual(STPBankAccount.status(from: "verification_failed"), STPBankAccountStatus.verificationFailed) + XCTAssertEqual(STPBankAccount.status(from: "VERIFICATION_FAILED"), STPBankAccountStatus.verificationFailed) + + XCTAssertEqual(STPBankAccount.status(from: "errored"), STPBankAccountStatus.errored) + XCTAssertEqual(STPBankAccount.status(from: "ERRORED"), STPBankAccountStatus.errored) + + XCTAssertEqual(STPBankAccount.status(from: "garbage"), STPBankAccountStatus.new) + XCTAssertEqual(STPBankAccount.status(from: "GARBAGE"), STPBankAccountStatus.new) + } + + func testStringFromStatus() { + let values = [ + STPBankAccountStatus.new, + STPBankAccountStatus.validated, + STPBankAccountStatus.verified, + STPBankAccountStatus.verificationFailed, + STPBankAccountStatus.errored, + ] + + for status in values { + let string = STPBankAccount.string(from: status) + + switch status { + case STPBankAccountStatus.new: + XCTAssertEqual(string, "new") + case STPBankAccountStatus.validated: + XCTAssertEqual(string, "validated") + case STPBankAccountStatus.verified: + XCTAssertEqual(string, "verified") + case STPBankAccountStatus.verificationFailed: + XCTAssertEqual(string, "verification_failed") + case STPBankAccountStatus.errored: + XCTAssertEqual(string, "errored") + default: + break + } + } + } + + // MARK: - Equality Tests + + func testBankAccountEquals() { + let bankAccount1 = STPBankAccount.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("BankAccount")) + let bankAccount2 = STPBankAccount.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("BankAccount")) + + XCTAssertEqual(bankAccount1, bankAccount1) + XCTAssertEqual(bankAccount1, bankAccount2) + + XCTAssertEqual(bankAccount1?.hash, bankAccount1?.hash) + XCTAssertEqual(bankAccount1?.hash, bankAccount2?.hash) + } + + // MARK: - Description Tests + + func testDescription() { + let bankAccount = STPBankAccount.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("BankAccount")) + XCTAssertNotNil(bankAccount?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "id", + "last4", + "bank_name", + "country", + "currency", + "status", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("BankAccount") + response!.removeValue(forKey: field) + + XCTAssertNil(STPBankAccount.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPBankAccount.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("BankAccount"))) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("BankAccount") + let bankAccount = STPBankAccount.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(bankAccount?.stripeID, "ba_1AZmya2eZvKYlo2CQzt7Fwnz") + XCTAssertEqual(bankAccount?.accountHolderName, "Jane Austen") + XCTAssertEqual(bankAccount?.accountHolderType, .individual) + XCTAssertEqual(bankAccount?.bankName, "STRIPE TEST BANK") + XCTAssertEqual(bankAccount?.country, "US") + XCTAssertEqual(bankAccount?.currency, "usd") + XCTAssertEqual(bankAccount?.fingerprint, "1JWtPxqbdX5Gamtc") + XCTAssertEqual(bankAccount?.last4, "6789") + XCTAssertEqual(bankAccount?.routingNumber, "110000000") + XCTAssertEqual(bankAccount?.status, STPBankAccountStatus.new) + + XCTAssertEqual(bankAccount!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPBinRangeTest.swift b/Stripe/StripeiOSTests/STPBinRangeTest.swift new file mode 100644 index 00000000..8bd42f92 --- /dev/null +++ b/Stripe/StripeiOSTests/STPBinRangeTest.swift @@ -0,0 +1,190 @@ +// +// STPBinRangeTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 5/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPBinRangeTest: XCTestCase { + func testAllRanges() { + for binRange in STPBINController.shared.allRanges() { + XCTAssertEqual(binRange.accountRangeLow.count, binRange.accountRangeHigh.count) + } + } + + func testMatchesNumber() { + var binRange = STPBINRange( + panLength: 0, + brand: .unknown, + accountRangeLow: "134", + accountRangeHigh: "167", + country: nil + ) + + XCTAssertFalse(binRange.matchesNumber("0")) + XCTAssertTrue(binRange.matchesNumber("1")) + XCTAssertFalse(binRange.matchesNumber("2")) + + XCTAssertFalse(binRange.matchesNumber("00")) + XCTAssertTrue(binRange.matchesNumber("13")) + XCTAssertTrue(binRange.matchesNumber("14")) + XCTAssertTrue(binRange.matchesNumber("16")) + XCTAssertFalse(binRange.matchesNumber("20")) + + XCTAssertFalse(binRange.matchesNumber("133")) + XCTAssertTrue(binRange.matchesNumber("134")) + XCTAssertTrue(binRange.matchesNumber("135")) + XCTAssertTrue(binRange.matchesNumber("167")) + XCTAssertFalse(binRange.matchesNumber("168")) + + XCTAssertFalse(binRange.matchesNumber("1244")) + XCTAssertTrue(binRange.matchesNumber("1340")) + XCTAssertTrue(binRange.matchesNumber("1344")) + XCTAssertTrue(binRange.matchesNumber("1444")) + XCTAssertTrue(binRange.matchesNumber("1670")) + XCTAssertTrue(binRange.matchesNumber("1679")) + XCTAssertFalse(binRange.matchesNumber("1680")) + + binRange = STPBINRange( + panLength: 0, + brand: .unknown, + accountRangeLow: "004", + accountRangeHigh: "017", + country: nil + ) + + XCTAssertTrue(binRange.matchesNumber("0")) + XCTAssertFalse(binRange.matchesNumber("1")) + + XCTAssertTrue(binRange.matchesNumber("00")) + XCTAssertTrue(binRange.matchesNumber("01")) + XCTAssertFalse(binRange.matchesNumber("10")) + XCTAssertFalse(binRange.matchesNumber("20")) + + XCTAssertFalse(binRange.matchesNumber("000")) + XCTAssertFalse(binRange.matchesNumber("002")) + XCTAssertTrue(binRange.matchesNumber("004")) + XCTAssertTrue(binRange.matchesNumber("009")) + XCTAssertTrue(binRange.matchesNumber("014")) + XCTAssertTrue(binRange.matchesNumber("017")) + XCTAssertFalse(binRange.matchesNumber("019")) + XCTAssertFalse(binRange.matchesNumber("020")) + XCTAssertFalse(binRange.matchesNumber("100")) + + XCTAssertFalse(binRange.matchesNumber("0000")) + XCTAssertFalse(binRange.matchesNumber("0021")) + XCTAssertTrue(binRange.matchesNumber("0044")) + XCTAssertTrue(binRange.matchesNumber("0098")) + XCTAssertTrue(binRange.matchesNumber("0143")) + XCTAssertTrue(binRange.matchesNumber("0173")) + XCTAssertFalse(binRange.matchesNumber("0195")) + XCTAssertFalse(binRange.matchesNumber("0202")) + XCTAssertFalse(binRange.matchesNumber("1004")) + + binRange = STPBINRange( + panLength: 0, + brand: .unknown, + accountRangeLow: "", + accountRangeHigh: "", + country: nil + ) + XCTAssertTrue(binRange.matchesNumber("")) + XCTAssertTrue(binRange.matchesNumber("1")) + } + + func testBinRangesForNumber() { + var binRanges: [STPBINRange]? + + binRanges = STPBINController.shared.binRanges(forNumber: "4136000000008") + XCTAssertEqual(binRanges?.count, 3) + + binRanges = STPBINController.shared.binRanges(forNumber: "4242424242424242") + XCTAssertEqual(binRanges?.count, 2) + + binRanges = STPBINController.shared.binRanges(forNumber: "5555555555554444") + XCTAssertEqual(binRanges?.count, 2) + + binRanges = STPBINController.shared.binRanges(forNumber: "") + XCTAssertEqual(binRanges?.count, STPBINController.shared.allRanges().count) + + binRanges = STPBINController.shared.binRanges(forNumber: "123") + XCTAssertEqual(binRanges?.count, 1) + } + + func testBinRangesForBrand() { + let allBrands: [STPCardBrand] = [ + .visa, + .amex, + .mastercard, + .discover, + .JCB, + .dinersClub, + .unionPay, + .unknown, + ] + for brand in allBrands { + let binRanges = STPBINController.shared.binRanges(for: brand) + for binRange in binRanges { + XCTAssertEqual(binRange.brand, brand) + } + } + } + + func testMostSpecificBinRangeForNumber() { + var binRange: STPBINRange? + + binRange = STPBINController.shared.mostSpecificBINRange(forNumber: "") + XCTAssertNotEqual(binRange?.brand, .unknown) + + binRange = STPBINController.shared.mostSpecificBINRange(forNumber: "4242424242422") + XCTAssertEqual(binRange?.brand, .visa) + XCTAssertEqual(binRange?.panLength, 16) + + binRange = STPBINController.shared.mostSpecificBINRange(forNumber: "4136000000008") + XCTAssertEqual(binRange?.brand, .visa) + XCTAssertEqual(binRange?.panLength, 13) + + binRange = STPBINController.shared.mostSpecificBINRange(forNumber: "4242424242424242") + XCTAssertEqual(binRange?.brand, .visa) + XCTAssertEqual(binRange?.panLength, 16) + } + + func testMostSpecificBinRangePrefersKnownBrand() { + // 624478 is a real world case that returns ranges for UnionPay and NYCE, the latter being handled as unknown. + let mockedRanges = [ + STPBINRange( + panLength: 16, + brand: .unionPay, + accountRangeLow: "6244780000000000", + accountRangeHigh: "6244789999999999", + country: "HK" + ), + STPBINRange( + panLength: 16, + brand: .unknown, + accountRangeLow: "6244780000000000", + accountRangeHigh: "6244789999999999", + country: "CN" + ), + ] + + STPBINController.shared.sRetrievedRanges["624478"] = mockedRanges + STPBINController.shared.sAllRanges += mockedRanges + + let binRange = STPBINController.shared.mostSpecificBINRange(forNumber: "624478") + XCTAssertEqual(binRange.accountRangeLow, "6244780000000000") + XCTAssertEqual(binRange.accountRangeHigh, "6244789999999999") + XCTAssertEqual(binRange.brand, .unionPay) + + // Cleanup added values to avoid issues caused by singleton state. + STPBINController.shared.sRetrievedRanges["624478"] = nil + STPBINController.shared.sAllRanges = STPBINController.STPBINRangeInitialRanges + } +} diff --git a/Stripe/StripeiOSTests/STPBlocks.h b/Stripe/StripeiOSTests/STPBlocks.h new file mode 100644 index 00000000..d9be6f72 --- /dev/null +++ b/Stripe/StripeiOSTests/STPBlocks.h @@ -0,0 +1,255 @@ +// +// STPBlocks.h +// Stripe +// +// Created by Jack Flintermann on 3/23/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import + +@class STP3DS2AuthenticateResponse; +@class STPToken; +@class STPFile; +@class STPSource; +@class STPCustomer; +@protocol STPSourceProtocol; +@class STPPaymentIntent; +@class STPSetupIntent; +@class STPPaymentMethod; +@class STPIssuingCardPin; +@class STPFPXBankStatusResponse; + +/** + These values control the labels used in the shipping info collection form. + */ +typedef NS_ENUM(NSUInteger, STPShippingType) { + /** + Shipping the purchase to the provided address using a third-party + shipping company. + */ + STPShippingTypeShipping, + /** + Delivering the purchase by the seller. + */ + STPShippingTypeDelivery, +}; + +/** + An enum representing the status of a shipping address validation. + */ +typedef NS_ENUM(NSUInteger, STPShippingStatus) { + /** + The shipping address is valid. + */ + STPShippingStatusValid, + /** + The shipping address is invalid. + */ + STPShippingStatusInvalid, +}; + +/** + An enum representing the status of a payment requested from the user. + */ +typedef NS_ENUM(NSUInteger, STPPaymentStatus) { + /** + The payment succeeded. + */ + STPPaymentStatusSuccess, + /** + The payment failed due to an unforeseen error, such as the user's Internet connection being offline. + */ + STPPaymentStatusError, + /** + The user cancelled the payment (for example, by hitting "cancel" in the Apple Pay dialog). + */ + STPPaymentStatusUserCancellation, +}; + +/** + An empty block, called with no arguments, returning nothing. + */ +typedef void (^STPVoidBlock)(void); + +/** + A block that may optionally be called with an error. + + @param error The error that occurred, if any. + */ +typedef void (^STPErrorBlock)(NSError * __nullable error); + +/** + A block that contains a boolean success param and may optionally be called with an error. + + @param success Whether the task succeeded. + @param error The error that occurred, if any. + */ +typedef void (^STPBooleanSuccessBlock)(BOOL success, NSError * __nullable error); + +/** + A callback to be run with a JSON response. + + @param jsonResponse The JSON response, or nil if an error occured. + @param error The error that occurred, if any. + */ +typedef void (^STPJSONResponseCompletionBlock)(NSDictionary * __nullable jsonResponse, NSError * __nullable error); + +/** + A callback to be run with a token response from the Stripe API. + + @param token The Stripe token from the response. Will be nil if an error occurs. @see STPToken + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPTokenCompletionBlock)(STPToken * __nullable token, NSError * __nullable error); + +/** + A callback to be run with a source response from the Stripe API. + + @param source The Stripe source from the response. Will be nil if an error occurs. @see STPSource + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPSourceCompletionBlock)(STPSource * __nullable source, NSError * __nullable error); + +/** + A callback to be run with a source or card response from the Stripe API. + + @param source The Stripe source from the response. Will be nil if an error occurs. @see STPSourceProtocol + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPSourceProtocolCompletionBlock)(id __nullable source, NSError * __nullable error); + +/** + A callback to be run with a PaymentIntent response from the Stripe API. + + @param paymentIntent The Stripe PaymentIntent from the response. Will be nil if an error occurs. @see STPPaymentIntent + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPPaymentIntentCompletionBlock)(STPPaymentIntent * __nullable paymentIntent, NSError * __nullable error); + +/** + A callback to be run with a PaymentIntent response from the Stripe API. + + @param setupIntent The Stripe SetupIntent from the response. Will be nil if an error occurs. @see STPSetupIntent + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPSetupIntentCompletionBlock)(STPSetupIntent * __nullable setupIntent, NSError * __nullable error); + +/** + A callback to be run with a PaymentMethod response from the Stripe API. + + @param paymentMethod The Stripe PaymentMethod from the response. Will be nil if an error occurs. @see STPPaymentMethod + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPPaymentMethodCompletionBlock)(STPPaymentMethod * __nullable paymentMethod, NSError * __nullable error); + +/** + A callback to be run with an array of PaymentMethods response from the Stripe API. + + @param paymentMethods An array of PaymentMethod from the response. Will be nil if an error occurs. @see STPPaymentMethod + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPPaymentMethodsCompletionBlock)(NSArray *__nullable paymentMethods, NSError * __nullable error); + +/** + A callback to be run with a validation result and shipping methods for a + shipping address. + + @param status An enum representing whether the shipping address is valid. + @param shippingValidationError If the shipping address is invalid, an error describing the issue with the address. If no error is given and the address is invalid, the default error message will be used. + @param shippingMethods The shipping methods available for the address. + @param selectedShippingMethod The default selected shipping method for the address. + */ +typedef void (^STPShippingMethodsCompletionBlock)(STPShippingStatus status, NSError * __nullable shippingValidationError, NSArray* __nullable shippingMethods, PKShippingMethod * __nullable selectedShippingMethod); + +/** + A callback to be run with a file response from the Stripe API. + + @param file The Stripe file from the response. Will be nil if an error occurs. @see STPFile + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPFileCompletionBlock)(STPFile * __nullable file, NSError * __nullable error); + +/** + A callback to be run with a customer response from the Stripe API. + + @param customer The Stripe customer from the response, or nil if an error occurred. @see STPCustomer + @param error The error returned from the response, or nil if none occurs. + */ +typedef void (^STPCustomerCompletionBlock)(STPCustomer * __nullable customer, NSError * __nullable error); + +/** + An enum representing the success and error states of PIN management + */ +typedef NS_ENUM(NSUInteger, STPPinStatus) { + /** + The verification object was already redeemed + */ + STPPinSuccess, + /** + The verification object was already redeemed + */ + STPPinErrorVerificationAlreadyRedeemed, + /** + The one-time code was incorrect + */ + STPPinErrorVerificationCodeIncorrect, + /** + The verification object was expired + */ + STPPinErrorVerificationExpired, + /** + The verification object has been attempted too many times + */ + STPPinErrorVerificationTooManyAttempts, + /** + An error occured while retrieving the ephemeral key + */ + STPPinEphemeralKeyError, + /** + An unknown error occured + */ + STPPinUnknownError, +}; + +/** + A callback to be run with a card PIN response from the Stripe API. + + @param cardPin The Stripe card PIN from the response. Will be nil if an error occurs. @see STPIssuingCardPin + @param status The status to help you sort between different error state, or STPPinSuccess when succesful. @see STPPinStatus for possible values. + @param error The error returned from the response, or nil if none occurs. @see StripeError.h for possible values. + */ +typedef void (^STPPinCompletionBlock)(STPIssuingCardPin * __nullable cardPin, STPPinStatus status, NSError * __nullable error); + +/** + A callback to be run with a 3DS2 authenticate response from the Stripe API. + + @param authenticateResponse The Stripe AuthenticateResponse. Will be nil if an error occurs. @see STP3DS2AuthenticateResponse + @param error The error returned from the response, or nil if none occurs. + */ +typedef void (^STP3DS2AuthenticateCompletionBlock)(STP3DS2AuthenticateResponse * _Nullable authenticateResponse, NSError * _Nullable error); + +/** + A callback to be run with a response from the Stripe API containing information about the online status of FPX banks. + + @param bankStatusResponse The response from Stripe containing the status of the various banks. Will be nil if an error occurs. @see STPFPXBankStatusResponse + @param error The error returned from the response, or nil if none occurs. + */ +typedef void (^STPFPXBankStatusCompletionBlock)(STPFPXBankStatusResponse * _Nullable bankStatusResponse, NSError * _Nullable error); + +/** + A block called with a payment status and an optional error. + + @param error The error that occurred, if any. + */ +typedef void (^STPPaymentStatusBlock)(STPPaymentStatus status, NSError * __nullable error); + +/** + A block to be run with the client secret of a PaymentIntent or SetupIntent. + + @param clientSecret The client secret of the PaymentIntent or SetupIntent. See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-client_secret + @param error The error that occurred when creating the Intent, or nil if none occurred. + */ +typedef void (^STPIntentClientSecretCompletionBlock)(NSString * __nullable clientSecret, NSError * __nullable error); + diff --git a/Stripe/StripeiOSTests/STPCardBINMetadataTests.swift b/Stripe/StripeiOSTests/STPCardBINMetadataTests.swift new file mode 100644 index 00000000..bb98e92f --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardBINMetadataTests.swift @@ -0,0 +1,55 @@ +// +// STPCardBINMetadataTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 7/20/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardBINMetadataTests: XCTestCase { + func testAPICall() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + + let expectation = self.expectation(description: "Retrieve card metadata") + + // 625035 is a randomly selected UnionPay BIN + STPBINRange.retrieve( + forPrefix: "625035", + completion: { result in + let cardMetadata = try! result.get() + XCTAssertTrue(cardMetadata.data.count > 0) + XCTAssertEqual(cardMetadata.data.first!.brand, .unionPay) + expectation.fulfill() + } + ) + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + } + + func testLoadingInBINRange() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + + let expectation = self.expectation(description: "Retrieve card metadata") + let hardCodedBinRanges = STPBINController.shared.allRanges() + STPBINController.shared.retrieveBINRanges(forPrefix: "625035") { result in + let ranges = try! result.get() + XCTAssertTrue(ranges.count > 0) + XCTAssertTrue( + STPBINController.shared.allRanges().count == hardCodedBinRanges.count + ranges.count + ) + for range in ranges { + XCTAssertTrue(STPBINController.shared.allRanges().contains(range)) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: STPTestingNetworkRequestTimeout) + + } +} diff --git a/Stripe/StripeiOSTests/STPCardBrandTest.swift b/Stripe/StripeiOSTests/STPCardBrandTest.swift new file mode 100644 index 00000000..8c74756a --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardBrandTest.swift @@ -0,0 +1,95 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPCardBrandTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/3/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPCardBrandTest: XCTestCase { + func testStringFromBrand() { + let brands = [ + NSNumber(value: STPCardBrand.amex.rawValue), + NSNumber(value: STPCardBrand.dinersClub.rawValue), + NSNumber(value: STPCardBrand.discover.rawValue), + NSNumber(value: STPCardBrand.JCB.rawValue), + NSNumber(value: STPCardBrand.mastercard.rawValue), + NSNumber(value: STPCardBrand.unionPay.rawValue), + NSNumber(value: STPCardBrand.visa.rawValue), + NSNumber(value: STPCardBrand.cartesBancaires.rawValue), + NSNumber(value: STPCardBrand.unknown.rawValue), + ] + + for brandNumber in brands { + let brand = STPCardBrand(rawValue: brandNumber.intValue) + let string = STPCardBrandUtilities.stringFrom(brand!) + + switch brand { + case .amex: + XCTAssertEqual(string, "American Express") + case .dinersClub: + XCTAssertEqual(string, "Diners Club") + case .discover: + XCTAssertEqual(string, "Discover") + case .JCB: + XCTAssertEqual(string, "JCB") + case .mastercard: + XCTAssertEqual(string, "Mastercard") + case .unionPay: + XCTAssertEqual(string, "UnionPay") + case .visa: + XCTAssertEqual(string, "Visa") + case .cartesBancaires: + XCTAssertEqual(string, "Cartes Bancaires") + case .unknown: + XCTAssertEqual(string, "Unknown") + case .none: + XCTAssertEqual(string, "Unknown") + @unknown default: + break + } + } + } + + func testApiValueFromBrand() { + let brands = [ + STPCardBrand.visa, + STPCardBrand.amex, + STPCardBrand.mastercard, + STPCardBrand.discover, + STPCardBrand.JCB, + STPCardBrand.dinersClub, + STPCardBrand.unionPay, + STPCardBrand.cartesBancaires, + STPCardBrand.unknown, + ] + + for brand in brands { + let string = STPCardBrandUtilities.apiValue(from: brand) + + switch brand { + case .amex: + XCTAssertEqual(string, "american_express") + case .dinersClub: + XCTAssertEqual(string, "diners_club") + case .discover: + XCTAssertEqual(string, "discover") + case .JCB: + XCTAssertEqual(string, "jcb") + case .mastercard: + XCTAssertEqual(string, "mastercard") + case .unionPay: + XCTAssertEqual(string, "unionpay") + case .visa: + XCTAssertEqual(string, "visa") + case .cartesBancaires: + XCTAssertEqual(string, "cartes_bancaires") + case .unknown: + XCTAssertEqual(string, "unknown") + @unknown default: + break + } + } + } +} diff --git a/Stripe/StripeiOSTests/STPCardCVCInputTextFieldFormatterTests.swift b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldFormatterTests.swift new file mode 100644 index 00000000..1288fcea --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldFormatterTests.swift @@ -0,0 +1,52 @@ +// +// STPCardCVCInputTextFieldFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardCVCInputTextFieldFormatterTests: XCTestCase { + + func testAllowedInput() { + let formatter = STPCardCVCInputTextFieldFormatter() + + formatter.cardBrand = .unknown + XCTAssertTrue(formatter.isAllowedInput("1", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("12", to: "", at: NSRange(location: 0, length: 2))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "1", at: NSRange(location: 1, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("3", to: "12", at: NSRange(location: 2, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("123", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("4", to: "123", at: NSRange(location: 3, length: 1))) + XCTAssertFalse(formatter.isAllowedInput("5", to: "1234", at: NSRange(location: 4, length: 1))) + + formatter.cardBrand = .amex + XCTAssertTrue(formatter.isAllowedInput("1", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("12", to: "", at: NSRange(location: 0, length: 2))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "1", at: NSRange(location: 1, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("3", to: "12", at: NSRange(location: 2, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("123", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("4", to: "123", at: NSRange(location: 3, length: 1))) + XCTAssertFalse(formatter.isAllowedInput("5", to: "1234", at: NSRange(location: 4, length: 1))) + + formatter.cardBrand = .visa + XCTAssertTrue(formatter.isAllowedInput("1", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("12", to: "", at: NSRange(location: 0, length: 2))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "1", at: NSRange(location: 1, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("3", to: "12", at: NSRange(location: 2, length: 1))) + XCTAssertTrue(formatter.isAllowedInput("123", to: "", at: NSRange(location: 0, length: 1))) + XCTAssertFalse(formatter.isAllowedInput("4", to: "123", at: NSRange(location: 3, length: 1))) + XCTAssertFalse(formatter.isAllowedInput("5", to: "1234", at: NSRange(location: 4, length: 1))) + + XCTAssertFalse(formatter.isAllowedInput("a", to: "123", at: NSRange(location: 0, length: 1))) + } + +} diff --git a/Stripe/StripeiOSTests/STPCardCVCInputTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldSnapshotTests.swift new file mode 100644 index 00000000..2007bbf0 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldSnapshotTests.swift @@ -0,0 +1,57 @@ +// +// STPCardCVCInputTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardCVCInputTextFieldSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let field = STPCardCVCInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + + STPSnapshotVerifyView(field) + } + + func testIncomplete() { + let field = STPCardCVCInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + field.text = "1" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testValid() { + let field = STPCardCVCInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + field.text = "123" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testInvalid() { + let field = STPCardCVCInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + field.text = "12345" + field.textDidChange() + + STPSnapshotVerifyView(field) + } +} diff --git a/Stripe/StripeiOSTests/STPCardCVCInputTextFieldTests.swift b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldTests.swift new file mode 100644 index 00000000..dcd15604 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldTests.swift @@ -0,0 +1,34 @@ +// +// STPCardCVCInputTextFieldTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 8/31/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardCVCInputTextFieldTests: XCTestCase { + + func testTruncatingCVCWhenTooLong() { + let cvcField = STPCardCVCInputTextField() + cvcField.cardBrand = .amex + cvcField.text = String( + repeating: "1", + count: Int(STPCardValidator.maxCVCLength(for: .amex)) + ) + XCTAssertEqual(cvcField.text?.count, Int(STPCardValidator.maxCVCLength(for: .amex))) + + // Switching the card brand to `visa` should truncate the field text to + // the max length allowed for the brand + cvcField.cardBrand = .visa + XCTAssertEqual(cvcField.text?.count, Int(STPCardValidator.maxCVCLength(for: .visa))) + } + +} diff --git a/Stripe/StripeiOSTests/STPCardCVCInputTextFieldValidatorTests.swift b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldValidatorTests.swift new file mode 100644 index 00000000..deb018dc --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardCVCInputTextFieldValidatorTests.swift @@ -0,0 +1,54 @@ +// +// STPCardCVCInputTextFieldValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardCVCInputTextFieldValidatorTests: XCTestCase { + + func testValidation() { + let validator = STPCardCVCInputTextFieldValidator() + validator.cardBrand = .visa + + validator.inputValue = "123" + if case .valid = validator.validationState { + XCTAssertTrue(true) + } else { + XCTAssertTrue(false, "123 should be valid for Visa") + } + + validator.inputValue = "1" + if case .incomplete(let description) = validator.validationState { + XCTAssertTrue(true) + XCTAssertEqual(description, "Your card's security code is incomplete.") + } else { + XCTAssertTrue(false, "1 should be incomplete for Visa") + } + + validator.inputValue = "1234" + if case .invalid(let errorMessage) = validator.validationState { + XCTAssertEqual(errorMessage, "Your card's security code is invalid.") + } else { + XCTAssertTrue(false, "1234 should be invalid for Visa") + } + + validator.cardBrand = .amex + // don't update inputValue so we know validationState is recalculated on cardBrand change + if case .valid = validator.validationState { + XCTAssertTrue(true) + } else { + XCTAssertTrue(false, "1234 should be valid for Amex") + } + } + +} diff --git a/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldFormatterTests.swift b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldFormatterTests.swift new file mode 100644 index 00000000..ee89f7f3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldFormatterTests.swift @@ -0,0 +1,64 @@ +// +// STPCardExpiryInputTextFieldFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardExpiryInputTextFieldFormatterTests: XCTestCase { + + func testAllowedInput() { + let formatter = STPCardExpiryInputTextFieldFormatter() + XCTAssertTrue(formatter.isAllowedInput("1226", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("12/26", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("12 / 26", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("122026", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("12/2026", to: "", at: NSRange(location: 0, length: 0))) + + XCTAssertTrue(formatter.isAllowedInput("1", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "1", at: NSRange(location: 1, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "12", at: NSRange(location: 2, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("2", to: "12/", at: NSRange(location: 2, length: 0))) + + // the formatter does NOT verify that these are sensical dates (that is delegated to the validator) + XCTAssertTrue(formatter.isAllowedInput("16/1901", to: "", at: NSRange(location: 0, length: 0))) + + XCTAssertFalse(formatter.isAllowedInput("12 / 25 / 26", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("12 / 25 / 26", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("12.26", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("2026/12", to: "", at: NSRange(location: 0, length: 0))) + } + + func testFormattedText() { + let formatter = STPCardExpiryInputTextFieldFormatter() + XCTAssertEqual( + formatter.formattedText(from: "1226", with: [:]), + NSAttributedString(string: "12/26") + ) + XCTAssertEqual( + formatter.formattedText(from: "12/26", with: [:]), + NSAttributedString(string: "12/26") + ) + XCTAssertEqual( + formatter.formattedText(from: "12 / 26", with: [:]), + NSAttributedString(string: "12/26") + ) + XCTAssertEqual( + formatter.formattedText(from: "122026", with: [:]), + NSAttributedString(string: "12/26") + ) + XCTAssertEqual( + formatter.formattedText(from: "12 / 2026", with: [:]), + NSAttributedString(string: "12/26") + ) + } +} diff --git a/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldSnapshotTests.swift new file mode 100644 index 00000000..e597729b --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldSnapshotTests.swift @@ -0,0 +1,52 @@ +// +// STPCardExpiryInputTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardExpiryInputTextFieldSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let field = STPCardExpiryInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + + STPSnapshotVerifyView(field) + } + + func testIncomplete() { + let field = STPCardExpiryInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + field.text = "1" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + // We can't have a valid test here because the date would have to change as time marches on + // func testValid() { + // } + + func testInvalid() { + let field = STPCardExpiryInputTextField() + field.sizeToFit() + field.frame.size.width = 200 + field.text = "16/22" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + +} diff --git a/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldValidatorTests.swift b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldValidatorTests.swift new file mode 100644 index 00000000..5d22d3b5 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardExpiryInputTextFieldValidatorTests.swift @@ -0,0 +1,139 @@ +// +// STPCardExpiryInputTextFieldValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardExpiryInputTextFieldValidatorTests: XCTestCase { + + func testValidation() { + let now = Date() + guard + let nowMonth = Calendar(identifier: .gregorian).dateComponents( + Set([Calendar.Component.month]), + from: now + ).month, + let fullYear = Calendar(identifier: .gregorian).dateComponents( + Set([Calendar.Component.year]), + from: now + ).year + else { + XCTFail("Chaos reigns") + return + } + let nowYear = fullYear % 100 + + let validator = STPCardExpiryInputTextFieldValidator() + validator.inputValue = String(format: "01/%2d", (nowYear + 1) % 100) + if case .valid = validator.validationState { + XCTAssertTrue(true) + } else { + XCTFail("January of next year should be valid") + } + + let oneMonthAhead: String = { + if nowMonth == 12 { + return String(format: "01/%2d", (nowYear + 1) % 100) + } else { + return String(format: "%02d/%2d", nowMonth + 1, nowYear) + } + }() + validator.inputValue = oneMonthAhead + if case .valid = validator.validationState { + XCTAssertTrue(true) + } else { + XCTFail("One month ahead should be valid") + } + + let oneMonthAgo: String = { + if nowMonth == 1 { + return String(format: "01/%2d", max(0, nowYear - 1)) + } else { + return String(format: "%02d/%2d", nowMonth - 1, nowYear) + } + }() + validator.inputValue = oneMonthAgo + if case .invalid(let errorMessage) = validator.validationState { + XCTAssertEqual(errorMessage, "Your card's expiration year is invalid.") + } else { + XCTFail("One month ago should be invalid") + } + + let nonsensical = "16/55" + validator.inputValue = nonsensical + if case .invalid(let errorMessage) = validator.validationState { + XCTAssertEqual(errorMessage, "Your card's expiration date is invalid.") + } else { + XCTFail("Invalid month+year should be invalid") + } + + let nineties = "01/95" + validator.inputValue = nineties + if case .invalid(let errorMessage) = validator.validationState { + XCTAssertEqual(errorMessage, "Your card's expiration year is invalid.") + } else { + XCTFail("The 90s are over") + } + + validator.inputValue = "2" + if case .incomplete(let description) = validator.validationState { + XCTAssertEqual(description, "Your card's expiration date is incomplete.") + } else { + XCTFail("One digit should be incomplete") + } + + validator.inputValue = "2/" + if case .incomplete(let description) = validator.validationState { + XCTAssertEqual(description, "Your card's expiration date is incomplete.") + } else { + XCTFail("One digit with separator should be incomplete") + } + + validator.inputValue = String(format: "1/%2d", (nowYear + 1) % 100) + if case .incomplete(let description) = validator.validationState { + XCTAssertEqual(description, "Your card's expiration date is incomplete.") + } else { + XCTFail("Single digit month should be incomplete") + } + + validator.inputValue = "13/" + if case .invalid(let description) = validator.validationState { + XCTAssertEqual(description, "Your card's expiration month is invalid.") + } else { + XCTFail("Invalid month should be invalid") + } + } + + func testExpiryStringFormatsYear() throws { + let validator = STPCardExpiryInputTextFieldValidator() + + validator.inputValue = "02/24" + + let expiryStrings = try XCTUnwrap(validator.expiryStrings) + + XCTAssertEqual(expiryStrings.month, "02") + XCTAssertEqual(expiryStrings.year, "2024") + } + + func testExpiryStringDoesNotFormatYear() throws { + let validator = STPCardExpiryInputTextFieldValidator() + + validator.inputValue = "02/2024" + + let expiryStrings = try XCTUnwrap(validator.expiryStrings) + + XCTAssertEqual(expiryStrings.month, "02") + XCTAssertEqual(expiryStrings.year, "2024") + } + +} diff --git a/Stripe/StripeiOSTests/STPCardFormViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPCardFormViewSnapshotTests.swift new file mode 100644 index 00000000..df44aa05 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardFormViewSnapshotTests.swift @@ -0,0 +1,160 @@ +// +// STPCardFormViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardFormViewSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let formView = STPCardFormView(billingAddressCollection: .automatic) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testIncomplete() { + let formView = STPCardFormView(billingAddressCollection: .automatic) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 265)) + + formView.numberField.text = "4242" + formView.numberField.textDidChange() + formView.cvcField.text = "123" + formView.cvcField.textDidChange() + + STPSnapshotVerifyView(formView) + } + + // valid expiration date will change over time so we just test without it + func testCompleteWithoutExpiry() { + let formView = STPCardFormView(billingAddressCollection: .automatic) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formView.numberField.text = "4242424242424242" + formView.numberField.textDidChange() + formView.cvcField.text = "123" + formView.cvcField.textDidChange() + formView.postalCodeField.text = "12345" + + STPSnapshotVerifyView(formView) + } + + func testEmptyHiddenPostalCode() { + let formView = STPCardFormView(billingAddressCollection: .automatic) + formView.countryCode = "AE" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testWithFullBillingDetails() { + let formView = STPCardFormView(billingAddressCollection: .required) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 400)) + + STPSnapshotVerifyView(formView) + } + + // MARK: - Standalone + + func testDefaultStandalone() { + let formView = STPCardFormView() + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testBorderlessStandalone() { + let formView = STPCardFormView(style: .borderless) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testCustomBackgroundStandalone() { + let formView = STPCardFormView() + formView.countryCode = "US" + formView.backgroundColor = .green + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testCustomBackgroundDisabledColorStandalone() { + let formView = STPCardFormView() + formView.countryCode = "US" + formView.disabledBackgroundColor = .green + formView.isUserInteractionEnabled = false + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formView) + } + + func testBorderlessStandaloneIncomplete() { + let formView = STPCardFormView(style: .borderless) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formView.numberField.text = "4242" + formView.numberField.textDidChange() + formView.cvcField.text = "123" + formView.cvcField.textDidChange() + + STPSnapshotVerifyView(formView) + } + + func testCBC() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let formView = STPCardFormView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + formView.numberField.text = "4973019750239993" + formView.numberField.textDidChange() + formView.cvcField.text = "123" + formView.cvcField.textDidChange() + formView.postalCodeField.text = "12345" + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.STPSnapshotVerifyView(formView) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } + + func testCBCPreselectVisa() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let formView = STPCardFormView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + formView.countryCode = "US" + formView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formView.numberField.text = "4973019750239993" + formView.numberField.textDidChange() + formView.cvcField.text = "123" + formView.cvcField.textDidChange() + formView.postalCodeField.text = "12345" + formView.preferredNetworks = [.visa] + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.STPSnapshotVerifyView(formView) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } + +} diff --git a/Stripe/StripeiOSTests/STPCardFormViewTests.swift b/Stripe/StripeiOSTests/STPCardFormViewTests.swift new file mode 100644 index 00000000..5845045b --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardFormViewTests.swift @@ -0,0 +1,283 @@ +// +// STPCardFormViewTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 1/19/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardFormViewTests: XCTestCase { + + func testMarkFormErrorsLogic() { + let cardForm = STPCardFormView() + + let handledErrorsTypes = [ + "incorrect_number", + "invalid_number", + "invalid_expiry_month", + "invalid_expiry_year", + "expired_card", + "invalid_cvc", + "incorrect_cvc", + "incorrect_zip", + ] + + let unhandledErrorTypes = [ + "card_declined", + "processing_error", + "imaginary_error", + "", + nil, + ] + + for shouldHandle in handledErrorsTypes { + let error = NSError( + domain: STPError.stripeDomain, + code: STPErrorCode.apiError.rawValue, + userInfo: [STPError.stripeErrorCodeKey: shouldHandle] + ) + XCTAssertTrue( + cardForm.markFormErrors(for: error), + "Failed to handle error for \(shouldHandle)" + ) + } + + for shouldNotHandle in unhandledErrorTypes { + let error: NSError + if let shouldNotHandle = shouldNotHandle { + error = NSError( + domain: STPError.stripeDomain, + code: STPErrorCode.apiError.rawValue, + userInfo: [STPError.stripeErrorCodeKey: shouldNotHandle] + ) + } else { + error = NSError( + domain: STPError.stripeDomain, + code: STPErrorCode.apiError.rawValue, + userInfo: nil + ) + } + XCTAssertFalse( + cardForm.markFormErrors(for: error), + "Incorrectly handled \(shouldNotHandle ?? "nil")" + ) + } + } + + func testHidingPostalCodeOnInit() { + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "zh_Hans_HK")) { + let cardForm = STPCardFormView() + XCTAssertTrue(cardForm.postalCodeField.isHidden) + } + } + + func testHidingPostalUPECodeOnInit() { + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "zh_Hans_HK")) { + let cardForm = STPCardFormView( + billingAddressCollection: .automatic, + style: .standard, + postalCodeRequirement: .upe, + prefillDetails: nil + ) + XCTAssertTrue(cardForm.postalCodeField.isHidden) + } + } + + func testNotHidingPostalUPECodeOnInit() { + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_US")) { + let cardForm = STPCardFormView( + billingAddressCollection: .automatic, + style: .standard, + postalCodeRequirement: .upe, + prefillDetails: nil + ) + XCTAssertFalse(cardForm.postalCodeField.isHidden) + } + } + + func testPanLockedOnInit() { + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_US")) { + let cardForm = STPCardFormView( + billingAddressCollection: .automatic, + style: .standard, + postalCodeRequirement: .upe, + prefillDetails: nil, + inputMode: .panLocked + ) + XCTAssertFalse(cardForm.numberField.isUserInteractionEnabled) + } + } + + func testPrefilledOnInit() { + let prefillDeatils = STPCardFormView.PrefillDetails( + last4: "4242", + expiryMonth: 12, + expiryYear: 25, + cardBrand: .amex + ) + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_US")) { + let cardForm = STPCardFormView( + billingAddressCollection: .automatic, + style: .standard, + postalCodeRequirement: .upe, + prefillDetails: prefillDeatils, + inputMode: .panLocked + ) + + XCTAssertEqual(cardForm.numberField.text, prefillDeatils.formattedLast4) + XCTAssertEqual(cardForm.numberField.cardBrandState.brand, prefillDeatils.cardBrand) + XCTAssertEqual(cardForm.expiryField.text, prefillDeatils.formattedExpiry) + XCTAssertEqual(cardForm.cvcField.cardBrand, prefillDeatils.cardBrand) + } + } + + func testCBCWithPreferredNetwork() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let cardFormView = STPCardFormView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "5555552500001001" + cardParams.expYear = 2050 + cardParams.expMonth = 12 + cardParams.cvc = "123" + cardParams.networks = .init(preferred: "cartes_bancaires") + let billingDetails = STPPaymentMethodBillingDetails(postalCode: "12345", countryCode: "US") + let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: billingDetails, metadata: nil) + cardFormView.cardParams = paymentMethodParams + XCTAssertEqual(cardFormView.cardParams?.card?.number, cardParams.number) + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + XCTAssertEqual(cardFormView.cardParams?.card?.networks?.preferred, "cartes_bancaires") + XCTAssertEqual(cardFormView.numberField.cardBrandState.brand, .cartesBancaires) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } + + func testCBCOBO() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let cardFormView = STPCardFormView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + cardFormView.onBehalfOf = "acct_abc123" + XCTAssertEqual((cardFormView.numberField.validator as! STPCardNumberInputTextFieldValidator).cbcController.onBehalfOf, "acct_abc123") + } + + func testCBCFourDigitCVCIsInvalid() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let cardFormView = STPCardFormView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "5555552500001001" + cardParams.expYear = 2050 + cardParams.expMonth = 12 + cardParams.cvc = "1234" + let billingDetails = STPPaymentMethodBillingDetails(postalCode: "12345", countryCode: "US") + let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: billingDetails, metadata: nil) + cardFormView.cardParams = paymentMethodParams + let exp = expectation(description: "Wait for validation") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + XCTAssertFalse(cardFormView.cvcField.isValid) + exp.fulfill() + } + waitForExpectations(timeout: 0.5) + } + + // MARK: Functional Tests + // If these fail it's _possibly_ because the returned error formats have changed + + func helperFunctionalTestNumber(_ cardNumber: String, shouldHandle: Bool) { + let createPaymentIntentExpectation = self.expectation( + description: "createPaymentIntentExpectation" + ) + var retrievedClientSecret: String? + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { + (createdPIClientSecret, _) in + if let createdPIClientSecret = createdPIClientSecret { + retrievedClientSecret = createdPIClientSecret + createPaymentIntentExpectation.fulfill() + } else { + XCTFail() + } + } + wait(for: [createPaymentIntentExpectation], timeout: 8) // STPTestingNetworkRequestTimeout + guard let clientSecret = retrievedClientSecret, + let currentYear = Calendar.current.dateComponents([.year], from: Date()).year + else { + XCTFail() + return + } + + // STPTestingDefaultPublishableKey + let client = STPAPIClient(publishableKey: "pk_test_ErsyMEOTudSjQR8hh0VrQr5X008sBXGOu6") + + let expiryYear = NSNumber(value: currentYear + 2) + let expiryMonth = NSNumber(1) + + let cardParams = STPPaymentMethodCardParams() + cardParams.number = cardNumber + cardParams.expYear = expiryYear + cardParams.expMonth = expiryMonth + cardParams.cvc = "123" + + let address = STPPaymentMethodAddress() + address.postalCode = "12345" + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = address + + let paymentMethodParams = STPPaymentMethodParams.paramsWith( + card: cardParams, + billingDetails: billingDetails, + metadata: nil + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret) + paymentIntentParams.paymentMethodParams = paymentMethodParams + + let confirmExpectation = expectation(description: "confirmExpectation") + client.confirmPaymentIntent(with: paymentIntentParams) { (_, error) in + if let error = error { + let cardForm = STPCardFormView() + if shouldHandle { + XCTAssertTrue( + cardForm.markFormErrors(for: error), + "Failed to handle \(error) for \(cardNumber)" + ) + } else { + XCTAssertFalse( + cardForm.markFormErrors(for: error), + "Incorrectly handled \(error) for \(cardNumber)" + ) + } + confirmExpectation.fulfill() + } else { + XCTFail() + } + } + wait(for: [confirmExpectation], timeout: 8) // STPTestingNetworkRequestTimeout + } + + func testExpiredCard() { + helperFunctionalTestNumber("4000000000000069", shouldHandle: true) + } + + func testIncorrectCVC() { + helperFunctionalTestNumber("4000000000000127", shouldHandle: true) + } + + func testIncorrectCardNumber() { + helperFunctionalTestNumber("4242424242424241", shouldHandle: true) + } + + func testCardDeclined() { + helperFunctionalTestNumber("4000000000000002", shouldHandle: false) + } + + func testProcessingError() { + helperFunctionalTestNumber("4000000000000119", shouldHandle: false) + } +} diff --git a/Stripe/StripeiOSTests/STPCardFunctionalTest.swift b/Stripe/StripeiOSTests/STPCardFunctionalTest.swift new file mode 100644 index 00000000..5ab15732 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardFunctionalTest.swift @@ -0,0 +1,146 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPCardFunctionalTest.m +// Stripe +// +// Created by Ray Morgan on 7/11/14. +// +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPCardFunctionalTest: STPNetworkStubbingTestCase { + func testCreateCardToken() { + let card = STPCardParams() + + card.number = "4242 4242 4242 4242" + card.expMonth = 6 + card.expYear = 2050 + card.currency = "usd" + card.address.line1 = "123 Fake Street" + card.address.line2 = "Apartment 4" + card.address.city = "New York" + card.address.state = "NY" + card.address.country = "USA" + card.address.postalCode = "10002" + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "Card creation") + + client.createToken( + withCard: card) { token, error in + XCTAssertNil(error, "error should be nil") + XCTAssertNotNil(token, "token should not be nil") + + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .card) + XCTAssertEqual(6, token?.card?.expMonth) + XCTAssertEqual(2050, token?.card?.expYear) + XCTAssertEqual("4242", token?.card?.last4) + XCTAssertEqual("usd", token?.card?.currency) + XCTAssertEqual("10002", token?.card?.address?.postalCode) + expectation.fulfill() + + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCardTokenCreationWithInvalidParams() { + let card = STPCardParams() + + card.number = "4242 4242 4242 4241" + card.expMonth = 6 + card.expYear = 2024 + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "Card creation") + + client.createToken( + withCard: card) { token, error in + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual((error as NSError?)?.code, 70) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + XCTAssertEqual((error as NSError?)?.userInfo[STPError.errorParameterKey] as! String, "number") + XCTAssertNil(token, "token should be nil") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCardTokenCreationWithExpiredCard() { + let card = STPCardParams() + + card.number = "4242 4242 4242 4242" + card.expMonth = 6 + card.expYear = 2013 + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "Card creation") + + client.createToken( + withCard: card) { token, error in + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual((error as NSError?)?.code, 70) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain ) + XCTAssertEqual((error as NSError?)?.userInfo[STPError.cardErrorCodeKey] as! String, STPError.invalidExpYear) + XCTAssertEqual((error as NSError?)?.userInfo[STPError.errorParameterKey] as! String, "expYear") + XCTAssertNil(token, "token should be nil") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testInvalidKey() { + let card = STPCardParams() + + card.number = "4242 4242 4242 4242" + card.expMonth = 6 + card.expYear = 2050 + + let client = STPAPIClient(publishableKey: "not_a_valid_key_asdf") + + let expectation = self.expectation(description: "Card failure") + client.createToken( + withCard: card) { token, error in + XCTAssertNil(token, "token should be nil") + XCTAssertNotNil(error, "error should not be nil") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateCVCUpdateToken() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "CVC Update Token Creation") + + client.createToken(forCVCUpdate: "1234") { token, error in + XCTAssertNil(error, "error should be nil") + XCTAssertNotNil(token, "token should not be nil") + + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .cvcUpdate, "token should be type CVC Update") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testInvalidCVC() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "Invalid CVC") + + client.createToken( + forCVCUpdate: "1") { token, error in + XCTAssertNil(token, "token should be nil") + XCTAssertNotNil(error, "error should not be nil") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPCardNumberInputTextFieldFormatterTests.swift b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldFormatterTests.swift new file mode 100644 index 00000000..2c6bc060 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldFormatterTests.swift @@ -0,0 +1,73 @@ +// +// STPCardNumberInputTextFieldFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardNumberInputTextFieldFormatterTests: XCTestCase { + + func testAllowedInput() { + let formatter = STPCardNumberInputTextFieldFormatter() + XCTAssertTrue(formatter.isAllowedInput("4242424242424242", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("424242424242424", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue( + formatter.isAllowedInput("4242 4242 4242 4242", to: "", at: NSRange(location: 0, length: 0)) + ) + XCTAssertTrue(formatter.isAllowedInput("42424242 42424242", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("4242 ", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("3566002020360505", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput(" ", to: "4242", at: NSRange(location: 4, length: 0))) + + XCTAssertFalse( + formatter.isAllowedInput("4242.4242.4242.4242", to: "", at: NSRange(location: 0, length: 0)) + ) + XCTAssertFalse(formatter.isAllowedInput("4", to: "4242424242424242", at: NSRange(location: 0, length: 0))) + } + + func testFormatting() { + let formatter = STPCardNumberInputTextFieldFormatter() + var expected: NSMutableAttributedString = NSMutableAttributedString() + + expected = NSMutableAttributedString(string: "4242424242424242") + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 0, length: 3)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 3, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 4, length: 3)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 7, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 8, length: 3)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 11, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 12, length: 4)) + XCTAssertEqual(formatter.formattedText(from: "4242424242424242", with: [:]), expected) + XCTAssertEqual(formatter.formattedText(from: "4242 4242 4242 4242", with: [:]), expected) + + expected = NSMutableAttributedString(string: "4242") + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 0, length: 4)) + XCTAssertEqual(formatter.formattedText(from: "4242", with: [:]), expected) + + expected = NSMutableAttributedString(string: "42424") + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 0, length: 3)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 3, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 4, length: 1)) + XCTAssertEqual(formatter.formattedText(from: "42424", with: [:]), expected) + + expected = NSMutableAttributedString(string: "378282246310005") // 4, 6, 5, + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 0, length: 3)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 3, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 4, length: 5)) + expected.addAttribute(.kern, value: NSNumber(5), range: NSRange(location: 9, length: 1)) + expected.addAttribute(.kern, value: NSNumber(0), range: NSRange(location: 10, length: 5)) + // expected.addAttribute(.kern, value: NSNumber(5), range: NSMakeRange(11, 1)) + // expected.addAttribute(.kern, value: NSNumber(0), range: NSMakeRange(12, 4)) + XCTAssertEqual(formatter.formattedText(from: "378282246310005", with: [:]), expected) + } + +} diff --git a/Stripe/StripeiOSTests/STPCardNumberInputTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldSnapshotTests.swift new file mode 100644 index 00000000..5c4a56fd --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldSnapshotTests.swift @@ -0,0 +1,57 @@ +// +// STPCardNumberInputTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardNumberInputTextFieldSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let field = STPCardNumberInputTextField() + field.sizeToFit() + field.frame.size.width = 300 + + STPSnapshotVerifyView(field) + } + + func testIncomplete() { + let field = STPCardNumberInputTextField() + field.sizeToFit() + field.frame.size.width = 300 + field.text = "42" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testValid() { + let field = STPCardNumberInputTextField() + field.sizeToFit() + field.frame.size.width = 300 + field.text = "4242424242424242" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testInvalid() { + let field = STPCardNumberInputTextField() + field.sizeToFit() + field.frame.size.width = 300 + field.text = "4242424242424241" + field.textDidChange() + + STPSnapshotVerifyView(field) + } +} diff --git a/Stripe/StripeiOSTests/STPCardNumberInputTextFieldValidatorTests.swift b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldValidatorTests.swift new file mode 100644 index 00000000..c1f5e567 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardNumberInputTextFieldValidatorTests.swift @@ -0,0 +1,207 @@ +// +// STPCardNumberInputTextFieldValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardNumberInputTextFieldValidatorTests: XCTestCase { + + static let cardData: [(STPCardBrand, String, STPValidatedInputState)] = { + return [ + ( + .visa, + "4242424242424242", + .valid(message: nil) + ), + ( + .visa, + "4242424242422", + .incomplete(description: "Your card number is incomplete.") + ), + ( + .visa, + "4012888888881881", + .valid(message: nil) + ), + ( + .visa, + "4000056655665556", + .valid(message: nil) + ), + ( + .mastercard, + "5555555555554444", + .valid(message: nil) + ), + ( + .mastercard, + "5200828282828210", + .valid(message: nil) + ), + ( + .mastercard, + "5105105105105100", + .valid(message: nil) + ), + ( + .mastercard, + "2223000010089800", + .valid(message: nil) + ), + ( + .amex, + "378282246310005", + .valid(message: nil) + ), + ( + .amex, + "371449635398431", + .valid(message: nil) + ), + ( + .discover, + "6011111111111117", + .valid(message: nil) + ), + ( + .discover, + "6011000990139424", + .valid(message: nil) + ), + ( + .dinersClub, + "36227206271667", + .valid(message: nil) + ), + ( + .dinersClub, + "3056930009020004", + .valid(message: nil) + ), + ( + .JCB, + "3530111333300000", + .valid(message: nil) + ), + ( + .JCB, + "3566002020360505", + .valid(message: nil) + ), + ( + .unknown, + "1234567812345678", + .invalid(errorMessage: "Your card number is invalid.") + ), + ] + }() + + func testValidation() { + // same tests as in STPCardValidatorTest#testNumberValidation + var tests: [(STPValidatedInputState, String, STPCardBrand)] = [] + + for card in STPCardNumberInputTextFieldValidatorTests.cardData { + tests.append((card.2, card.1, card.0)) + } + + tests.append((.valid(message: nil), "4242 4242 4242 4242", .visa)) + tests.append((.valid(message: nil), "4136000000008", .visa)) + + let badCardNumbers: [(String, STPCardBrand)] = [ + ("0000000000000000", .unknown), + ("9999999999999995", .unknown), + ("1", .unknown), + ("1234123412341234", .unknown), + ("xxx", .unknown), + ("9999999999999999999999", .unknown), + ("42424242424242424242", .visa), + ("4242-4242-4242-4242", .visa), + ] + + for card in badCardNumbers { + tests.append((.invalid(errorMessage: "Your card number is invalid."), card.0, card.1)) + } + + let possibleCardNumbers: [(String, STPCardBrand)] = [ + ("4242", .visa), ("5", .mastercard), ("3", .unknown), ("", .unknown), + (" ", .unknown), ("6011", .discover), ("4012888888881", .visa), + ] + + for card in possibleCardNumbers { + tests.append( + ( + .incomplete( + description: card.0.isEmpty ? nil : "Your card number is incomplete." + ), + card.0, card.1 + ) + ) + } + + let validator = STPCardNumberInputTextFieldValidator() + for test in tests { + let card = test.1 + validator.inputValue = card + let validationState = validator.validationState + let expected = test.0 + if !(validationState == expected) { + XCTFail("Expected \(expected), got \(validationState) for number \"\(card)\"") + } + let expectedCardBrand = test.2 + if !(validator.cardBrandState.brand == expectedCardBrand) { + XCTFail( + "Expected \(expectedCardBrand), got \(validator.cardBrandState.brand) for number \(card)" + ) + } + } + + validator.inputValue = "1" + XCTAssertEqual( + .invalid(errorMessage: "Your card number is invalid."), + validator.validationState + ) + + validator.inputValue = "0000000000000000" + XCTAssertEqual( + .invalid(errorMessage: "Your card number is invalid."), + validator.validationState + ) + + validator.inputValue = "9999999999999995" + XCTAssertEqual( + .invalid(errorMessage: "Your card number is invalid."), + validator.validationState + ) + + validator.inputValue = "0000000000000000000" + XCTAssertEqual( + .invalid(errorMessage: "Your card number is invalid."), + validator.validationState + ) + + validator.inputValue = "9999999999999999998" + XCTAssertEqual( + .invalid(errorMessage: "Your card number is invalid."), + validator.validationState + ) + + validator.inputValue = "4242424242424" + XCTAssertEqual( + .incomplete(description: "Your card number is incomplete."), + validator.validationState + ) + + validator.inputValue = nil + XCTAssertEqual(.incomplete(description: nil), validator.validationState) + } +} diff --git a/Stripe/StripeiOSTests/STPCardParamsTest.swift b/Stripe/StripeiOSTests/STPCardParamsTest.swift new file mode 100644 index 00000000..cb29e726 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardParamsTest.swift @@ -0,0 +1,173 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPCardParamsTest.m +// Stripe +// +// Created by Joey Dong on 6/19/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import XCTest + +class STPCardParamsTest: XCTestCase { + // MARK: - + + func testLast4ReturnsCardNumberLast4() { + let cardParams = STPCardParams() + cardParams.number = "4242424242424242" + XCTAssertEqual(cardParams.last4(), "4242") + } + + func testLast4ReturnsNilWhenNoCardNumberSet() { + let cardParams = STPCardParams() + XCTAssertNil(cardParams.last4()) + } + + func testLast4ReturnsNilWhenCardNumberIsLessThanLength4() { + let cardParams = STPCardParams() + cardParams.number = "123" + XCTAssertNil(cardParams.last4()) + } + + func testNameSharedWithAddress() { + let cardParams = STPCardParams() + + cardParams.name = "James" + XCTAssertEqual(cardParams.name, "James") + XCTAssertEqual(cardParams.address.name, "James") + + let address = STPAddress() + address.name = "Jim" + + cardParams.address = address + XCTAssertEqual(cardParams.name, "Jim") + XCTAssertEqual(cardParams.address.name, "Jim") + + // Doesn't update `name`, since mutation invisible to the STPCardParams + cardParams.address.name = "Smith" + XCTAssertEqual(cardParams.name, "Jim") + XCTAssertEqual(cardParams.address.name, "Smith") + } + + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + + func testAddress() { + let cardParams = STPCardParams() + cardParams.name = "John Smith" + cardParams.addressLine1 = "55 John St" + cardParams.addressLine2 = "#3B" + cardParams.addressCity = "New York" + cardParams.addressState = "NY" + cardParams.addressZip = "10002" + cardParams.addressCountry = "US" + + let address = cardParams.address + + XCTAssertEqual(address.name, "John Smith") + XCTAssertEqual(address.line1, "55 John St") + XCTAssertEqual(address.line2, "#3B") + XCTAssertEqual(address.city, "New York") + XCTAssertEqual(address.state, "NY") + XCTAssertEqual(address.postalCode, "10002") + XCTAssertEqual(address.country, "US") + } + + func testSetAddress() { + let address = STPAddress() + address.name = "John Smith" + address.line1 = "55 John St" + address.line2 = "#3B" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let cardParams = STPCardParams() + cardParams.address = address + + XCTAssertEqual(cardParams.name, "John Smith") + XCTAssertEqual(cardParams.addressLine1, "55 John St") + XCTAssertEqual(cardParams.addressLine2, "#3B") + XCTAssertEqual(cardParams.addressCity, "New York") + XCTAssertEqual(cardParams.addressState, "NY") + XCTAssertEqual(cardParams.addressZip, "10002") + XCTAssertEqual(cardParams.addressCountry, "US") + } + + // #pragma clang diagnostic pop + + // MARK: - Description Tests + + func testDescription() { + let cardParams = STPCardParams() + XCTAssertNotNil(cardParams.description) + } + + // MARK: - STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertEqual(STPCardParams.rootObjectName(), "card") + } + + func testPropertyNamesToFormFieldNamesMapping() { + let cardParams = STPCardParams() + + let mapping = STPCardParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(cardParams.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(!formFieldName.isEmpty) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } + + // MARK: - NSCopying Tests + + func testCopyWithZone() { + let cardParams = STPFixtures.cardParams() + cardParams.address = STPFixtures.address() + let copiedCardParams = cardParams.copy() as! STPCardParams + + // The property names we expect to *not* be equal objects + let notEqualProperties = [ + // these include the object's address, so they won't be the same across copies + "debugDescription", + "description", + "hash", + // STPAddress does not override isEqual:, so this is pointer comparison + "address", + ] + + // use runtime inspection to find the list of properties. If a new property is + // added to the fixture, but not the `copyWithZone:` implementation, this should catch it + for property in STPTestUtils.propertyNames(of: cardParams) { + if notEqualProperties.contains(property) { + XCTAssertNotEqual( + cardParams.value(forKey: property) as? NSObject, + copiedCardParams.value(forKey: property) as? NSObject) + } else { + XCTAssertEqual( + cardParams.value(forKey: property) as? NSObject, + copiedCardParams.value(forKey: property) as? NSObject) + } + } + } + + func testAddressIsNotCopied() { + let cardParams = STPFixtures.cardParams() + cardParams.address = STPFixtures.address() + let secondCardParams = STPCardParams() + + secondCardParams.address = cardParams.address + cardParams.address.line1 = "123 Main" + + XCTAssertEqual(cardParams.address.line1, "123 Main") + XCTAssertEqual(secondCardParams.address.line1, "123 Main") + } +} diff --git a/Stripe/StripeiOSTests/STPCardTest.swift b/Stripe/StripeiOSTests/STPCardTest.swift new file mode 100644 index 00000000..e9fdf41b --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardTest.swift @@ -0,0 +1,270 @@ +// +// STPCardTest.swift +// StripeiOS Tests +// +// Created by Saikat Chakrabarti on 11/5/12. +// Copyright © 2012 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardTest: XCTestCase { + // MARK: - STPCardBrand Tests + + // These are only intended to be deprecated publicly. + // When removed from public header, can remove these pragmas + func testBrandFromString() { + XCTAssertEqual(STPCard.brand(from: "visa"), .visa) + XCTAssertEqual(STPCard.brand(from: "VISA"), .visa) + + XCTAssertEqual(STPCard.brand(from: "amex"), .amex) + XCTAssertEqual(STPCard.brand(from: "AMEX"), .amex) + XCTAssertEqual(STPCard.brand(from: "american express"), .amex) + XCTAssertEqual(STPCard.brand(from: "AMERICAN EXPRESS"), .amex) + XCTAssertEqual(STPCard.brand(from: "american_express"), .amex) + XCTAssertEqual(STPCard.brand(from: "AMERICAN_EXPRESS"), .amex) + + XCTAssertEqual(STPCard.brand(from: "mastercard"), .mastercard) + XCTAssertEqual(STPCard.brand(from: "MASTERCARD"), .mastercard) + + XCTAssertEqual(STPCard.brand(from: "discover"), .discover) + XCTAssertEqual(STPCard.brand(from: "DISCOVER"), .discover) + + XCTAssertEqual(STPCard.brand(from: "jcb"), .JCB) + XCTAssertEqual(STPCard.brand(from: "JCB"), .JCB) + + XCTAssertEqual(STPCard.brand(from: "diners club"), .dinersClub) + XCTAssertEqual(STPCard.brand(from: "DINERS CLUB"), .dinersClub) + XCTAssertEqual(STPCard.brand(from: "diners"), .dinersClub) + XCTAssertEqual(STPCard.brand(from: "DINERS"), .dinersClub) + XCTAssertEqual(STPCard.brand(from: "diners_club"), .dinersClub) + XCTAssertEqual(STPCard.brand(from: "DINERS_CLUB"), .dinersClub) + + XCTAssertEqual(STPCard.brand(from: "unionpay"), .unionPay) + XCTAssertEqual(STPCard.brand(from: "UNIONPAY"), .unionPay) + + XCTAssertEqual(STPCard.brand(from: "cartes bancaires"), .cartesBancaires) + XCTAssertEqual(STPCard.brand(from: "CARTES Bancaires"), .cartesBancaires) + XCTAssertEqual(STPCard.brand(from: "CARTES_Bancaires"), .cartesBancaires) + + XCTAssertEqual(STPCard.brand(from: "unknown"), .unknown) + XCTAssertEqual(STPCard.brand(from: "UNKNOWN"), .unknown) + + XCTAssertEqual(STPCard.brand(from: "garbage"), .unknown) + XCTAssertEqual(STPCard.brand(from: "GARBAGE"), .unknown) + } + + // MARK: - STPCardFundingType Tests + + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + // These are only intended to be deprecated publicly. + // When removed from public header, can remove these pragmas + func testFundingFromString() { + XCTAssertEqual(STPCard.funding(from: "credit"), .credit) + XCTAssertEqual(STPCard.funding(from: "CREDIT"), .credit) + + XCTAssertEqual(STPCard.funding(from: "debit"), .debit) + XCTAssertEqual(STPCard.funding(from: "DEBIT"), .debit) + + XCTAssertEqual(STPCard.funding(from: "prepaid"), .prepaid) + XCTAssertEqual(STPCard.funding(from: "PREPAID"), .prepaid) + + XCTAssertEqual(STPCard.funding(from: "other"), .other) + XCTAssertEqual(STPCard.funding(from: "OTHER"), .other) + + XCTAssertEqual(STPCard.funding(from: "unknown"), .other) + XCTAssertEqual(STPCard.funding(from: "UNKNOWN"), .other) + + XCTAssertEqual(STPCard.funding(from: "garbage"), .other) + XCTAssertEqual(STPCard.funding(from: "GARBAGE"), .other) + } + + // #pragma clang diagnostic pop + func testStringFromFunding() { + let values: [STPCardFundingType] = [ + .credit, + .debit, + .prepaid, + .other, + ] + + for funding in values { + let string = STPCard.string(fromFunding: funding) + + switch funding { + case .credit: + XCTAssertEqual(string, "credit") + case .debit: + XCTAssertEqual(string, "debit") + case .prepaid: + XCTAssertEqual(string, "prepaid") + case .other: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + // These tests can ber removed in the future, they should be covered by + // the equivalent response decodeable tests + func testInitWithIDBrandLast4ExpMonthExpYearFunding() { + let card = STPCard( + id: "card_1AVRojEOD54MuFwSxr93QJSx", + brand: .visa, + last4: "5556", + expMonth: 12, + expYear: 2034, + funding: .debit + ) + XCTAssertEqual(card.stripeID, "card_1AVRojEOD54MuFwSxr93QJSx") + XCTAssertEqual(card.brand, .visa) + XCTAssertEqual(card.last4, "5556") + XCTAssertEqual(card.expMonth, Int(12)) + XCTAssertEqual(card.expYear, Int(2034)) + XCTAssertEqual(card.funding, .debit) + } + + // #pragma clang diagnostic pop + func testIsApplePayCard() { + let card = STPFixtures.card() + + card.allResponseFields = [:] + XCTAssertFalse(card.isApplePayCard) + + card.allResponseFields = [ + "tokenization_method": "android_pay" + ] + XCTAssertFalse(card.isApplePayCard) + + card.allResponseFields = [ + "tokenization_method": "apple_pay" + ] + XCTAssertTrue(card.isApplePayCard) + + card.allResponseFields = [ + "tokenization_method": "garbage" + ] + XCTAssertFalse(card.isApplePayCard) + + card.allResponseFields = [ + "tokenization_method": "" + ] + XCTAssertFalse(card.isApplePayCard) + + // See: https://stripe.com/docs/api#card_object-tokenization_method + } + + func testAddressPopulated() { + let card = STPFixtures.card() + XCTAssertEqual(card.address?.name, "Jane Austen") + XCTAssertEqual(card.address?.line1, "123 Fake St") + XCTAssertEqual(card.address?.line2, "Apt 1") + XCTAssertEqual(card.address?.city, "Pittsburgh") + XCTAssertEqual(card.address?.state, "PA") + XCTAssertEqual(card.address?.postalCode, "19219") + XCTAssertEqual(card.address?.country, "US") + } + + // MARK: - Equality Tests + func testCardEquals() { + let card1 = STPFixtures.card() + let card2 = STPFixtures.card() + + XCTAssertEqual(card1, card1) + XCTAssertEqual(card1, card2) + + XCTAssertEqual(card1.hash, card1.hash) + XCTAssertEqual(card1.hash, card2.hash) + } + + // MARK: - STPAPIResponseDecodable Tests + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = ["id", "last4", "brand", "exp_month", "exp_year"] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("Card") + response?.removeValue(forKey: field) + + XCTAssertNil(STPCard.decodedObject(fromAPIResponse: response)) + } + + XCTAssert((STPCard.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("Card")) != nil)) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("Card")! + let card = STPCard.decodedObject(fromAPIResponse: response)! + + XCTAssertEqual(card.stripeID, "card_103kbR2eZvKYlo2CDczLmw4K") + + XCTAssertEqual(card.address?.city, "Pittsburgh") + XCTAssertEqual(card.address?.country, "US") + XCTAssertEqual(card.address?.line1, "123 Fake St") + XCTAssertEqual(card.address?.line2, "Apt 1") + XCTAssertEqual(card.address?.state, "PA") + XCTAssertEqual(card.address?.postalCode, "19219") + + XCTAssertEqual(card.perform(NSSelectorFromString("cardId")).takeUnretainedValue() as? NSString, "card_103kbR2eZvKYlo2CDczLmw4K") + + XCTAssertEqual(card.perform(NSSelectorFromString("addressCity")).takeUnretainedValue() as? NSString, "Pittsburgh") + XCTAssertEqual(card.perform(NSSelectorFromString("addressCountry")).takeUnretainedValue() as? NSString, "US") + XCTAssertEqual(card.perform(NSSelectorFromString("addressLine1")).takeUnretainedValue() as? NSString, "123 Fake St") + XCTAssertEqual(card.perform(NSSelectorFromString("addressLine2")).takeUnretainedValue() as? NSString, "Apt 1") + XCTAssertEqual(card.perform(NSSelectorFromString("addressState")).takeUnretainedValue() as? NSString, "PA") + XCTAssertEqual(card.perform(NSSelectorFromString("addressZip")).takeUnretainedValue() as? NSString, "19219") + XCTAssertNil(card.perform(NSSelectorFromString("metadata"))) + + XCTAssertEqual(card.brand, .visa) + XCTAssertEqual(card.country, "US") + XCTAssertEqual(card.currency, "usd") + XCTAssertEqual(card.dynamicLast4, "5678") + XCTAssertEqual(card.expMonth, Int(5)) + XCTAssertEqual(card.expYear, Int(2017)) + XCTAssertEqual(card.funding, .credit) + XCTAssertEqual(card.last4, "4242") + XCTAssertEqual(card.name, "Jane Austen") + + XCTAssertEqual(card.allResponseFields as NSDictionary, response as NSDictionary) + } + + // MARK: - STPSourceProtocol Tests + func testStripeID() { + let card = STPFixtures.card() + XCTAssertEqual(card.stripeID, "card_103kbR2eZvKYlo2CDczLmw4K") + } + + // MARK: - STPPaymentOption Tests + func testLabel() { + let card = STPCard.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("Card"))! + XCTAssertEqual(card.label, "Visa 4242") + } + + // MARK: - + func forEachBrand(_ block: @escaping (_ brand: STPCardBrand) -> Void) { + let values: [STPCardBrand] = [ + .amex, + .dinersClub, + .discover, + .JCB, + .mastercard, + .unionPay, + .visa, + .unknown, + ] + + for brand in values { + block(brand) + } + } +} diff --git a/Stripe/StripeiOSTests/STPCardValidatorTest.swift b/Stripe/StripeiOSTests/STPCardValidatorTest.swift new file mode 100644 index 00000000..efcd911b --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardValidatorTest.swift @@ -0,0 +1,480 @@ +// +// STPCardValidatorTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 7/24/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +import UIKit +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardValidatorTest: XCTestCase { + override class func setUp() { + STPBINController.shared.reset() + } + + static let cardData: [(STPCardBrand, String, STPCardValidationState)] = { + return [ + ( + .visa, + "4242424242424242", + .valid + ), + ( + .visa, + "4242424242422", + .incomplete + ), + ( + .visa, + "4012888888881881", + .valid + ), + ( + .visa, + "4000056655665556", + .valid + ), + ( + .mastercard, + "5555555555554444", + .valid + ), + ( + .mastercard, + "5200828282828210", + .valid + ), + ( + .mastercard, + "5105105105105100", + .valid + ), + ( + .mastercard, + "2223000010089800", + .valid + ), + ( + .amex, + "378282246310005", + .valid + ), + ( + .amex, + "371449635398431", + .valid + ), + ( + .discover, + "6011111111111117", + .valid + ), + ( + .discover, + "6011000990139424", + .valid + ), + ( + .dinersClub, + "36227206271667", + .valid + ), + ( + .dinersClub, + "3056930009020004", + .valid + ), + ( + .JCB, + "3530111333300000", + .valid + ), + ( + .JCB, + "3566002020360505", + .valid + ), + ( + .unknown, + "1234567812345678", + .invalid + ), + ] + }() + + func testNumberSanitization() { + let tests = [ + ["4242424242424242", "4242424242424242"], + ["XXXXXX", ""], + ["424242424242424X", "424242424242424"], + ["X4242", "4242"], + ["4242 4242 4242 4242", "4242424242424242"], + ] + for test in tests { + XCTAssertEqual(STPCardValidator.sanitizedNumericString(for: test[0]), test[1]) + } + } + + func testNumberValidation() { + var tests: [(STPCardValidationState, String)] = [] + + for card in STPCardValidatorTest.cardData { + tests.append((card.2, card.1)) + } + + tests.append((.valid, "4242 4242 4242 4242")) + tests.append((.valid, "4136000000008")) + + let badCardNumbers = [ + "0000000000000000", + "9999999999999995", + "1", + "1234123412341234", + "xxx", + "9999999999999999999999", + "42424242424242424242", + "4242-4242-4242-4242", + ] + + for card in badCardNumbers { + tests.append((.invalid, card)) + } + + let possibleCardNumbers = ["4242", "5", "3", "", " ", "6011", "4012888888881"] + + for card in possibleCardNumbers { + tests.append((.incomplete, card)) + } + + for test in tests { + let card = test.1 + let validationState = STPCardValidator.validationState( + forNumber: card, + validatingCardBrand: true + ) + let expected = test.0 + if !(validationState == expected) { + XCTFail("Expected \(expected), got \(validationState) for number \(card)") + } + } + + XCTAssertEqual( + .incomplete, + STPCardValidator.validationState(forNumber: "1", validatingCardBrand: false) + ) + XCTAssertEqual( + .incomplete, + STPCardValidator.validationState( + forNumber: "0000000000000000", + validatingCardBrand: false + ) + ) + XCTAssertEqual( + .incomplete, + STPCardValidator.validationState( + forNumber: "9999999999999995", + validatingCardBrand: false + ) + ) + XCTAssertEqual( + .valid, + STPCardValidator.validationState( + forNumber: "0000000000000000000", + validatingCardBrand: false + ) + ) + XCTAssertEqual( + .valid, + STPCardValidator.validationState( + forNumber: "9999999999999999998", + validatingCardBrand: false + ) + ) + XCTAssertEqual( + .incomplete, + STPCardValidator.validationState(forNumber: "4242424242424", validatingCardBrand: true) + ) + XCTAssertEqual( + .incomplete, + STPCardValidator.validationState(forNumber: nil, validatingCardBrand: true) + ) + } + + func testBrand() { + for test in STPCardValidatorTest.cardData { + XCTAssertEqual(STPCardValidator.brand(forNumber: test.1), test.0) + } + } + + func testLengthsForCardBrand() { + let tests: [(STPCardBrand, Set)] = [ + (.visa, Set([13, 16])), + (.mastercard, Set([16])), + (.amex, Set([15])), + (.discover, Set([16])), + (.dinersClub, Set([14, 16])), + (.JCB, Set([16])), + (.unionPay, Set([16, 19])), + (.unknown, Set([19])), + ] + for test in tests { + let lengths = STPCardValidator.lengths(for: test.0) as NSSet + let expected = test.1 as NSSet + if !lengths.isEqual(expected) { + XCTFail("Invalid lengths for brand \(test.0): expected \(expected), got \(lengths)") + } + } + } + + func testFragmentLength() { + let tests: [(STPCardBrand, Int)] = [ + (.visa, 4), + (.mastercard, 4), + (.amex, 5), + (.discover, 4), + (.dinersClub, 4), + (.JCB, 4), + (.unionPay, 4), + (.unknown, 4), + ] + for test in tests { + XCTAssertEqual(STPCardValidator.fragmentLength(for: test.0), test.1) + } + } + + func testMonthValidation() { + let tests: [(String, STPCardValidationState)] = [ + ("", .incomplete), + ("0", .incomplete), + ("1", .incomplete), + ("2", .valid), + ("9", .valid), + ("10", .valid), + ("12", .valid), + ("13", .invalid), + ("11a", .invalid), + ("x", .invalid), + ("100", .invalid), + ("00", .invalid), + ("13", .invalid), + ] + for test in tests { + XCTAssertEqual(STPCardValidator.validationState(forExpirationMonth: test.0), test.1) + } + } + + func testYearValidation() { + let tests: [(String, String, STPCardValidationState)] = [ + ("12", "15", .valid), + ("8", "15", .valid), + ("9", "15", .valid), + ("11", "16", .valid), + ("11", "99", .invalid), + ("01", "99", .invalid), + ("11", "50", .valid), + ("01", "50", .valid), + ("1", "50", .valid), + ("1", "99", .invalid), + ("00", "99", .invalid), + ("00", "50", .invalid), + ("12", "14", .invalid), + ("7", "15", .invalid), + ("12", "00", .invalid), + ("13", "16", .invalid), + ("12", "2", .incomplete), + ("12", "1", .incomplete), + ("12", "0", .incomplete), + ] + + for test in tests { + let state = STPCardValidator.validationState( + forExpirationYear: test.1, + inMonth: test.0, + inCurrentYear: 15, + currentMonth: 8 + ) + XCTAssertEqual(state, test.2, "Failed to validate \(test.0)/\(test.1)") + } + } + + func testCVCLength() { + let tests: [(STPCardBrand, UInt)] = [ + (.visa, 3), + (.mastercard, 3), + (.amex, 4), + (.discover, 3), + (.dinersClub, 3), + (.JCB, 3), + (.unionPay, 3), + (.unknown, 4), + ] + for test in tests { + let maxCVCLength = STPCardValidator.maxCVCLength(for: test.0) + XCTAssertEqual(maxCVCLength, test.1) + } + } + + func testCVCValidation() { + let tests: [(String, STPCardBrand, STPCardValidationState)] = [ + ("x", .visa, .invalid), + ("", .visa, .incomplete), + ("1", .visa, .incomplete), + ("12", .visa, .incomplete), + ("1x3", .visa, .invalid), + ("123", .visa, .valid), + ("123", .amex, .valid), + ("123", .unknown, .valid), + ("1234", .visa, .invalid), + ("1234", .amex, .valid), + ("12345", .amex, .invalid), + ] + + for test in tests { + let state = STPCardValidator.validationState(forCVC: test.0, cardBrand: test.1) + XCTAssertEqual(state, test.2) + } + } + + func testCardValidation() { + // swiftlint:disable:next large_tuple + let tests: [(String, UInt, UInt, String, STPCardValidationState)] = [ + ( + "4242424242424242", + 12, + 15, + "123", + .valid + ), + ( + "4242424242424242", + 12, + 15, + "x", + .invalid + ), + ( + "4242424242424242", + 12, + 15, + "1", + .incomplete + ), + ( + "4242424242424242", + 12, + 14, + "123", + .invalid + ), + ( + "4242424242424242", + 21, + 15, + "123", + .invalid + ), + ( + "42424242", + 12, + 15, + "123", + .incomplete + ), + ( + "378282246310005", + 12, + 15, + "1234", + .valid + ), + ( + "378282246310005", + 12, + 15, + "123", + .valid + ), + ( + "378282246310005", + 12, + 15, + "12345", + .invalid + ), + ( + "1234567812345678", + 12, + 15, + "12345", + .invalid + ), + ] + for test in tests { + let card = STPCardParams() + card.number = test.0 + card.expMonth = test.1 + card.expYear = test.2 + card.cvc = test.3 + let state = STPCardValidator.validationState( + forCard: card, + inCurrentYear: 15, + currentMonth: 8 + ) + if state != test.4 { + XCTFail( + "Wrong validation state for \(String(describing: card.number)). Expected \(test.4), got \(state))" + ) + } + } + } + + func testCBCFetch() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let mcExp = expectation(description: "Mastercard/CBC") + let visaExp = expectation(description: "Visa/CBC") + let justVisaExp = expectation(description: "Visa Only") + let paramsExp = expectation(description: "Params") + let emptyParamsExp = expectation(description: "Empty Params") + STPCardValidator.possibleBrands(forNumber: "513130") { result in + let brands = try! result.get() + XCTAssertEqual(brands, [.cartesBancaires, .mastercard]) + mcExp.fulfill() + } + STPCardValidator.possibleBrands(forNumber: "455673") { result in + let brands = try! result.get() + XCTAssertEqual(brands, [.cartesBancaires, .visa]) + visaExp.fulfill() + } + STPCardValidator.possibleBrands(forNumber: "424242") { result in + let brands = try! result.get() + XCTAssertEqual(brands, [.visa]) + justVisaExp.fulfill() + } + + let params = STPPaymentMethodCardParams() + params.number = "5131301234" + STPCardValidator.possibleBrands(forCard: params) { result in + let brands = try! result.get() + XCTAssertEqual(brands, [.cartesBancaires, .mastercard]) + paramsExp.fulfill() + } + + let paramsEmpty = STPPaymentMethodCardParams() + STPCardValidator.possibleBrands(forCard: paramsEmpty) { result in + let brands = try! result.get() + XCTAssertEqual(brands, Set(STPCardBrand.allCases)) + emptyParamsExp.fulfill() + } + + wait(for: [mcExp, visaExp, justVisaExp, paramsExp, emptyParamsExp], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPCertTest.swift b/Stripe/StripeiOSTests/STPCertTest.swift new file mode 100644 index 00000000..5280f9fe --- /dev/null +++ b/Stripe/StripeiOSTests/STPCertTest.swift @@ -0,0 +1,68 @@ +// +// STPCertTest.swift +// StripeiOS Tests +// +// Created by Phillip Cohen on 4/14/14. +// Copyright © 2014 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +let STPExamplePublishableKey = "bad_key" + +class STPCertTest: XCTestCase { + func testNoError() { + let expectation = self.expectation(description: "Token creation") + let client = STPAPIClient(publishableKey: STPExamplePublishableKey) + client.createToken( + withParameters: [:]) { token, error in + expectation.fulfill() + // Note that this API request *will* fail, but it will return error + // messages from the server and not be blocked by local cert checks + XCTAssertNil(token, "Expected no token") + XCTAssertNotNil(error, "Expected error") + } + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testExpired() { + createToken( + withBaseURL: URL(string: "https://expired.badssl.com/") + ) { token, error in + XCTAssertNil(token, "Token should be nil.") + XCTAssertEqual((error as NSError?)?.domain, "NSURLErrorDomain") + XCTAssertNotNil( + (error as NSError?)?.userInfo["NSURLErrorFailingURLPeerTrustErrorKey"], + "There should be a secTustRef for Foundation HTTPS errors" + ) + } + } + + func testMismatched() { + createToken( + withBaseURL: URL(string: "https://mismatched.stripe.com") + ) { token, error in + XCTAssertNil(token, "Token should be nil.") + XCTAssertEqual((error as NSError?)?.domain, "NSURLErrorDomain") + } + } + + // helper method + func createToken(withBaseURL baseURL: URL?, completion: @escaping STPTokenCompletionBlock) { + let expectation = self.expectation(description: "Token creation") + let client = STPAPIClient(publishableKey: STPExamplePublishableKey) + client.apiURL = baseURL + client.createToken( + withParameters: [:]) { token, error in + expectation.fulfill() + completion(token, error) + } + waitForExpectations(timeout: 20.0, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPConfirmCardOptionsTest.swift b/Stripe/StripeiOSTests/STPConfirmCardOptionsTest.swift new file mode 100644 index 00000000..23a508af --- /dev/null +++ b/Stripe/StripeiOSTests/STPConfirmCardOptionsTest.swift @@ -0,0 +1,31 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPConfirmCardOptionsTest.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 1/10/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +class STPConfirmCardOptionsTest: XCTestCase { + func testCVC() { + let cardOptions = STPConfirmCardOptions() + + XCTAssertNil(cardOptions.cvc, "Initial/default value should be nil.") + XCTAssertNil(cardOptions.network, "Initial/default value should be nil.") + + cardOptions.cvc = "123" + XCTAssertEqual(cardOptions.cvc, "123") + cardOptions.network = "visa" + XCTAssertEqual(cardOptions.network, "visa") + } + + func testEncoding() { + let propertyMap = STPConfirmCardOptions.propertyNamesToFormFieldNamesMapping() + let expected = [ + "cvc": "cvc", + "network": "network", + ] + XCTAssertEqual(propertyMap, expected) + } +} diff --git a/Stripe/StripeiOSTests/STPConfirmPaymentMethodOptionsTest.swift b/Stripe/StripeiOSTests/STPConfirmPaymentMethodOptionsTest.swift new file mode 100644 index 00000000..02cd737e --- /dev/null +++ b/Stripe/StripeiOSTests/STPConfirmPaymentMethodOptionsTest.swift @@ -0,0 +1,34 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPConfirmPaymentMethodOptionsTest.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 1/10/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +class STPConfirmPaymentMethodOptionsTest: XCTestCase { + func testCardOptions() { + let paymentMethodOptions = STPConfirmPaymentMethodOptions() + + XCTAssertNil(paymentMethodOptions.cardOptions, "Default card value should be nil.") + + let cardOptions = STPConfirmCardOptions() + paymentMethodOptions.cardOptions = cardOptions + XCTAssertEqual(paymentMethodOptions.cardOptions, cardOptions, "Should hold reference to set cardOptions.") + } + + func testFormEncoding() { + let propertyToFieldMap = STPConfirmPaymentMethodOptions.propertyNamesToFormFieldNamesMapping() + let expected = [ + "cardOptions": "card", + "alipayOptions": "alipay", + "blikOptions": "blik", + "weChatPayOptions": "wechat_pay", + "usBankAccountOptions": "us_bank_account", + "konbiniOptions": "konbini", + ] + + XCTAssertEqual(propertyToFieldMap, expected) + } +} diff --git a/Stripe/StripeiOSTests/STPConnectAccountAddressTest.swift b/Stripe/StripeiOSTests/STPConnectAccountAddressTest.swift new file mode 100644 index 00000000..573864d8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPConnectAccountAddressTest.swift @@ -0,0 +1,33 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPConnectAccountAddressTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 8/2/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPConnectAccountAddressTest: XCTestCase { + // MARK: STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertNil(STPConnectAccountAddress.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let address = STPConnectAccountAddress() + + let mapping = STPConnectAccountAddress.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(address.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(!formFieldName.isEmpty) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } +} diff --git a/Stripe/StripeiOSTests/STPConnectAccountFunctionalTest.swift b/Stripe/StripeiOSTests/STPConnectAccountFunctionalTest.swift new file mode 100644 index 00000000..372986d1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPConnectAccountFunctionalTest.swift @@ -0,0 +1,85 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPConnectAccountFunctionalTest.m +// StripeiOS Tests +// +// Created by Daniel Jackson on 1/8/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils + +class STPConnectAccountFunctionalTest: XCTestCase { + /// Client with test publishable key + var client: STPAPIClient! + var individual: STPConnectAccountIndividualParams! + var company: STPConnectAccountCompanyParams! + + override func setUp() { + super.setUp() + + client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + individual = STPConnectAccountIndividualParams() + individual.firstName = "Test" + var dob = DateComponents() + dob.day = 31 + dob.month = 8 + dob.year = 2006 + individual.dateOfBirth = dob + company = STPConnectAccountCompanyParams() + company.name = "Test" + } + + func testTokenCreation_terms_nil() { + XCTAssertNil( + STPConnectAccountParams( + tosShownAndAccepted: false, + individual: individual), + "Guard to prevent trying to call this with `NO`") + XCTAssertNil( + STPConnectAccountParams( + tosShownAndAccepted: false, + company: company), + "Guard to prevent trying to call this with `NO`") + } + + func testTokenCreation_customer() { + createToken( + STPConnectAccountParams(company: company), + shouldSucceed: true) + } + + func testTokenCreation_company() { + createToken( + STPConnectAccountParams(individual: individual), + shouldSucceed: true) + } + + func testTokenCreation_empty_init() { + createToken(STPConnectAccountParams(), shouldSucceed: true) + + } + + // MARK: - + + func createToken(_ params: STPConnectAccountParams?, shouldSucceed: Bool) { + let expectation = self.expectation(description: "Connect Account Token") + + client.createToken(withConnectAccount: params!) { token, error in + expectation.fulfill() + + if shouldSucceed { + XCTAssertNil(error) + XCTAssertNotNil(token) + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .account) + } else { + XCTAssertNil(token) + XCTAssertNotNil(error) + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPConnectAccountParamsTest.swift b/Stripe/StripeiOSTests/STPConnectAccountParamsTest.swift new file mode 100644 index 00000000..15d9a2bf --- /dev/null +++ b/Stripe/StripeiOSTests/STPConnectAccountParamsTest.swift @@ -0,0 +1,53 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPConnectAccountParamsTest.m +// StripeiOS Tests +// +// Created by Daniel Jackson on 1/10/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments + +class STPConnectAccountParamsTest: XCTestCase { + // MARK: - STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertEqual(STPConnectAccountParams.rootObjectName(), "account") + } + + func testBusinessType() { + let individual = STPConnectAccountIndividualParams() + let company = STPConnectAccountCompanyParams() + + XCTAssertEqual(STPConnectAccountParams(individual: individual).businessType, .individual) + XCTAssertEqual(STPConnectAccountParams(tosShownAndAccepted: true, individual: individual)!.businessType, .individual) + + XCTAssertEqual(STPConnectAccountParams(company: company).businessType, .company) + XCTAssertEqual(STPConnectAccountParams(tosShownAndAccepted: true, company: company)!.businessType, .company) + } + + func testBusinessTypeString() { + XCTAssertEqual("individual", STPConnectAccountParams.string(from: .individual)) + XCTAssertEqual("company", STPConnectAccountParams.string(from: .company)) + XCTAssertEqual(nil, STPConnectAccountParams.string(from: .none)) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let individual = STPConnectAccountIndividualParams() + let accountParams = STPConnectAccountParams(individual: individual) + + let mapping = STPConnectAccountParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(accountParams.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } +} diff --git a/Stripe/StripeiOSTests/STPCountryPickerInputFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPCountryPickerInputFieldSnapshotTests.swift new file mode 100644 index 00000000..f05827b8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCountryPickerInputFieldSnapshotTests.swift @@ -0,0 +1,27 @@ +// +// STPCountryPickerInputFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 12/2/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCountryPickerInputFieldSnapshotTests: STPSnapshotTestCase { + + func testDefault() { + let field = STPCountryPickerInputField() + field.sizeToFit() + field.frame.size.width = 200 + + STPSnapshotVerifyView(field) + } +} diff --git a/Stripe/StripeiOSTests/STPCustomerContextTest.swift b/Stripe/StripeiOSTests/STPCustomerContextTest.swift new file mode 100644 index 00000000..f622ac96 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCustomerContextTest.swift @@ -0,0 +1,671 @@ +// +// STPCustomerContextTest.swift +// StripeiOS Tests +// +// Created by David Estes on 9/20/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments + +class MockEphemeralKeyManager: STPEphemeralKeyManagerProtocol { + var ephemeralKey: STPEphemeralKey? + var error: Error? + + init( + key: STPEphemeralKey?, + error: Error? + ) { + self.ephemeralKey = key + self.error = error + } + + func getOrCreateKey(_ completion: @escaping STPEphemeralKeyCompletionBlock) { + completion(ephemeralKey, error) + } +} + +class STPCustomerContextTests: APIStubbedTestCase { + func stubRetrieveCustomers( + key: STPEphemeralKey, + returningCustomerJSON: [AnyHashable: Any], + expectedCount: Int, + apiClient: STPAPIClient + ) { + let exp = expectation(description: "retrieveCustomer") + exp.expectedFulfillmentCount = expectedCount + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/customers") ?? false + && urlRequest.httpMethod == "GET" + } response: { _ in + DispatchQueue.main.async { + // Fulfill after response is sent + exp.fulfill() + } + return HTTPStubsResponse( + jsonObject: returningCustomerJSON, + statusCode: 200, + headers: nil + ) + } + } + + func stubListPaymentMethods( + key: STPEphemeralKey, + paymentMethodJSONs: [[AnyHashable: Any]], + expectedCount: Int, + apiClient: STPAPIClient + ) { + let exp = expectation(description: "listPaymentMethod") + exp.expectedFulfillmentCount = expectedCount + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + && urlRequest.httpMethod == "GET" + { + // Check to make sure we pass the ephemeral key correctly + let keyFromHeader = urlRequest.allHTTPHeaderFields!["Authorization"]? + .replacingOccurrences(of: "Bearer ", with: "") + XCTAssertEqual(keyFromHeader, key.secret) + return true + } + return false + } response: { _ in + let paymentMethodsJSON = """ + { + "object": "list", + "url": "/v1/payment_methods", + "has_more": false, + "data": [ + ] + } + """ + var pmList = + try! JSONSerialization.jsonObject( + with: paymentMethodsJSON.data(using: .utf8)!, + options: [] + ) as! [AnyHashable: Any] + pmList["data"] = paymentMethodJSONs + DispatchQueue.main.async { + // Fulfill after response is sent + exp.fulfill() + } + return HTTPStubsResponse(jsonObject: pmList, statusCode: 200, headers: nil) + } + } + + func testGetOrCreateKeyErrorForwardedToRetrieveCustomer() { + let exp = expectation(description: "retrieveCustomer") + let expectedError = NSError(domain: "test", code: 123, userInfo: nil) + let apiClient = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/customers") ?? false + } response: { _ in + XCTFail("Retrieve customer should not be called") + return HTTPStubsResponse(error: NSError(domain: "test", code: 100, userInfo: nil)) + } + let ekm = MockEphemeralKeyManager(key: nil, error: expectedError) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + sut.retrieveCustomer { customer, error in + XCTAssertNil(customer) + XCTAssertEqual((error as NSError?)?.domain, expectedError.domain) + exp.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testInitRetrievesResourceKeyAndCustomerAndPaymentMethods() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + XCTAssertNotNil(sut) + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrieveCustomerUsesCachedCustomerIfNotExpired() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomer = STPFixtures.customerWithSingleCardTokenSource() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + + // apiClient.retrieveCustomer should be called once, when the context is initialized. + // When sut.retrieveCustomer is called below, the cached customer will be used. + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + waitForExpectations(timeout: 2, handler: nil) + let exp2 = expectation(description: "retrieveCustomer again") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.retrieveCustomer { customer, _ in + XCTAssertEqual(customer!.stripeID, expectedCustomer.stripeID) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrieveCustomerDoesNotUseCachedCustomerIfExpired() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomer = STPFixtures.customerWithSingleCardTokenSource() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + + // apiClient.retrieveCustomer should be called twice: + // - when the context is initialized, + // - when sut.retrieveCustomer is called below, as the cached customer has expired. + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 2, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then reset and check cache + let exp2 = expectation(description: "retrieveCustomer again") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.customerRetrievedDate = Date(timeIntervalSinceNow: -70) + sut.retrieveCustomer { customer, _ in + XCTAssertEqual(customer!.stripeID, expectedCustomer.stripeID) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrieveCustomerDoesNotUseCachedCustomerAfterClearingCache() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomer = STPFixtures.customerWithSingleCardTokenSource() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + + // apiClient.retrieveCustomer should be called twice: + // - when the context is initialized, + // - when sut.retrieveCustomer is called below, as the cached customer has been cleared. + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 2, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then reset and check cache + let exp2 = expectation(description: "retrieveCustomer again") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.clearCache() + sut.retrieveCustomer { customer, _ in + XCTAssertEqual(customer!.stripeID, expectedCustomer.stripeID) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrievePaymentMethodsUsesCacheIfNotExpired() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()] + let apiClient = stubbedAPIClient() + + // apiClient.listPaymentMethods should be called once, when the context is initialized. + // When sut.listPaymentMethods is called below, the cached list will be used. + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp2 = expectation(description: "listPaymentMethods") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.listPaymentMethodsForCustomer { paymentMethods, _ in + XCTAssertEqual(paymentMethods!.count, expectedPaymentMethods.count) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrievePaymentMethodsDoesNotUseCacheIfExpired() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()] + let apiClient = stubbedAPIClient() + + // apiClient.listPaymentMethods should be called twice: + // - when the context is initialized, + // - when sut.listPaymentMethods is called below, as the cached list has expired. + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 2, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp2 = expectation(description: "listPaymentMethods") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.paymentMethodsRetrievedDate = Date(timeIntervalSinceNow: -70) + sut.listPaymentMethodsForCustomer { paymentMethods, _ in + XCTAssertEqual(paymentMethods!.count, expectedPaymentMethods.count) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testRetrievePaymentMethodsDoesNotUseCacheAfterClearingCache() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()] + let apiClient = stubbedAPIClient() + + // apiClient.listPaymentMethods should be called twice: + // - when the context is initialized, + // - when sut.listPaymentMethods is called below, as the cached list has been cleared + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 2, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp2 = expectation(description: "listPaymentMethods") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.clearCache() + sut.listPaymentMethodsForCustomer { paymentMethods, _ in + XCTAssertEqual(paymentMethods!.count, expectedPaymentMethods.count) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testSetCustomerShippingCallsAPIClientCorrectly() { + let address = STPFixtures.address() + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let exp = expectation(description: "updateCustomer") + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/customers") ?? false + && urlRequest.httpMethod == "POST" + { + let state = urlRequest.queryItems?.first(where: { item in + item.name == "shipping[address][state]" + })! + XCTAssertEqual(state?.value, address.state) + return true + } + return false + } response: { _ in + exp.fulfill() + return HTTPStubsResponse( + jsonObject: expectedCustomerJSON, + statusCode: 200, + headers: nil + ) + } + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + let exp2 = expectation(description: "updateCustomerWithShipping") + sut.updateCustomer(withShippingAddress: address) { error in + XCTAssertNil(error) + exp2.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testAttachPaymentMethodCallsAPIClientCorrectly() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + let expectedPaymentMethod = STPFixtures.paymentMethod() + let expectedPaymentMethodJSON = STPFixtures.paymentMethodJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let exp = expectation(description: "payment method attach") + // We're attaching 2 payment methods: + exp.expectedFulfillmentCount = 2 + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_method") ?? false + && urlRequest.httpMethod == "POST" + { + return true + } + return false + } response: { _ in + exp.fulfill() + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodJSON, + statusCode: 200, + headers: nil + ) + } + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + let exp2 = expectation(description: "CustomerContext attachPaymentMethod") + sut.attachPaymentMethod(toCustomer: expectedPaymentMethods.first!) { error in + XCTAssertNil(error) + exp2.fulfill() + } + + let exp3 = expectation(description: "CustomerContext attachPaymentMethod with ID") + sut.attachPaymentMethodToCustomer(paymentMethodId: expectedPaymentMethod.stripeId) { + error in + XCTAssertNil(error) + exp3.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testDetachPaymentMethodCallsAPIClientCorrectly() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let apiClient = stubbedAPIClient() + let expectedPaymentMethod = STPFixtures.paymentMethod() + let expectedPaymentMethodJSON = STPFixtures.paymentMethodJSON() + let expectedPaymentMethods = [STPFixtures.paymentMethod()] + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: [], + expectedCount: 1, + apiClient: apiClient + ) + + let exp = expectation(description: "payment method detach") + // We're detaching 2 payment methods: + exp.expectedFulfillmentCount = 2 + stub { urlRequest in + if urlRequest.url?.absoluteString.contains("/payment_method") ?? false + && urlRequest.httpMethod == "POST" + { + return true + } + return false + } response: { _ in + exp.fulfill() + return HTTPStubsResponse( + jsonObject: expectedPaymentMethodJSON, + statusCode: 200, + headers: nil + ) + } + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + let exp2 = expectation(description: "CustomerContext detachPaymentMethod") + sut.detachPaymentMethod(fromCustomer: expectedPaymentMethods.first!) { error in + XCTAssertNil(error) + exp2.fulfill() + } + + let exp3 = expectation(description: "CustomerContext detachPaymentMethod with ID") + sut.detachPaymentMethodFromCustomer(paymentMethodId: expectedPaymentMethod.stripeId) { + error in + XCTAssertNil(error) + exp3.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testFiltersApplePayPaymentMethodsByDefault() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let expectedPaymentMethodsJSON = [ + STPFixtures.paymentMethodJSON(), STPFixtures.applePayPaymentMethodJSON(), + ] + let apiClient = stubbedAPIClient() + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp2 = expectation(description: "listPaymentMethods") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.listPaymentMethodsForCustomer { paymentMethods, _ in + // Apple Pay should be filtered out + XCTAssertEqual(paymentMethods!.count, 1) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testIncludesApplePayPaymentMethods() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithSingleCardTokenSourceJSON() + let expectedPaymentMethodsJSON = [ + STPFixtures.paymentMethodJSON(), STPFixtures.applePayPaymentMethodJSON(), + ] + let apiClient = stubbedAPIClient() + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + sut.includeApplePayPaymentMethods = true + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp2 = expectation(description: "listPaymentMethods") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.listPaymentMethodsForCustomer { paymentMethods, _ in + // Apple Pay should be included + XCTAssertEqual(paymentMethods!.count, 2) + exp2.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testFiltersApplePaySourcesByDefault() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithCardAndApplePaySourcesJSON() + let expectedPaymentMethodsJSON = [ + STPFixtures.paymentMethodJSON(), STPFixtures.applePayPaymentMethodJSON(), + ] + let apiClient = stubbedAPIClient() + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp = expectation(description: "retrieveCustomer") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.retrieveCustomer { customer, _ in + // Apple Pay should be filtered out + XCTAssertEqual(customer!.sources.count, 1) + exp.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testIncludeApplePaySources() { + let customerKey = STPFixtures.ephemeralKey() + let expectedCustomerJSON = STPFixtures.customerWithCardAndApplePaySourcesJSON() + let expectedPaymentMethodsJSON = [ + STPFixtures.paymentMethodJSON(), STPFixtures.applePayPaymentMethodJSON(), + ] + let apiClient = stubbedAPIClient() + + stubRetrieveCustomers( + key: customerKey, + returningCustomerJSON: expectedCustomerJSON, + expectedCount: 1, + apiClient: apiClient + ) + stubListPaymentMethods( + key: customerKey, + paymentMethodJSONs: expectedPaymentMethodsJSON, + expectedCount: 1, + apiClient: apiClient + ) + + let ekm = MockEphemeralKeyManager(key: customerKey, error: nil) + let sut = STPCustomerContext(keyManager: ekm, apiClient: apiClient) + sut.includeApplePayPaymentMethods = true + // Give the mocked API request a little time to complete and cache the customer, then check cache + let exp = expectation(description: "retrieveCustomer") + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + ohhttpDelay) { + sut.retrieveCustomer { customer, _ in + // Apple Pay should be filtered out + XCTAssertEqual(customer!.sources.count, 2) + exp.fulfill() + } + } + waitForExpectations(timeout: 2, handler: nil) + } + + let ohhttpDelay = 0.1 +} diff --git a/Stripe/StripeiOSTests/STPCustomerTest.swift b/Stripe/StripeiOSTests/STPCustomerTest.swift new file mode 100644 index 00000000..54662c7a --- /dev/null +++ b/Stripe/StripeiOSTests/STPCustomerTest.swift @@ -0,0 +1,63 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPCustomerTest.m +// Stripe +// +// Created by Ben Guo on 7/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +class STPCustomerTest: XCTestCase { + func testDecoding_invalidJSON() { + let sut = STPCustomer.decodedObject(fromAPIResponse: [:]) + XCTAssertNil(sut) + } + + func testDecoding_validJSON() { + var card1 = STPTestUtils.jsonNamed("Card") + card1!["id"] = "card_123" + + var card2 = STPTestUtils.jsonNamed("Card") + card2!["id"] = "card_456" + + var applePayCard1 = STPTestUtils.jsonNamed("Card") + applePayCard1!["id"] = "card_apple_pay1" + applePayCard1!["tokenization_method"] = "apple_pay" + + var applePayCard2 = applePayCard1 + applePayCard2!["id"] = "card_apple_pay2" + + let cardSource = STPTestUtils.jsonNamed("CardSource") + let threeDSSource = STPTestUtils.jsonNamed("3DSSource") + + var customer = STPTestUtils.jsonNamed("Customer") + var sources = customer!["sources"] as? [AnyHashable: Any] + sources?["data"] = [applePayCard1, card1, applePayCard2, card2, cardSource, threeDSSource] + customer!["default_source"] = card1!["id"] + if let sources { + customer!["sources"] = sources + } + + guard let sut = STPCustomer.decodedObject(fromAPIResponse: customer) else { + XCTFail() + return + } + XCTAssertEqual(sut.stripeID, customer!["id"] as! String) + XCTAssertTrue(sut.sources.count == 4) + XCTAssertEqual(sut.sources[0].stripeID, card1!["id"] as! String) + XCTAssertEqual(sut.sources[1].stripeID, card2!["id"] as! String) + XCTAssertEqual(sut.defaultSource!.stripeID, card1!["id"] as! String) + XCTAssertEqual(sut.sources[2].stripeID, cardSource!["id"] as! String) + XCTAssertEqual(sut.sources[3].stripeID, threeDSSource!["id"] as! String) + + XCTAssertEqual(sut.shippingAddress!.name, (customer!["shipping"] as! [AnyHashable: Any])["name"] as? String) + XCTAssertEqual(sut.shippingAddress!.phone, (customer!["shipping"] as! [AnyHashable: Any])["phone"] as? String) + let addressDict = (customer!["shipping"] as! [AnyHashable: Any])["address"] as! [AnyHashable: String] + XCTAssertEqual(sut.shippingAddress!.city, addressDict["city"]) + XCTAssertEqual(sut.shippingAddress!.country, addressDict["country"]) + XCTAssertEqual(sut.shippingAddress!.line1, addressDict["line1"]) + XCTAssertEqual(sut.shippingAddress!.line2, addressDict["line2"]) + XCTAssertEqual(sut.shippingAddress!.postalCode, addressDict["postal_code"]) + XCTAssertEqual(sut.shippingAddress!.state, addressDict["state"]) + } +} diff --git a/Stripe/StripeiOSTests/STPE2ETest.swift b/Stripe/StripeiOSTests/STPE2ETest.swift new file mode 100644 index 00000000..7361d1db --- /dev/null +++ b/Stripe/StripeiOSTests/STPE2ETest.swift @@ -0,0 +1,156 @@ +// +// STPE2ETest.swift +// StripeiOS Tests +// +// Created by David Estes on 2/16/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Stripe +import XCTest + +class STPE2ETest: XCTestCase { + let E2ETestTimeout: TimeInterval = 120 + + struct E2EExpectation { + var amount: Int + var currency: String + var accountID: String + } + + class E2EBackend { + static let backendAPIURL = URL( + string: "https://stp-mobile-ci-test-backend-e1b3.stripedemos.com/e2e" + )! + + func createPaymentIntent( + completion: @escaping (STPPaymentIntentParams, E2EExpectation) -> Void + ) { + requestAPI("create_pi", method: "POST") { (json) in + let paymentIntentClientSecret = json["paymentIntent"] as! String + let expectedAmount = json["expectedAmount"] as! Int + let expectedCurrency = json["expectedCurrency"] as! String + let expectedAccountID = json["expectedAccountID"] as! String + let publishableKey = json["publishableKey"] as! String + STPAPIClient.shared.publishableKey = publishableKey + completion( + STPPaymentIntentParams(clientSecret: paymentIntentClientSecret), + E2EExpectation( + amount: expectedAmount, + currency: expectedCurrency, + accountID: expectedAccountID + ) + ) + } + } + + func fetchPaymentIntent(id: String, completion: @escaping (E2EExpectation) -> Void) { + requestAPI("fetch_pi", queryItems: [URLQueryItem(name: "pi", value: id)]) { (json) in + let resultAmount = json["amount"] as! Int + let resultCurrency = json["currency"] as! String + let resultAccountID = json["on_behalf_of"] as! String + completion( + E2EExpectation( + amount: resultAmount, + currency: resultCurrency, + accountID: resultAccountID + ) + ) + } + } + + private func requestAPI( + _ resource: String, + method: String = "GET", + queryItems: [URLQueryItem] = [], + completion: @escaping ([String: Any]) -> Void + ) { + var url = URLComponents( + url: Self.backendAPIURL.appendingPathComponent(resource), + resolvingAgainstBaseURL: false + )! + url.queryItems = queryItems + var request = URLRequest(url: url.url!) + request.httpMethod = method + let task = URLSession.shared.dataTask( + with: request, + completionHandler: { (data, _, error) in + guard let data = data, + let json = try? JSONSerialization.jsonObject(with: data, options: []) + as? [String: Any] + else { + XCTFail( + "Did not receive valid JSON response from E2E server. \(String(describing: error))" + ) + return + } + DispatchQueue.main.async { + completion(json) + } + } + ) + task.resume() + } + } + + static let TestPM: STPPaymentMethodParams = { + let testCard = STPPaymentMethodCardParams() + testCard.number = "4242424242424242" + testCard.expYear = 2050 + testCard.expMonth = 12 + testCard.cvc = "123" + return STPPaymentMethodParams(card: testCard, billingDetails: nil, metadata: nil) + }() + + // MARK: LOG.04.01c + // In this test, a PaymentIntent object is created from an example merchant backend, + // confirmed by the iOS SDK, and then retrieved to validate that the original amount, + // currency, and merchant are the same as the original inputs. + // (Don't network mock this one!) + func testE2E() throws { + continueAfterFailure = false + let backend = E2EBackend() + let createPI = XCTestExpectation(description: "Create PaymentIntent") + let fetchPIBackend = XCTestExpectation( + description: "Fetch and check PaymentIntent via backend" + ) + let fetchPIClient = XCTestExpectation( + description: "Fetch and check PaymentIntent via client" + ) + let confirmPI = XCTestExpectation(description: "Confirm PaymentIntent") + + // Create a PaymentIntent + backend.createPaymentIntent { (pip, expected) in + createPI.fulfill() + + // Confirm the PaymentIntent using a test card + pip.paymentMethodParams = STPE2ETest.TestPM + STPAPIClient.shared.confirmPaymentIntent(with: pip) { (confirmedPI, confirmError) in + confirmPI.fulfill() + XCTAssertNotNil(confirmedPI) + XCTAssertNil(confirmError) + + // Check the PI information using the backend + backend.fetchPaymentIntent(id: pip.stripeId!) { (expectationResult) in + XCTAssertEqual(expectationResult.amount, expected.amount) + XCTAssertEqual(expectationResult.accountID, expected.accountID) + XCTAssertEqual(expectationResult.currency, expected.currency) + fetchPIBackend.fulfill() + } + + // Check the PI information using the client + STPAPIClient.shared.retrievePaymentIntent(withClientSecret: pip.clientSecret) { + (fetchedPI, fetchError) in + XCTAssertNil(fetchError) + let fetchedPI = fetchedPI! + XCTAssertEqual(fetchedPI.status, .succeeded) + XCTAssertEqual(fetchedPI.amount, expected.amount) + XCTAssertEqual(fetchedPI.currency, expected.currency) + // The client can't check the "on_behalf_of" field, so we check it via the merchant test above. + fetchPIClient.fulfill() + } + } + } + wait(for: [createPI, confirmPI, fetchPIBackend, fetchPIClient], timeout: E2ETestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPEphemeralKeyManagerTest.swift b/Stripe/StripeiOSTests/STPEphemeralKeyManagerTest.swift new file mode 100644 index 00000000..90b507ee --- /dev/null +++ b/Stripe/StripeiOSTests/STPEphemeralKeyManagerTest.swift @@ -0,0 +1,155 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPEphemeralKeyManagerTest.m +// Stripe +// +// Created by Ben Guo on 5/9/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import Stripe + +class FakeEphemeralKeyProvider: NSObject, STPCustomerEphemeralKeyProvider { + var response: [AnyHashable: Any]? + var expectation: XCTestExpectation? + + init(response: [AnyHashable: Any]?, expectation: XCTestExpectation?) { + self.response = response + self.expectation = expectation + } + + func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping StripePayments.STPJSONResponseCompletionBlock) { + completion(response!, nil) + expectation?.fulfill() + } +} + +class FailingEphemeralKeyProvider: NSObject, STPCustomerEphemeralKeyProvider { + func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping StripePayments.STPJSONResponseCompletionBlock) { + XCTFail("createCustomerKey should not be called") + } +} + +class DelayingEphemeralKeyProvider: NSObject, STPCustomerEphemeralKeyProvider { + var response: [AnyHashable: Any] + var expectation: XCTestExpectation + + init(response: [AnyHashable: Any], expectation: XCTestExpectation) { + self.response = response + self.expectation = expectation + } + + func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping StripePayments.STPJSONResponseCompletionBlock) { + expectation.fulfill() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { + completion(self.response, nil) + }) + } +} + +class STPEphemeralKeyManagerTest: XCTestCase { + let apiVersion = "2015-03-03" + + override func setUp() { + super.setUp() + } + + func mockKeyProvider(withKeyResponse keyResponse: [AnyHashable: Any]?) -> Any? { + let exp = expectation(description: "createCustomerKey") + let mockKeyProvider = FakeEphemeralKeyProvider(response: keyResponse, expectation: exp) + return mockKeyProvider + } + + func testgetOrCreateKeyCreatesNewKeyAfterInit() { + let expectedKey = STPFixtures.ephemeralKey() + let keyResponse = expectedKey.allResponseFields + let mockKeyProvider = self.mockKeyProvider(withKeyResponse: keyResponse) + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + let exp = expectation(description: "getOrCreateKey") + sut.getOrCreateKey({ resourceKey, error in + XCTAssertEqual(resourceKey, expectedKey) + XCTAssertNil(error) + exp.fulfill() + }) + waitForExpectations(timeout: 2, handler: nil) + } + + func testgetOrCreateKeyUsesStoredKeyIfNotExpiring() { + let mockKeyProvider = FailingEphemeralKeyProvider() + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + let expectedKey = STPFixtures.ephemeralKey() + sut.ephemeralKey = expectedKey + let exp = expectation(description: "getOrCreateKey") + sut.getOrCreateKey({ resourceKey, error in + XCTAssertEqual(resourceKey, expectedKey) + XCTAssertNil(error) + exp.fulfill() + }) + waitForExpectations(timeout: 2, handler: nil) + } + + func testgetOrCreateKeyCreatesNewKeyIfExpiring() { + let expectedKey = STPFixtures.ephemeralKey() + let keyResponse = expectedKey.allResponseFields + let mockKeyProvider = self.mockKeyProvider(withKeyResponse: keyResponse) + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + sut.ephemeralKey = STPFixtures.expiringEphemeralKey() + let exp = expectation(description: "retrieve") + sut.getOrCreateKey({ resourceKey, error in + XCTAssertEqual(resourceKey, expectedKey) + XCTAssertNil(error) + exp.fulfill() + }) + waitForExpectations(timeout: 2, handler: nil) + } + + func testgetOrCreateKeyCoalescesRepeatCalls() { + let expectedKey = STPFixtures.ephemeralKey() + let keyResponse = expectedKey.allResponseFields + let createExp = expectation(description: "createKey") + createExp.assertForOverFulfill = true + + let mockKeyProvider = DelayingEphemeralKeyProvider(response: keyResponse, expectation: createExp) + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + let getExp1 = expectation(description: "getOrCreateKey") + sut.getOrCreateKey({ ephemeralKey, error in + XCTAssertEqual(ephemeralKey, expectedKey) + XCTAssertNil(error) + getExp1.fulfill() + }) + let getExp2 = expectation(description: "getOrCreateKey") + sut.getOrCreateKey({ ephemeralKey, error in + XCTAssertEqual(ephemeralKey, expectedKey) + XCTAssertNil(error) + getExp2.fulfill() + }) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnterForegroundRefreshesResourceKeyIfExpiring() { + let key = STPFixtures.expiringEphemeralKey() + let keyResponse = key.allResponseFields + let mockKeyProvider = self.mockKeyProvider(withKeyResponse: keyResponse) + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + XCTAssertNotNil(sut) + NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnterForegroundDoesNotRefreshResourceKeyIfNotExpiring() { + let mockKeyProvider = FailingEphemeralKeyProvider() + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + sut.ephemeralKey = STPFixtures.ephemeralKey() + NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + } + + func testThrottlingEnterForegroundRefreshes() { + let mockKeyProvider = FailingEphemeralKeyProvider() + let sut = STPEphemeralKeyManager(keyProvider: mockKeyProvider, apiVersion: apiVersion, performsEagerFetching: true) + sut.ephemeralKey = STPFixtures.expiringEphemeralKey() + sut.lastEagerKeyRefresh = Date(timeIntervalSinceNow: -60) + NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPEphemeralKeyTest.swift b/Stripe/StripeiOSTests/STPEphemeralKeyTest.swift new file mode 100644 index 00000000..b424c92a --- /dev/null +++ b/Stripe/StripeiOSTests/STPEphemeralKeyTest.swift @@ -0,0 +1,32 @@ +// +// STPEphemeralKeyTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 5/17/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPEphemeralKeyTest: XCTestCase { + func testDecoding() { + let json = STPTestUtils.jsonNamed("EphemeralKey")! + let key = STPEphemeralKey.decodedObject(fromAPIResponse: json)! + XCTAssertEqual(key.stripeID, json["id"] as! String) + XCTAssertEqual(key.secret, json["secret"] as! String) + XCTAssertEqual( + key.created, + Date(timeIntervalSince1970: TimeInterval((json["created"] as! NSNumber).doubleValue)) + ) + XCTAssertEqual( + key.expires, + Date(timeIntervalSince1970: TimeInterval((json["expires"] as! NSNumber).doubleValue)) + ) + XCTAssertEqual(key.livemode, (json["livemode"] as! NSNumber).boolValue) + XCTAssertEqual(key.customerID, "cus_123") + } +} diff --git a/Stripe/StripeiOSTests/STPErrorBridgeTest.m b/Stripe/StripeiOSTests/STPErrorBridgeTest.m new file mode 100644 index 00000000..bdcc414d --- /dev/null +++ b/Stripe/StripeiOSTests/STPErrorBridgeTest.m @@ -0,0 +1,37 @@ +// +// STPErrorBridgeTest.m +// StripeiOS Tests +// +// Created by David Estes on 9/23/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +@import Stripe; +@import XCTest; +@import PassKit; +@import StripePaymentsObjcTestUtils; + +@interface STPErrorBridgeTest : XCTestCase + +@end + +@implementation STPErrorBridgeTest + +- (void)testSTPErrorBridge { + // Grab a constant from each class, just to make sure we didn't forget to include the bridge: + XCTAssertEqual(STPInvalidRequestError, 50); + XCTAssertEqualObjects(STPError.errorMessageKey, @"com.stripe.lib:ErrorMessageKey"); + NSDictionary *json = @{ + @"error": @{ + @"type": @"invalid_request_error", + @"message": @"Your card number is incorrect.", + @"code": @"incorrect_number" + } + }; + + // Make sure we can parse a Stripe response + NSError *expectedError = [NSError stp_errorFromStripeResponse:json]; + XCTAssertEqualObjects(expectedError.domain, STPError.stripeDomain); +} + +@end diff --git a/Stripe/StripeiOSTests/STPFPXBankBrandTest.swift b/Stripe/StripeiOSTests/STPFPXBankBrandTest.swift new file mode 100644 index 00000000..a540deb2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPFPXBankBrandTest.swift @@ -0,0 +1,104 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPFPXBankBrandTest.m +// StripeiOS Tests +// +// Created by David Estes on 8/26/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPFPXBankBrandTest: XCTestCase { + func testStringFromBrand() { + let brands: [STPFPXBankBrand] = [ + .affinBank, + .allianceBank, + .ambank, + .bankIslam, + .bankMuamalat, + .bankRakyat, + .BSN, + .CIMB, + .hongLeongBank, + .HSBC, + .KFH, + .maybank2E, + .maybank2U, + .ocbc, + .publicBank, + .CIMB, + .RHB, + .standardChartered, + .UOB, + .unknown, + ] + + for brand in brands { + let brandName = STPFPXBank.stringFrom(brand) + let brandID = STPFPXBank.identifierFrom(brand) + let reverseTransformedBrand = STPFPXBank.brandFrom(brandID) + XCTAssertEqual(reverseTransformedBrand, brand) + + switch brand { + case .affinBank: + XCTAssertEqual(brandID, "affin_bank") + XCTAssertEqual(brandName, "Affin Bank") + case .allianceBank: + XCTAssertEqual(brandID, "alliance_bank") + XCTAssertEqual(brandName, "Alliance Bank") + case .ambank: + XCTAssertEqual(brandID, "ambank") + XCTAssertEqual(brandName, "AmBank") + case .bankIslam: + XCTAssertEqual(brandID, "bank_islam") + XCTAssertEqual(brandName, "Bank Islam") + case .bankMuamalat: + XCTAssertEqual(brandID, "bank_muamalat") + XCTAssertEqual(brandName, "Bank Muamalat") + case .bankRakyat: + XCTAssertEqual(brandID, "bank_rakyat") + XCTAssertEqual(brandName, "Bank Rakyat") + case .BSN: + XCTAssertEqual(brandID, "bsn") + XCTAssertEqual(brandName, "BSN") + case .CIMB: + XCTAssertEqual(brandID, "cimb") + XCTAssertEqual(brandName, "CIMB Clicks") + case .hongLeongBank: + XCTAssertEqual(brandID, "hong_leong_bank") + XCTAssertEqual(brandName, "Hong Leong Bank") + case .HSBC: + XCTAssertEqual(brandID, "hsbc") + XCTAssertEqual(brandName, "HSBC BANK") + case .KFH: + XCTAssertEqual(brandID, "kfh") + XCTAssertEqual(brandName, "KFH") + case .maybank2E: + XCTAssertEqual(brandID, "maybank2e") + XCTAssertEqual(brandName, "Maybank2E") + case .maybank2U: + XCTAssertEqual(brandID, "maybank2u") + XCTAssertEqual(brandName, "Maybank2U") + case .ocbc: + XCTAssertEqual(brandID, "ocbc") + XCTAssertEqual(brandName, "OCBC Bank") + case .publicBank: + XCTAssertEqual(brandID, "public_bank") + XCTAssertEqual(brandName, "Public Bank") + case .RHB: + XCTAssertEqual(brandID, "rhb") + XCTAssertEqual(brandName, "RHB Bank") + case .standardChartered: + XCTAssertEqual(brandID, "standard_chartered") + XCTAssertEqual(brandName, "Standard Chartered") + case .UOB: + XCTAssertEqual(brandID, "uob") + XCTAssertEqual(brandName, "UOB Bank") + case .unknown: + XCTAssertEqual(brandID, "unknown") + XCTAssertEqual(brandName, "Unknown") + @unknown default: + break + } + } + } +} diff --git a/Stripe/StripeiOSTests/STPFileFunctionalTest.swift b/Stripe/StripeiOSTests/STPFileFunctionalTest.swift new file mode 100644 index 00000000..d161daa3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPFileFunctionalTest.swift @@ -0,0 +1,85 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPFileFunctionalTest.m +// Stripe +// +// Created by Charles Scalesse on 1/8/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPFileFunctionalTest: STPNetworkStubbingTestCase { + func testImage() -> UIImage { + return UIImage( + named: "stp_test_upload_image.jpeg", + in: Bundle(for: STPFileFunctionalTest.self), + compatibleWith: nil)! + } + + func testCreateFileForIdentityDocument() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "File creation for identity document") + + let image = testImage() + + client.uploadImage( + image, + purpose: .identityDocument) { file, error in + expectation.fulfill() + XCTAssertNil(error, "error should be nil") + + XCTAssertNotNil(file?.fileId) + XCTAssertNotNil(file?.created) + XCTAssertEqual(file?.purpose, .identityDocument) + XCTAssertNotNil(file?.size) + XCTAssertEqual("jpg", file?.type) + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateFileForDisputeEvidence() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "File creation for dispute evidence") + + let image = testImage() + + client.uploadImage( + image, + purpose: .disputeEvidence) { file, error in + expectation.fulfill() + XCTAssertNil(error, "error should be nil") + + XCTAssertNotNil(file?.fileId) + XCTAssertNotNil(file?.created) + XCTAssertEqual(file?.purpose, .disputeEvidence) + XCTAssertNotNil(file?.size) + XCTAssertEqual("jpg", file?.type) + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testInvalidKey() { + let client = STPAPIClient(publishableKey: "not_a_valid_key_asdf") + + let expectation = self.expectation(description: "Bad file creation") + + let image = testImage() + + client.uploadImage( + image, + purpose: .identityDocument) { file, error in + expectation.fulfill() + XCTAssertNil(file, "file should be nil") + XCTAssertNotNil(error, "error should not be nil") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPFileTest.swift b/Stripe/StripeiOSTests/STPFileTest.swift new file mode 100644 index 00000000..90383dcf --- /dev/null +++ b/Stripe/StripeiOSTests/STPFileTest.swift @@ -0,0 +1,99 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPFileTest.m +// Stripe +// +// Created by Charles Scalesse on 1/8/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +import XCTest + +class STPFileTest: XCTestCase { + // MARK: - STPFilePurpose Tests + + func testPurposeFromString() { + XCTAssertEqual(STPFile.purpose(from: "dispute_evidence"), .disputeEvidence) + XCTAssertEqual(STPFile.purpose(from: "DISPUTE_EVIDENCE"), .disputeEvidence) + + XCTAssertEqual(STPFile.purpose(from: "identity_document"), .identityDocument) + XCTAssertEqual(STPFile.purpose(from: "IDENTITY_DOCUMENT"), .identityDocument) + + XCTAssertEqual(STPFile.purpose(from: "unknown"), .unknown) + XCTAssertEqual(STPFile.purpose(from: "UNKNOWN"), .unknown) + + XCTAssertEqual(STPFile.purpose(from: "garbage"), .unknown) + XCTAssertEqual(STPFile.purpose(from: "GARBAGE"), .unknown) + } + + func testStringFromPurpose() { + let values: [STPFilePurpose] = [ + .disputeEvidence, + .identityDocument, + .unknown, + ] + + for purpose in values { + let string = STPFile.string(from: purpose) + + switch purpose { + case .disputeEvidence: + XCTAssertEqual(string, "dispute_evidence") + case .identityDocument: + XCTAssertEqual(string, "identity_document") + case .unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - Equality Tests + + func testFileEquals() { + let file1 = STPFile.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("FileUpload")) + let file2 = STPFile.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("FileUpload")) + + XCTAssertEqual(file1, file1) + XCTAssertEqual(file1, file2) + + XCTAssertEqual(file1?.hash, file1?.hash) + XCTAssertEqual(file1?.hash, file2?.hash) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "id", + "created", + "size", + "purpose", + "type", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("FileUpload") + response!.removeValue(forKey: field) + + XCTAssertNil(STPFile.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPFile.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("FileUpload"))) + } + + func testInitializingFileWithAttributeDictionary() { + let response = STPTestUtils.jsonNamed("FileUpload")! + let file = STPFile.decodedObject(fromAPIResponse: response)! + + XCTAssertEqual(file.fileId, "file_1AZl0o2eZvKYlo2CoIkwLzfd") + XCTAssertEqual(file.created, Date(timeIntervalSince1970: 1498674938)) + XCTAssertEqual(file.purpose, .disputeEvidence) + XCTAssertEqual(file.size, NSNumber(value: 34478)) + XCTAssertEqual(file.type, "jpg") + + XCTAssertEqual(file.allResponseFields as NSDictionary, response as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPFloatingPlaceholderTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPFloatingPlaceholderTextFieldSnapshotTests.swift new file mode 100644 index 00000000..38fcf912 --- /dev/null +++ b/Stripe/StripeiOSTests/STPFloatingPlaceholderTextFieldSnapshotTests.swift @@ -0,0 +1,439 @@ +// +// STPFloatingPlaceholderTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/9/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPFloatingPlaceholderTextFieldSnapshotTests: STPSnapshotTestCase { + + // MARK: Not Floating + + func testNotFloating_noBackground() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_whiteBackground() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_roundedRectBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_bezelBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_lineBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + // MARK: Floating + + func testFloating_noBackground() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_whiteBackground() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_roundedRectBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_bezelBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_lineBorderStyle() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + // MARK: Right/Left Views Not Floating + + func testNotFloating_noBackground_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_whiteBackground_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_roundedRectBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_bezelBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_lineBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_noBackground_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_whiteBackground_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_roundedRectBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_bezelBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_lineBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_noBackground_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_whiteBackground_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_roundedRectBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_bezelBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testNotFloating_lineBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + // MARK: Right/Left Views Floating + + func testFloating_noBackground_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_whiteBackground_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_roundedRectBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_bezelBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_lineBorderStyle_rightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_noBackground_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_whiteBackground_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_roundedRectBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_bezelBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_lineBorderStyle_leftView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_noBackground_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_whiteBackground_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.backgroundColor = .white + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_roundedRectBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .roundedRect + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_bezelBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .bezel + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } + + func testFloating_lineBorderStyle_leftRightView() { + let textField: STPFloatingPlaceholderTextField = STPFloatingPlaceholderTextField() + textField.placeholder = "Test Placeholder" + textField.text = "Input Text" + textField.leftView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.leftViewMode = .always + textField.rightView = UIImageView(image: STPImageLibrary.unknownCardCardImage()) + textField.rightViewMode = .always + textField.borderStyle = .line + textField.sizeToFit() + STPSnapshotVerifyView(textField) + } +} diff --git a/Stripe/StripeiOSTests/STPFormEncoderTest.swift b/Stripe/StripeiOSTests/STPFormEncoderTest.swift new file mode 100644 index 00000000..b66e07e0 --- /dev/null +++ b/Stripe/StripeiOSTests/STPFormEncoderTest.swift @@ -0,0 +1,210 @@ +// +// STPFormEncoderTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 1/8/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPTestFormEncodableObject: NSObject, STPFormEncodable { + var additionalAPIParameters: [AnyHashable: Any] = [:] + + @objc var testProperty: String? + @objc var testIgnoredProperty: String? + @objc var testArrayProperty: [AnyHashable]? + @objc var testDictionaryProperty: [AnyHashable: Any]? + @objc var testNestedObjectProperty: STPTestFormEncodableObject? + + class func rootObjectName() -> String? { + return "test_object" + } + + class func propertyNamesToFormFieldNamesMapping() -> [String: String] { + return [ + "testProperty": "test_property", + "testArrayProperty": "test_array_property", + "testDictionaryProperty": "test_dictionary_property", + "testNestedObjectProperty": "test_nested_property", + ] + } +} + +class STPTestNilRootObjectFormEncodableObject: STPTestFormEncodableObject { + override class func rootObjectName() -> String? { + return nil + } +} + +class STPFormEncoderTest: XCTestCase { + // helper test method + func encode(_ object: STPTestFormEncodableObject?) -> String? { + let dictionary = STPFormEncoder.dictionary(forObject: object!) + return URLEncoder.queryString(from: dictionary) + } + + func testFormEncoding_emptyObject() { + let testObject = STPTestFormEncodableObject() + XCTAssertEqual(encode(testObject), "") + } + + func testFormEncoding_normalObject() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.testIgnoredProperty = "ignoreme" + XCTAssertEqual(encode(testObject), "test_object[test_property]=success") + } + + func testFormEncoding_additionalAttributes() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.additionalAPIParameters = [ + "foo": "bar", + "nested": [ + "nested_key": "nested_value" + ], + ] + XCTAssertEqual( + encode(testObject), + "test_object[foo]=bar&test_object[nested][nested_key]=nested_value&test_object[test_property]=success" + ) + } + + func testFormEncoding_arrayValue_empty() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.testArrayProperty = [] + XCTAssertEqual(encode(testObject), "test_object[test_property]=success") + } + + func testFormEncoding_arrayValue() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.testArrayProperty = [NSNumber(value: 1), NSNumber(value: 2), NSNumber(value: 3)] + XCTAssertEqual( + encode(testObject), + "test_object[test_array_property][0]=1&test_object[test_array_property][1]=2&test_object[test_array_property][2]=3&test_object[test_property]=success" + ) + } + + func testFormEncoding_BoolAndNumbers() { + let testObject = STPTestFormEncodableObject() + testObject.testArrayProperty = [ + NSNumber(value: 0), + NSNumber(value: 1), + NSNumber(value: false), + NSNumber(value: true), + NSNumber(value: true), + ] + XCTAssertEqual( + encode(testObject), + """ + test_object[test_array_property][0]=0\ + &test_object[test_array_property][1]=1\ + &test_object[test_array_property][2]=false\ + &test_object[test_array_property][3]=true\ + &test_object[test_array_property][4]=true + """ + ) + } + + func testFormEncoding_arrayOfEncodable() { + let testObject = STPTestFormEncodableObject() + + let inner1 = STPTestFormEncodableObject() + inner1.testProperty = "inner1" + let inner2 = STPTestFormEncodableObject() + inner2.testArrayProperty = ["inner2"] + + testObject.testArrayProperty = [inner1, inner2] + + XCTAssertEqual( + encode(testObject), + """ + test_object[test_array_property][0][test_property]=inner1\ + &test_object[test_array_property][1][test_array_property][0]=inner2 + """ + ) + } + + func testFormEncoding_dictionaryValue_empty() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.testDictionaryProperty = [:] + XCTAssertEqual(encode(testObject), "test_object[test_property]=success") + } + + func testFormEncoding_dictionaryValue() { + let testObject = STPTestFormEncodableObject() + testObject.testProperty = "success" + testObject.testDictionaryProperty = [ + "foo": "bar" + ] + XCTAssertEqual( + encode(testObject), + "test_object[test_dictionary_property][foo]=bar&test_object[test_property]=success" + ) + } + + func testFormEncoding_dictionaryOfEncodable() { + let testObject = STPTestFormEncodableObject() + + let inner1 = STPTestFormEncodableObject() + inner1.testProperty = "inner1" + let inner2 = STPTestFormEncodableObject() + inner2.testArrayProperty = ["inner2"] + + testObject.testDictionaryProperty = [ + "one": inner1, + "two": inner2, + ] + + XCTAssertEqual( + encode(testObject), + """ + test_object[test_dictionary_property][one][test_property]=inner1\ + &test_object[test_dictionary_property][two][test_array_property][0]=inner2 + """ + ) + } + + func testFormEncoding_setOfEncodable() { + let testObject = STPTestFormEncodableObject() + + let inner = STPTestFormEncodableObject() + inner.testProperty = "inner" + + testObject.testArrayProperty = [Set([inner])] + + XCTAssertEqual( + encode(testObject), + "test_object[test_array_property][0][test_property]=inner" + ) + } + + func testFormEncoding_nestedValue() { + let testObject1 = STPTestFormEncodableObject() + let testObject2 = STPTestFormEncodableObject() + testObject2.testProperty = "nested_object" + testObject1.testProperty = "success" + testObject1.testNestedObjectProperty = testObject2 + XCTAssertEqual( + encode(testObject1), + "test_object[test_nested_property][test_property]=nested_object&test_object[test_property]=success" + ) + } + + func testFormEncoding_nilRootObject() { + let testObject = STPTestNilRootObjectFormEncodableObject() + testObject.testProperty = "success" + XCTAssertEqual(encode(testObject), "test_property=success") + } +} diff --git a/Stripe/StripeiOSTests/STPFormTextFieldTest.swift b/Stripe/StripeiOSTests/STPFormTextFieldTest.swift new file mode 100644 index 00000000..83100b32 --- /dev/null +++ b/Stripe/StripeiOSTests/STPFormTextFieldTest.swift @@ -0,0 +1,62 @@ +// +// STPFormTextFieldTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 3/22/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPFormTextFieldTest: XCTestCase { + func testAutoFormattingBehavior_None() { + let sut = STPFormTextField() + sut.autoFormattingBehavior = .none + sut.text = "123456789" + XCTAssertEqual(sut.text, "123456789") + } + + func testAutoFormattingBehavior_PhoneNumbers() { + let sut = STPFormTextField() + sut.autoFormattingBehavior = .phoneNumbers + sut.text = "123456789" + XCTAssertEqual(sut.text, "(123) 456-789") + } + + func testAutoFormattingBehavior_CardNumbers() { + let sut = STPFormTextField() + sut.autoFormattingBehavior = .cardNumbers + sut.text = "4242424242424242" + XCTAssertEqual(sut.text, "4242424242424242") + var range = NSRange() + var value = sut.attributedText!.attribute(.kern, at: 0, effectiveRange: &range) as! Int + XCTAssertEqual(value, 0) + XCTAssertEqual(range.length, Int(3)) + value = sut.attributedText!.attribute(.kern, at: 3, effectiveRange: &range) as! Int + XCTAssertEqual(value, 5) + XCTAssertEqual(range.length, Int(1)) + value = sut.attributedText!.attribute(.kern, at: 4, effectiveRange: &range) as! Int + XCTAssertEqual(value, 0) + XCTAssertEqual(range.length, Int(3)) + value = sut.attributedText!.attribute(.kern, at: 7, effectiveRange: &range) as! Int + XCTAssertEqual(value, 5) + XCTAssertEqual(range.length, Int(1)) + value = sut.attributedText!.attribute(.kern, at: 8, effectiveRange: &range) as! Int + XCTAssertEqual(value, 0) + XCTAssertEqual(range.length, Int(3)) + value = sut.attributedText?.attribute(.kern, at: 11, effectiveRange: &range) as! Int + XCTAssertEqual(value, 5) + XCTAssertEqual(range.length, Int(1)) + value = sut.attributedText?.attribute(.kern, at: 12, effectiveRange: &range) as! Int + XCTAssertEqual(value, 0) + XCTAssertEqual(range.length, Int(4)) + XCTAssertEqual(sut.attributedText!.length, Int(16)) + + sut.placeholder = "enteracardnumber" + XCTAssertNil(sut.attributedPlaceholder!.attribute(.kern, at: 3, effectiveRange: &range)) + } +} diff --git a/Stripe/StripeiOSTests/STPFormViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPFormViewSnapshotTests.swift new file mode 100644 index 00000000..47973d4f --- /dev/null +++ b/Stripe/StripeiOSTests/STPFormViewSnapshotTests.swift @@ -0,0 +1,161 @@ +// +// STPFormViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/23/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPFormViewSnapshotTests: STPSnapshotTestCase { + + func testSingleInput() { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Single input" + let section = STPFormView.Section(rows: [[input]], title: nil, accessoryButton: nil) + let formView = STPFormView(sections: [section]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + + func testSingleInputPerRow() { + var rows = [[STPInputTextField]]() + for row in 0..<5 { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row)" + rows.append([input]) + } + let section = STPFormView.Section(rows: rows, title: nil, accessoryButton: nil) + let formView = STPFormView(sections: [section]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + + func testMultiInputPerRow() { + var rows = [[STPInputTextField]]() + for row in 0..<5 { + var rowInputs = [STPInputTextField]() + for c in ["A", "B", "C"] { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row) \(c)" + rowInputs.append(input) + } + + rows.append(rowInputs) + } + let section = STPFormView.Section(rows: rows, title: nil, accessoryButton: nil) + let formView = STPFormView(sections: [section]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + + func testMixSingleMultiInputPerRow() { + var rows = [[STPInputTextField]]() + for row in 0..<5 { + var rowInputs = [STPInputTextField]() + for c in ["A", "B", "C"] { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row) \(c)" + rowInputs.append(input) + if row % 2 == 0 { + break + } + } + + rows.append(rowInputs) + } + let section = STPFormView.Section(rows: rows, title: nil, accessoryButton: nil) + let formView = STPFormView(sections: [section]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + + func testSingleSectionWithTitle() { + var rows = [[STPInputTextField]]() + for row in 0..<5 { + var rowInputs = [STPInputTextField]() + for c in ["A", "B", "C"] { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row) \(c)" + rowInputs.append(input) + } + + rows.append(rowInputs) + } + let section = STPFormView.Section(rows: rows, title: "Single Section", accessoryButton: nil) + let formView = STPFormView(sections: [section]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + + func testMultiSection() { + var rows1 = [[STPInputTextField]]() + for row in 0..<5 { + var rowInputs = [STPInputTextField]() + for c in ["A", "B", "C"] { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row) \(c)" + rowInputs.append(input) + } + + rows1.append(rowInputs) + } + let section1 = STPFormView.Section( + rows: rows1, + title: "First Section", + accessoryButton: nil + ) + + var rows2 = [[STPInputTextField]]() + for row in 0..<5 { + var rowInputs = [STPInputTextField]() + for c in ["A", "B", "C"] { + let input = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + input.placeholder = "Row \(row) \(c)" + rowInputs.append(input) + } + + rows2.append(rowInputs) + } + let section2 = STPFormView.Section( + rows: rows2, + title: "Second Section", + accessoryButton: nil + ) + + let formView = STPFormView(sections: [section1, section2]) + formView.frame.size = formView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + STPSnapshotVerifyView(formView) + } + +} diff --git a/Stripe/StripeiOSTests/STPGenericInputPickerFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPGenericInputPickerFieldSnapshotTests.swift new file mode 100644 index 00000000..494e102a --- /dev/null +++ b/Stripe/StripeiOSTests/STPGenericInputPickerFieldSnapshotTests.swift @@ -0,0 +1,79 @@ +// +// STPGenericInputPickerFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Mel Ludowise on 2/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +final class STPGenericInputPickerFieldSnapshotTests: STPSnapshotTestCase { + + private var field: STPGenericInputPickerField! + + override func setUp() { + super.setUp() + + field = STPGenericInputPickerField(dataSource: MockDataSource()) + field.placeholder = "Placeholder" + field.sizeToFit() + field.frame.size.width = 200 + } + + func testEmptySelection() { + STPSnapshotVerifyView(field) + } + + func testWithDefaultSelection() { + // The 0th row should be auto-selected when tapping into the field + field.delegate?.textFieldDidBeginEditing?(field) + + STPSnapshotVerifyView(field) + } + + func testWithExplicitSelection() { + let index = 5 + + // Explicitly select a row + field.pickerView.selectRow(index, inComponent: 0, animated: false) + + // Because we're interacting with the picker programatically, we need to explicitly + // call `resignFirstResponder` to commit the changes. + _ = field.resignFirstResponder() + + STPSnapshotVerifyView(field) + } +} + +/// Simple DataSource that displays numbers 0–9 +private final class MockDataSource: STPGenericInputPickerFieldDataSource { + func numberOfRows() -> Int { + return 10 + } + + func inputPickerField( + _ pickerField: STPGenericInputPickerField, + titleForRow row: Int + ) + -> String? + { + return "\(row)" + } + + func inputPickerField( + _ pickerField: STPGenericInputPickerField, + inputValueForRow row: Int + ) + -> String? + { + return "\(row)" + } +} diff --git a/Stripe/StripeiOSTests/STPGenericInputPickerFieldValidatorTest.swift b/Stripe/StripeiOSTests/STPGenericInputPickerFieldValidatorTest.swift new file mode 100644 index 00000000..0a04d8b8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPGenericInputPickerFieldValidatorTest.swift @@ -0,0 +1,45 @@ +// +// STPGenericInputPickerFieldValidatorTest.swift +// StripeiOS Tests +// +// Created by Mel Ludowise on 2/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +final class STPGenericInputPickerFieldValidatorTest: XCTestCase { + + private var validator: STPGenericInputPickerField.Validator! + + override func setUp() { + super.setUp() + + validator = STPGenericInputPickerField.Validator() + } + + func testInitial() { + XCTAssertEqual(validator.validationState, .unknown) + } + + func testValidInput() { + validator.inputValue = "hello" + XCTAssertEqual(validator.validationState, .valid(message: nil)) + } + + func testEmptyInput() { + validator.inputValue = "" + XCTAssertEqual(validator.validationState, .incomplete(description: nil)) + } + + func testNilInput() { + validator.inputValue = nil + XCTAssertEqual(validator.validationState, .incomplete(description: nil)) + } +} diff --git a/Stripe/StripeiOSTests/STPGenericInputTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPGenericInputTextFieldSnapshotTests.swift new file mode 100644 index 00000000..fa7408a6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPGenericInputTextFieldSnapshotTests.swift @@ -0,0 +1,42 @@ +// +// STPGenericInputTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 12/2/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPGenericInputTextFieldSnapshotTests: STPSnapshotTestCase { + + override func setUp() { + super.setUp() + } + + func testEmpty() { + let field = STPGenericInputTextField(placeholder: "Empty") + field.sizeToFit() + field.frame.size.width = 200 + + STPSnapshotVerifyView(field) + } + + func testWithContent() { + let field = STPGenericInputTextField(placeholder: "Has Content") + field.sizeToFit() + field.frame.size.width = 200 + field.text = "Hello" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + +} diff --git a/Stripe/StripeiOSTests/STPImageLibraryTest.swift b/Stripe/StripeiOSTests/STPImageLibraryTest.swift new file mode 100644 index 00000000..b90d6e5d --- /dev/null +++ b/Stripe/StripeiOSTests/STPImageLibraryTest.swift @@ -0,0 +1,367 @@ +// +// STPImageLibraryTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 4/7/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class STPImageLibraryTestSwift: XCTestCase { + + static let cardBrands: [STPCardBrand] = [ + .amex, + .cartesBancaires, + .dinersClub, + .discover, + .JCB, + .mastercard, + .unionPay, + .unknown, + .visa, + ] + + func testCardIconMethods() { + STPAssertEqualImages( + STPImageLibrary.applePayCardImage(), + STPImageLibrary.safeImageNamed("stp_card_applepay", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.amexCardImage(), + STPImageLibrary.safeImageNamed("stp_card_amex", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.dinersClubCardImage(), + STPImageLibrary.safeImageNamed("stp_card_diners", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.discoverCardImage(), + STPImageLibrary.safeImageNamed("stp_card_discover", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.jcbCardImage(), + STPImageLibrary.safeImageNamed("stp_card_jcb", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.mastercardCardImage(), + STPImageLibrary.safeImageNamed("stp_card_mastercard", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.unionPayCardImage(), + STPImageLibrary.safeImageNamed("stp_card_unionpay", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.visaCardImage(), + STPImageLibrary.safeImageNamed("stp_card_visa", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.unknownCardCardImage(), + STPImageLibrary.safeImageNamed("stp_card_unknown", templateIfAvailable: false) + ) + } + + func testBrandImageForCardBrand() { + for brand in Self.cardBrands { + let image = STPImageLibrary.brandImage(for: brand, template: false) + + switch brand { + case .visa: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_visa", templateIfAvailable: false) + ) + case .amex: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_amex", templateIfAvailable: false) + ) + case .mastercard: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_mastercard", + templateIfAvailable: false + ) + ) + case .discover: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_discover", templateIfAvailable: false) + ) + case .JCB: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_jcb", templateIfAvailable: false) + ) + case .dinersClub: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_diners", templateIfAvailable: false) + ) + case .unionPay: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_unionpay", templateIfAvailable: false) + ) + case .cartesBancaires: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_cartes_bancaires", templateIfAvailable: false) + ) + case .unknown: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_unknown", templateIfAvailable: false) + ) + } + } + } + + func testTemplatedBrandImageForCardBrand() { + for brand in Self.cardBrands { + let image = STPImageLibrary.templatedBrandImage(for: brand) + + switch brand { + case .visa: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_visa_template", + templateIfAvailable: true + ) + ) + case .amex: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_amex_template", + templateIfAvailable: true + ) + ) + case .mastercard: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_mastercard_template", + templateIfAvailable: true + ) + ) + case .discover: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_discover_template", + templateIfAvailable: true + ) + ) + case .JCB: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_jcb_template", + templateIfAvailable: true + ) + ) + case .dinersClub: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_diners_template", + templateIfAvailable: true + ) + ) + case .unionPay: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_unionpay_template", + templateIfAvailable: true + ) + ) + case .cartesBancaires: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed( + "stp_card_cartes_bancaires_template", + templateIfAvailable: true + ) + ) + case .unknown: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_unknown", templateIfAvailable: true) + ) + } + } + } + + func testUnpaddedImageForCardBrands() { + for brand in STPCardBrand.allCases { + let image = STPImageLibrary.unpaddedCardBrandImage(for: brand) + // Assert image exists + XCTAssert(image.size != .zero) + } + } + + func testCVCImageForCardBrand() { + for brand in Self.cardBrands { + let image = STPImageLibrary.cvcImage(for: brand) + + switch brand { + case .amex: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_cvc_amex", templateIfAvailable: false) + ) + default: + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_cvc", templateIfAvailable: false) + ) + } + } + } + + func testErrorImageForCardBrand() { + for brand in Self.cardBrands { + let image = STPImageLibrary.errorImage(for: brand) + STPAssertEqualImages( + image, + STPImageLibrary.safeImageNamed("stp_card_error", templateIfAvailable: false) + ) + } + } + + func testMiscImages() { + STPAssertEqualImages( + STPLegacyImageLibrary.addIcon(), + STPLegacyImageLibrary.safeImageNamed("stp_icon_add", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPImageLibrary.bankIcon(), + STPImageLibrary.safeImageNamed("stp_icon_bank", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPLegacyImageLibrary.checkmarkIcon(), + STPLegacyImageLibrary.safeImageNamed("stp_icon_checkmark", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPLegacyImageLibrary.largeCardFrontImage(), + STPLegacyImageLibrary.safeImageNamed("stp_card_form_front", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPLegacyImageLibrary.largeCardBackImage(), + STPLegacyImageLibrary.safeImageNamed("stp_card_form_back", templateIfAvailable: false) + ) + STPAssertEqualImages( + STPLegacyImageLibrary.largeCardAmexCVCImage(), + STPLegacyImageLibrary.safeImageNamed( + "stp_card_form_amex_cvc", + templateIfAvailable: false + ) + ) + STPAssertEqualImages( + STPLegacyImageLibrary.largeShippingImage(), + STPLegacyImageLibrary.safeImageNamed("stp_shipping_form", templateIfAvailable: false) + ) + } + + func testFPXImages() { + // Probably better to make STPFPXBankBrand conform to CaseIterable, + // but let's not change behavior of a legacy product just for this test. + for i in 0...(STPFPXBankBrand.unknown.rawValue - 1) { + let brand = STPFPXBankBrand(rawValue: i)! + let bankIdentifier = STPFPXBank.identifierFrom(brand)! + let bankImageName = "stp_bank_fpx_" + bankIdentifier + STPAssertEqualImages( + STPLegacyImageLibrary.fpxBrandImage(for: brand), + STPLegacyImageLibrary.safeImageNamed(bankImageName, templateIfAvailable: false) + ) + + } + } + + func testBankIconCodeImagesExist() { + for iconCode in PaymentSheetImageLibrary.BankIconCodeRegexes.keys { + XCTAssertNotNil( + PaymentSheetImageLibrary.bankIcon(for: iconCode), + "Missing image for \(iconCode)" + ) + } + } + + func testBankNameToIconCode() { + // bank of america + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "bank of america"), "boa") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "BANK of AMERICA"), "boa") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "BANKof AMERICA"), "default") + + // capital one + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "capital one"), "capitalone") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Capital One"), "capitalone") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Capital One"), "default") + + // citibank + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "citibank"), "citibank") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Citibank"), "citibank") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Citi Bank"), "default") + + // compass + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "bbva"), "compass") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "BBVA"), "compass") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "compass"), "compass") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "b b v a"), "default") + + // morganchase + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Morgan Chase"), "morganchase") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "morgan chase"), "morganchase") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "jp morgan"), "morganchase") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "JP Morgan"), "morganchase") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Chase"), "morganchase") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "chase"), "morganchase") + + // pnc + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "pncbank"), "pnc") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "PNCBANK"), "pnc") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "pnc bank"), "pnc") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "PNC Bank"), "pnc") + + // suntrust + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "suntrust"), "suntrust") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "SUNTRUST"), "suntrust") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "suntrust bank"), "suntrust") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Suntrust Bank"), "suntrust") + + // svb + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Silicon Valley Bank"), "svb") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "SILICON VALLEY BANK"), "svb") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "SILICONVALLEYBANK"), "default") + + // usaa + XCTAssertEqual( + PaymentSheetImageLibrary.bankIconCode(for: "USAA Federal Savings Bank"), + "usaa" + ) + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "USAA Bank"), "usaa") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "USAA Savings Bank"), "default") + + // usbank + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "US Bank"), "usbank") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "U.S. Bank"), "usbank") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "u.s. Bank"), "usbank") + + // wellsfargo + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Wells Fargo"), "wellsfargo") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "WELLS FARGO"), "wellsfargo") + XCTAssertEqual(PaymentSheetImageLibrary.bankIconCode(for: "Well's Fargo"), "default") + } + +} diff --git a/Stripe/StripeiOSTests/STPInputTextFieldFormatterTests.swift b/Stripe/StripeiOSTests/STPInputTextFieldFormatterTests.swift new file mode 100644 index 00000000..8be567ca --- /dev/null +++ b/Stripe/StripeiOSTests/STPInputTextFieldFormatterTests.swift @@ -0,0 +1,81 @@ +// +// STPInputTextFieldFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPInputTextFieldFormatterTests: XCTestCase { + + func testAllowsDeletion() { + let formatter = STPInputTextFieldFormatter() + let textField = UITextField() + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 2), + replacementString: "" + ), + "Should allow deletion on empty" + ) + textField.text = "Hi" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 2), + replacementString: "" + ), + "Should allow full deletion" + ) + textField.text = "Hello" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 4, length: 1), + replacementString: "" + ), + "Should allow partial deletion at end" + ) + textField.text = "Hello" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 3, length: 1), + replacementString: "" + ), + "Should allow partial deletion in middle" + ) + textField.text = "Hello" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 1), + replacementString: "" + ), + "Should allow partial deletion at beginning" + ) + } + + func testAllowsInitialSpaceForAutofill() { + let formatter = STPInputTextFieldFormatter() + let textField = UITextField() + textField.textContentType = .nickname + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 0), + replacementString: " " + ) + ) + } + +} diff --git a/Stripe/StripeiOSTests/STPInputTextFieldValidatorTests.swift b/Stripe/StripeiOSTests/STPInputTextFieldValidatorTests.swift new file mode 100644 index 00000000..542ff6b3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPInputTextFieldValidatorTests.swift @@ -0,0 +1,64 @@ +// +// STPInputTextFieldValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPInputTextFieldValidatorTests: XCTestCase { + + class ObserverWithExpectation: NSObject, STPFormInputValidationObserver { + + let expectation: XCTestExpectation + init( + _ expectation: XCTestExpectation + ) { + self.expectation = expectation + super.init() + } + + func validationDidUpdate( + to state: STPValidatedInputState, + from previousState: STPValidatedInputState, + for unformattedInput: String?, + in input: STPFormInput + ) { + expectation.fulfill() + } + + } + + func testUpdatingObservers() { + let textField = STPInputTextField( + formatter: STPInputTextFieldFormatter(), + validator: STPInputTextFieldValidator() + ) + let expectationForNewValue = expectation(description: "Receives expectation with new value") + let observerForNewValue = ObserverWithExpectation(expectationForNewValue) + let validator = textField.validator + + validator.addObserver(observerForNewValue) + validator.validationState = STPValidatedInputState.valid(message: nil) + wait(for: [expectationForNewValue], timeout: 1) + validator.removeObserver(observerForNewValue) + + let expectationForSameValue = expectation( + description: "Receives expectation with same value" + ) + let observerForSameValue = ObserverWithExpectation(expectationForSameValue) + validator.validationState = .incomplete(description: nil) + validator.addObserver(observerForSameValue) + validator.validationState = .incomplete(description: nil) + wait(for: [expectationForSameValue], timeout: 1) + } + +} diff --git a/Stripe/StripeiOSTests/STPIntentActionAlipayHandleRedirectTest.swift b/Stripe/StripeiOSTests/STPIntentActionAlipayHandleRedirectTest.swift new file mode 100644 index 00000000..73a55ab7 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionAlipayHandleRedirectTest.swift @@ -0,0 +1,55 @@ +// +// STPIntentActionAlipayHandleRedirectTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 12/2/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPIntentActionAlipayHandleRedirectTest: XCTestCase { + func testMarlinReturnURL() throws { + let testJSONString = """ + { + "alipay_handle_redirect": { + "native_url": "alipay://alipayclient/?%7B%22dataString%22%3A%22_input_charset=utf-8%26app_pay=Y%26currency=USD%26forex_biz=FP%26notify_url=https%253A%252F%252Fhooks.stripe.com%252Falipay%252Falipay%252Fhook%252REDACTED%252Fsrc_REDACTED%26out_trade_no=src_REDACTED%26partner=REDACTED%26payment_type=1%26product_code=NEW_WAP_OVERSEAS_SELLER%26return_url=https%253A%252F%252Fhooks.stripe.com%252Fadapter%252Falipay%252Fredirect%252Fcomplete%252Fsrc_REDACTED%252Fsrc_client_secret_REDACTED%26secondary_merchant_id=acct_REDACTED%26secondary_merchant_industry=5734%26secondary_merchant_name=Yuki-Test%26sendFormat=normal%26service=create_forex_trade_wap%26sign=REDACTED%26sign_type=MD5%26subject=Yuki-Test%26supplier=Yuki-Test%26timeout_rule=20m%26total_fee=1.00%26bizcontext=%7B%5C%22av%5C%22%3A%5C%221.0%5C%22%2C%5C%22ty%5C%22%3A%5C%22ios_lite%5C%22%2C%5C%22appkey%5C%22%3A%5C%22123456789%5C%22%2C%5C%22sv%5C%22%3A%5C%22h.a.3.2.5%5C%22%2C%5C%22an%5C%22%3A%5C%22com.stripe.CustomSDKExample%5C%22%7D%22%2C%22fromAppUrlScheme%22%3A%22payments-example%22%2C%22requestType%22%3A%22SafePay%22%7D", + "return_url": "payments-example://safepay/", + "url": "https://hooks.stripe.com/redirect/authenticate/src_REDACTED?client_secret=src_client_secret_REDACTED" + }, + "type": "alipay_handle_redirect" + } + """ + guard + let testJSONData = testJSONString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject( + with: testJSONData, + options: .allowFragments + ) as? [AnyHashable: Any], + let nextAction = STPIntentAction.decodedObject(fromAPIResponse: json), + let alipayRedirect = nextAction.alipayHandleRedirect + else { + XCTFail() + return + } + XCTAssertEqual( + alipayRedirect.nativeURL, + URL( + string: + "alipay://alipayclient/?%7B%22dataString%22%3A%22_input_charset=utf-8%26app_pay=Y%26currency=USD%26forex_biz=FP%26notify_url=https%253A%252F%252Fhooks.stripe.com%252Falipay%252Falipay%252Fhook%252REDACTED%252Fsrc_REDACTED%26out_trade_no=src_REDACTED%26partner=REDACTED%26payment_type=1%26product_code=NEW_WAP_OVERSEAS_SELLER%26return_url=https%253A%252F%252Fhooks.stripe.com%252Fadapter%252Falipay%252Fredirect%252Fcomplete%252Fsrc_REDACTED%252Fsrc_client_secret_REDACTED%26secondary_merchant_id=acct_REDACTED%26secondary_merchant_industry=5734%26secondary_merchant_name=Yuki-Test%26sendFormat=normal%26service=create_forex_trade_wap%26sign=REDACTED%26sign_type=MD5%26subject=Yuki-Test%26supplier=Yuki-Test%26timeout_rule=20m%26total_fee=1.00%26bizcontext=%7B%5C%22av%5C%22%3A%5C%221.0%5C%22%2C%5C%22ty%5C%22%3A%5C%22ios_lite%5C%22%2C%5C%22appkey%5C%22%3A%5C%22123456789%5C%22%2C%5C%22sv%5C%22%3A%5C%22h.a.3.2.5%5C%22%2C%5C%22an%5C%22%3A%5C%22com.stripe.CustomSDKExample%5C%22%7D%22%2C%22fromAppUrlScheme%22%3A%22payments-example%22%2C%22requestType%22%3A%22SafePay%22%7D" + ) + ) + XCTAssertEqual(alipayRedirect.returnURL, URL(string: "payments-example://safepay/")) + XCTAssertEqual( + alipayRedirect.marlinReturnURL, + URL( + string: + "https://hooks.stripe.com/adapter/alipay/redirect/complete/src_REDACTED/src_client_secret_REDACTED" + ) + ) + } +} diff --git a/Stripe/StripeiOSTests/STPIntentActionMultibancoDisplayDetailsTest.swift b/Stripe/StripeiOSTests/STPIntentActionMultibancoDisplayDetailsTest.swift new file mode 100644 index 00000000..f526c3f2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionMultibancoDisplayDetailsTest.swift @@ -0,0 +1,44 @@ +// +// STPIntentActionMultibancoDisplayDetailsTest.swift +// StripeiOSTests +// +// Created by Nick Porter on 4/22/24. +// + +import Foundation + +class STPIntentActionMultibancoDisplayDetailsTest: XCTestCase { + func testActionDisplayDetails() throws { + let testJSONString = """ + { + "multibanco_display_details": { + "entity": "1234", + "expires_at": 1714405124, + "reference": "123456789", + "hosted_voucher_url": "https://payments.stripe.com/multibanco/voucher" + }, + "type": "multibanco_display_details" + } + """ + guard + let testJSONData = testJSONString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject( + with: testJSONData, + options: .allowFragments + ) as? [AnyHashable: Any], + let nextAction = STPIntentAction.decodedObject(fromAPIResponse: json), + let multibancoDisplayDetails = nextAction.multibancoDisplayDetails + else { + XCTFail() + return + } + + XCTAssertEqual(multibancoDisplayDetails.entity, "1234") + XCTAssertEqual(multibancoDisplayDetails.expiresAt.timeIntervalSince1970, 1714405124) + XCTAssertEqual(multibancoDisplayDetails.reference, "123456789") + XCTAssertEqual( + multibancoDisplayDetails.hostedVoucherURL, + URL(string: "https://payments.stripe.com/multibanco/voucher") + ) + } +} diff --git a/Stripe/StripeiOSTests/STPIntentActionPayNowDisplayQrCodeTest.swift b/Stripe/StripeiOSTests/STPIntentActionPayNowDisplayQrCodeTest.swift new file mode 100644 index 00000000..c0ee7df8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionPayNowDisplayQrCodeTest.swift @@ -0,0 +1,37 @@ +// +// STPIntentActionPayNowDisplayQrCodeTest.swift +// StripeiOSTests +// +// Created by Nick Porter on 9/11/23. +// + +@testable@_spi(STP) import Stripe + +class STPIntentActionPayNowDisplayQrCodeTest: XCTestCase { + func testActionHostedUrl() throws { + let testJSONString = """ + { + "paynow_display_qr_code": { + "hosted_instructions_url": "stripe.com/test/paynow/qr", + }, + "type": "paynow_display_qr_code" + } + """ + guard + let testJSONData = testJSONString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject( + with: testJSONData, + options: .allowFragments + ) as? [AnyHashable: Any], + let nextAction = STPIntentAction.decodedObject(fromAPIResponse: json), + let payNowDisplayQrCode = nextAction.payNowDisplayQrCode + else { + XCTFail() + return + } + XCTAssertEqual( + payNowDisplayQrCode.hostedInstructionsURL, + URL(string: "stripe.com/test/paynow/qr") + ) + } +} diff --git a/Stripe/StripeiOSTests/STPIntentActionPromptPayDisplayQrCodeTest.swift b/Stripe/StripeiOSTests/STPIntentActionPromptPayDisplayQrCodeTest.swift new file mode 100644 index 00000000..31d0272c --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionPromptPayDisplayQrCodeTest.swift @@ -0,0 +1,38 @@ +// +// STPIntentActionPromptPayDisplayQrCodeTest.swift +// StripeiOSTests +// +// Created by Nick Porter on 9/12/23. +// + +import Foundation +@testable@_spi(STP) import Stripe + +class STPIntentActionPromptPayDisplayQrCodeTest: XCTestCase { + func testActionHostedUrl() throws { + let testJSONString = """ + { + "promptpay_display_qr_code": { + "hosted_instructions_url": "stripe.com/test/promptpay/qr", + }, + "type": "promptpay_display_qr_code" + } + """ + guard + let testJSONData = testJSONString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject( + with: testJSONData, + options: .allowFragments + ) as? [AnyHashable: Any], + let nextAction = STPIntentAction.decodedObject(fromAPIResponse: json), + let promptPayDisplayQrCode = nextAction.promptPayDisplayQrCode + else { + XCTFail() + return + } + XCTAssertEqual( + promptPayDisplayQrCode.hostedInstructionsURL, + URL(string: "stripe.com/test/promptpay/qr") + ) + } +} diff --git a/Stripe/StripeiOSTests/STPIntentActionTest.swift b/Stripe/StripeiOSTests/STPIntentActionTest.swift new file mode 100644 index 00000000..4aa2b028 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionTest.swift @@ -0,0 +1,136 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPIntentActionTest.m +// StripeiOS Tests +// +// Created by Daniel Jackson on 11/7/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripePayments + +class STPIntentActionTest: XCTestCase { + func testDecodedObjectFromAPIResponseRedirectToURL() { + + let decode: (([AnyHashable: Any]?) -> STPIntentAction?) = { dict in + return .decodedObject(fromAPIResponse: dict) + } + + XCTAssertNil(decode(nil)) + XCTAssertNil(decode([:])) + XCTAssertNil( + decode([ + "redirect_to_url": [ + "url": "http://stripe.com" + ], + ]), + "fails without type") + + let missingDetails = decode( + [ + "type": "redirect_to_url" + ]) + XCTAssertNotNil(missingDetails) + XCTAssertEqual( + missingDetails!.type, + .unknown, + "Type becomes unknown if the redirect_to_url details are missing") + + let badURL = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "not a url" + ], + ]) + XCTAssertNotNil(badURL) + XCTAssertEqual( + badURL!.type, + .unknown, + "Type becomes unknown if the redirect_to_url details don't have a valid URL") + + let missingReturnURL = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "https://stripe.com/" + ], + ]) + XCTAssertNotNil(missingReturnURL) + XCTAssertEqual( + missingReturnURL!.type, + .redirectToURL, + "Missing return_url won't prevent it from decoding") + XCTAssertNotNil(missingReturnURL?.redirectToURL?.url) + XCTAssertEqual( + missingReturnURL?.redirectToURL?.url, + URL(string: "https://stripe.com/")) + XCTAssertNil(missingReturnURL?.redirectToURL?.returnURL) + + let badReturnURL = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "https://stripe.com/", + "return_url": "not a url", + ], + ]) + XCTAssertNotNil(badReturnURL) + XCTAssertEqual( + badReturnURL!.type, + .redirectToURL, + "invalid return_url won't prevent it from decoding") + XCTAssertNotNil(badReturnURL?.redirectToURL?.url) + XCTAssertEqual( + badReturnURL?.redirectToURL?.url, + URL(string: "https://stripe.com/")) + XCTAssertNil(badReturnURL?.redirectToURL?.returnURL) + + let complete = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "https://stripe.com/", + "return_url": "my-app://payment-complete", + ], + ]) + XCTAssertNotNil(complete) + XCTAssertEqual(complete?.type, .redirectToURL) + XCTAssertNotNil(complete?.redirectToURL?.url) + XCTAssertEqual( + complete?.redirectToURL?.url, + URL(string: "https://stripe.com/")) + XCTAssertNotNil(complete?.redirectToURL?.returnURL) + XCTAssertEqual( + complete?.redirectToURL?.returnURL, + URL(string: "my-app://payment-complete")) + XCTAssertFalse(complete!.redirectToURL!.followRedirects) + XCTAssertFalse(complete!.redirectToURL!.useWebAuthSession) + + let withFlags = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "https://stripe.com/redirect?useWebAuthSession=true&followRedirectsInSDK=true", + "return_url": "my-app://payment-complete", + ], + ]) + XCTAssertNotNil(withFlags) + XCTAssertEqual(withFlags?.type, .redirectToURL) + XCTAssertNotNil(withFlags?.redirectToURL?.url) + XCTAssertTrue(withFlags!.redirectToURL!.followRedirects) + XCTAssertTrue(withFlags!.redirectToURL!.useWebAuthSession) + + // Don't observe flags on non-Stripe URLs + let withNonStripeFlags = decode( + [ + "type": "redirect_to_url", + "redirect_to_url": [ + "url": "https://example.com/redirect?useWebAuthSession=true&followRedirectsInSDK=true", + "return_url": "my-app://payment-complete", + ], + ]) + XCTAssertFalse(withNonStripeFlags!.redirectToURL!.followRedirects) + XCTAssertFalse(withNonStripeFlags!.redirectToURL!.useWebAuthSession) + } +} diff --git a/Stripe/StripeiOSTests/STPIntentActionTypeTest.swift b/Stripe/StripeiOSTests/STPIntentActionTypeTest.swift new file mode 100644 index 00000000..e1879c45 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionTypeTest.swift @@ -0,0 +1,48 @@ +// +// STPIntentActionTypeTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 9/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPIntentActionTypeTest: XCTestCase { + + func testTypeFromString() { + XCTAssertEqual( + STPIntentActionType(string: "redirect_to_url"), + STPIntentActionType.redirectToURL + ) + XCTAssertEqual( + STPIntentActionType(string: "REDIRECT_TO_URL"), + STPIntentActionType.redirectToURL + ) + + XCTAssertEqual( + STPIntentActionType(string: "use_stripe_sdk"), + STPIntentActionType.useStripeSDK + ) + XCTAssertEqual( + STPIntentActionType(string: "USE_STRIPE_SDK"), + STPIntentActionType.useStripeSDK + ) + + XCTAssertEqual( + STPIntentActionType(string: "garbage"), + STPIntentActionType.unknown + ) + XCTAssertEqual( + STPIntentActionType(string: "GARBAGE"), + STPIntentActionType.unknown + ) + } + +} diff --git a/Stripe/StripeiOSTests/STPIntentActionWeChatPayRedirectToAppTest.swift b/Stripe/StripeiOSTests/STPIntentActionWeChatPayRedirectToAppTest.swift new file mode 100644 index 00000000..5e476ad1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPIntentActionWeChatPayRedirectToAppTest.swift @@ -0,0 +1,44 @@ +// +// STPIntentActionWeChatPayRedirectToAppTest.swift +// StripeiOS Tests +// +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPIntentActionWeChatPayRedirectToAppTest: XCTestCase { + func testActionNativeURL() throws { + let testJSONString = """ + { + "wechat_pay_redirect_to_ios_app": { + "native_url": "weixin://app/value:wx12345a1234b1234c/pay/?package=Sign=WXPay&appid=wx12345a1234b1234c&partnerid=123456789&prepayid=wx12345a1234b1234c&noncestr=12345×tamp=12345&sign=12341234", + }, + "type": "wechat_pay_redirect_to_ios_app" + } + """ + guard + let testJSONData = testJSONString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject( + with: testJSONData, + options: .allowFragments + ) as? [AnyHashable: Any], + let nextAction = STPIntentAction.decodedObject(fromAPIResponse: json), + let weChatPayRedirectToApp = nextAction.weChatPayRedirectToApp + else { + XCTFail() + return + } + XCTAssertEqual( + weChatPayRedirectToApp.nativeURL, + URL( + string: + "weixin://app/value:wx12345a1234b1234c/pay/?package=Sign=WXPay&appid=wx12345a1234b1234c&partnerid=123456789&prepayid=wx12345a1234b1234c&noncestr=12345×tamp=12345&sign=12341234" + ) + ) + } +} diff --git a/Stripe/StripeiOSTests/STPLabeledFormTextFieldViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPLabeledFormTextFieldViewSnapshotTests.swift new file mode 100644 index 00000000..5f8b56d3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPLabeledFormTextFieldViewSnapshotTests.swift @@ -0,0 +1,24 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPLabeledFormTextFieldViewSnapshotTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCaseCore +import StripeCoreTestUtils +@testable @_spi(STP) import StripePaymentsUI + +class STPLabeledFormTextFieldViewSnapshotTests: STPSnapshotTestCase { + func testAppearance() { + let formTextField = STPFormTextField() + formTextField.placeholder = "A placeholder" + formTextField.placeholderColor = UIColor.lightGray + let labeledFormField = STPLabeledFormTextFieldView(formLabel: "Test Label", textField: formTextField) + labeledFormField.formBackgroundColor = UIColor.white + labeledFormField.frame = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 44.0) + STPSnapshotVerifyView(labeledFormField, identifier: "STPLabeledFormTextFieldView.defaultAppearance") + } +} diff --git a/Stripe/StripeiOSTests/STPLabeledMultiFormTextFieldViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPLabeledMultiFormTextFieldViewSnapshotTests.swift new file mode 100644 index 00000000..e6116225 --- /dev/null +++ b/Stripe/StripeiOSTests/STPLabeledMultiFormTextFieldViewSnapshotTests.swift @@ -0,0 +1,41 @@ +// +// STPLabeledMultiFormTextFieldViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPLabeledMultiFormTextFieldViewSnapshotTests: STPSnapshotTestCase { + + func testAppearance() { + let formTextField1 = STPFormTextField() + formTextField1.placeholder = "Placeholder 1" + formTextField1.placeholderColor = UIColor.lightGray + + let formTextField2 = STPFormTextField() + formTextField2.placeholder = "Placeholder 2" + formTextField2.placeholderColor = UIColor.lightGray + + let labeledFormField = STPLabeledMultiFormTextFieldView( + formLabel: "Test Label", + firstTextField: formTextField1, + secondTextField: formTextField2 + ) + labeledFormField.formBackgroundColor = UIColor.white + labeledFormField.frame = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 62.0) + STPSnapshotVerifyView( + labeledFormField, + identifier: "STPLabeledMultiFormTextFieldView.defaultAppearance" + ) + } +} diff --git a/Stripe/StripeiOSTests/STPMandateCustomerAcceptanceParamsTest.swift b/Stripe/StripeiOSTests/STPMandateCustomerAcceptanceParamsTest.swift new file mode 100644 index 00000000..66f1e0d5 --- /dev/null +++ b/Stripe/StripeiOSTests/STPMandateCustomerAcceptanceParamsTest.swift @@ -0,0 +1,44 @@ +// +// STPMandateCustomerAcceptanceParamsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/18/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPMandateCustomerAcceptanceParamsTest: XCTestCase { + func testRootObjectName() { + XCTAssertEqual(STPMandateCustomerAcceptanceParams.rootObjectName(), "customer_acceptance") + } + + func testEncoding() { + let onlineParams = STPMandateOnlineParams(ipAddress: "", userAgent: "") + onlineParams.inferFromClient = NSNumber(value: true) + var params = STPMandateCustomerAcceptanceParams(type: .online, onlineParams: onlineParams)! + + var paramsAsDict = STPFormEncoder.dictionary(forObject: params) + var expected = [ + "customer_acceptance": [ + "type": "online", + "online": [ + "infer_from_client": NSNumber(value: true) + ], + ], + ] + XCTAssertEqual(paramsAsDict as NSDictionary, expected as NSDictionary) + + params = STPMandateCustomerAcceptanceParams(type: .offline, onlineParams: nil)! + paramsAsDict = STPFormEncoder.dictionary(forObject: params) + expected = [ + "customer_acceptance": [ + "type": "offline" + ], + ] + XCTAssertEqual(paramsAsDict as NSDictionary, expected as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPMandateDataParamsTest.swift b/Stripe/StripeiOSTests/STPMandateDataParamsTest.swift new file mode 100644 index 00000000..587d8f17 --- /dev/null +++ b/Stripe/StripeiOSTests/STPMandateDataParamsTest.swift @@ -0,0 +1,42 @@ +// +// STPMandateDataParamsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/18/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPMandateDataParamsTest: XCTestCase { + func testRootObjectName() { + XCTAssertEqual(STPMandateDataParams.rootObjectName(), "mandate_data") + } + + func testEncoding() { + let onlineParams = STPMandateOnlineParams(ipAddress: "", userAgent: "") + onlineParams.inferFromClient = NSNumber(value: true) + let customerAcceptanceParams = STPMandateCustomerAcceptanceParams( + type: .online, + onlineParams: onlineParams + )! + + let params = STPMandateDataParams(customerAcceptance: customerAcceptanceParams) + + let paramsAsDict = STPFormEncoder.dictionary(forObject: params) + let expected = [ + "mandate_data": [ + "customer_acceptance": [ + "type": "online", + "online": [ + "infer_from_client": true + ], + ], + ], + ] + XCTAssertEqual(paramsAsDict as NSDictionary, expected as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPMandateOnlineParamsTest.swift b/Stripe/StripeiOSTests/STPMandateOnlineParamsTest.swift new file mode 100644 index 00000000..990e02b2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPMandateOnlineParamsTest.swift @@ -0,0 +1,40 @@ +// +// STPMandateOnlineParamsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/18/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPMandateOnlineParamsTest: XCTestCase { + func testRootObjectName() { + XCTAssertEqual(STPMandateOnlineParams.rootObjectName(), "online") + } + + func testEncoding() { + var params = STPMandateOnlineParams(ipAddress: "test_ip_address", userAgent: "a_user_agent") + var paramsAsDict = STPFormEncoder.dictionary(forObject: params) + var expected: [String: AnyHashable] = [ + "online": [ + "ip_address": "test_ip_address", + "user_agent": "a_user_agent", + ], + ] + XCTAssertEqual(paramsAsDict as NSDictionary, expected as NSDictionary) + + params = STPMandateOnlineParams(ipAddress: "", userAgent: "") + params.inferFromClient = NSNumber(value: true) + paramsAsDict = STPFormEncoder.dictionary(forObject: params) + expected = [ + "online": [ + "infer_from_client": NSNumber(value: true) + ], + ] + XCTAssertEqual(paramsAsDict as NSDictionary, expected as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPMocks.h b/Stripe/StripeiOSTests/STPMocks.h new file mode 100644 index 00000000..1b782caa --- /dev/null +++ b/Stripe/StripeiOSTests/STPMocks.h @@ -0,0 +1,34 @@ +// +// STPMocks.h +// Stripe +// +// Created by Ben Guo on 4/5/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +#import +#import +@import Stripe; + +@interface STPMocks : NSObject + +/** + A stateless customer context that always retrieves the same customer object. + */ ++ (STPCustomerContext *)staticCustomerContext; + +/** + A static customer context that always retrieves the given customer and the given payment methods. + Selecting a default source and attaching a source have no effect. + */ ++ (STPCustomerContext *)staticCustomerContextWithCustomer:(STPCustomer *)customer paymentMethods:(NSArray *)paymentMethods; + +/** + A PaymentConfiguration object with a fake publishable key and a fake apple + merchant identifier that ignores the true value of [StripeAPI deviceSupportsApplePay] + and bases its `applePayEnabled` value solely on what is set + in `additionalPaymentOptions` + */ ++ (STPPaymentConfiguration *)paymentConfigurationWithApplePaySupportingDevice; + +@end diff --git a/Stripe/StripeiOSTests/STPMocks.m b/Stripe/StripeiOSTests/STPMocks.m new file mode 100644 index 00000000..63787637 --- /dev/null +++ b/Stripe/StripeiOSTests/STPMocks.m @@ -0,0 +1,55 @@ +// +// STPMocks.m +// Stripe +// +// Created by Ben Guo on 4/5/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +#import "STPMocks.h" + +@import StripePaymentsObjcTestUtils; +#import "StripeiOS_Tests-Swift.h" + +@interface STPPaymentConfiguration (STPMocks) + +/** + Mock apple pay enabled response to just be based on setting and not hardware + capability. + + `paymentConfigurationWithApplePaySupportingDevice` forwards calls to the + real method to this stub + */ +- (BOOL)stpmock_applePayEnabled; + +@end + +@implementation STPMocks + ++ (STPCustomerContext *)staticCustomerContext { + return [self staticCustomerContextWithCustomer:[STPFixtures customerWithSingleCardTokenSource] + paymentMethods:@[[STPFixtures paymentMethod]]]; +} + ++ (STPCustomerContext *)staticCustomerContextWithCustomer:(STPCustomer *)customer paymentMethods:(NSArray *)paymentMethods { + return [[Testing_StaticCustomerContext_Objc alloc] initWithCustomer:customer paymentMethods:paymentMethods]; +} + ++ (STPPaymentConfiguration *)paymentConfigurationWithApplePaySupportingDevice { + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + config.appleMerchantIdentifier = @"fake_apple_merchant_id"; + id partialMock = OCMPartialMock(config); + OCMStub([partialMock applePayEnabled]).andCall(partialMock, @selector(stpmock_applePayEnabled)); + return partialMock; +} + +@end + +@implementation STPPaymentConfiguration (STPMocks) + +- (BOOL)stpmock_applePayEnabled { + return self.applePayEnabled; +} + +@end + diff --git a/Stripe/StripeiOSTests/STPNumericDigitInputTextFormatterTests.swift b/Stripe/StripeiOSTests/STPNumericDigitInputTextFormatterTests.swift new file mode 100644 index 00000000..ea41c2f3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPNumericDigitInputTextFormatterTests.swift @@ -0,0 +1,157 @@ +// +// STPNumericDigitInputTextFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPNumericDigitInputTextFormatterTests: XCTestCase { + + func testDisallowsNonDigits() { + let formatter = STPNumericDigitInputTextFormatter() + XCTAssertFalse( + formatter.isAllowedInput("a", to: "", at: NSRange(location: 0, length: 1)), + "Shouldn't allow non-digit in empty string" + ) + XCTAssertFalse( + formatter.isAllowedInput("1a", to: "", at: NSRange(location: 0, length: 1)), + "Shouldn't allow digit + non-digit in empty string" + ) + XCTAssertFalse( + formatter.isAllowedInput("a", to: "1", at: NSRange(location: 0, length: 1)), + "Shouldn't allow non-digit in digit string" + ) + XCTAssertFalse( + formatter.isAllowedInput("1a", to: "1", at: NSRange(location: 0, length: 1)), + "Shouldn't allow digit + non-digit in digit string" + ) + XCTAssertFalse( + formatter.isAllowedInput(" ", to: "1", at: NSRange(location: 0, length: 1)), + "Shouldn't allow spaces" + ) + // for now we only validate the input, not the result + XCTAssertTrue( + formatter.isAllowedInput("1", to: "a", at: NSRange(location: 0, length: 1)), + "Should allow digit added to non-digit string" + ) + } + + func testAllowsDigits() { + let formatter = STPNumericDigitInputTextFormatter() + XCTAssertTrue( + formatter.isAllowedInput("1", to: "", at: NSRange(location: 0, length: 1)), + "Should allow digit in empty string" + ) + XCTAssertTrue( + formatter.isAllowedInput("2", to: "1", at: NSRange(location: 0, length: 1)), + "Should allow digit insert at beginning of string" + ) + XCTAssertTrue( + formatter.isAllowedInput("3", to: "1", at: NSRange(location: 1, length: 1)), + "Should allow digit insert at end of string" + ) + XCTAssertTrue( + formatter.isAllowedInput("45", to: "1", at: NSRange(location: 0, length: 1)), + "Should allow multi-digit insert" + ) + } + + func testFormattingCharacterSet() { + let formatter = STPNumericDigitInputTextFormatter( + allowedFormattingCharacterSet: CharacterSet(charactersIn: "xy") + ) + XCTAssertTrue( + formatter.isAllowedInput("x", to: "", at: NSRange(location: 0, length: 1)), + "Should allow formatting character in empty string" + ) + XCTAssertFalse( + formatter.isAllowedInput("xa", to: "", at: NSRange(location: 0, length: 1)), + "Shouldn't allow formatting + non-formatting in empty string" + ) + XCTAssertTrue( + formatter.isAllowedInput("x", to: "1", at: NSRange(location: 0, length: 1)), + "Should allow formatting character in digit string" + ) + XCTAssertTrue( + formatter.isAllowedInput("1x", to: "1", at: NSRange(location: 0, length: 1)), + "Should allow digit + formatting in digit string" + ) + XCTAssertTrue( + formatter.isAllowedInput("xxxxyyy", to: "1", at: NSRange(location: 0, length: 6)), + "Should allow multiple formatting in digit string" + ) + } + + // MARK: - Inherited Tests + func testAllowsDeletion() { + let formatter = STPNumericDigitInputTextFormatter() + let textField = UITextField() + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 2), + replacementString: "" + ), + "Should allow deletion on empty" + ) + textField.text = "12" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 2), + replacementString: "" + ), + "Should allow full deletion" + ) + textField.text = "12345" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 4, length: 1), + replacementString: "" + ), + "Should allow partial deletion at end" + ) + textField.text = "12345" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 3, length: 1), + replacementString: "" + ), + "Should allow partial deletion in middle" + ) + textField.text = "12345" + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 1), + replacementString: "" + ), + "Should allow partial deletion at beginning" + ) + } + + func testAllowsInitialSpaceForAutofill() { + let formatter = STPNumericDigitInputTextFormatter() + let textField = UITextField() + textField.textContentType = .nickname + XCTAssertTrue( + formatter.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 0), + replacementString: " " + ) + ) + } + +} diff --git a/Stripe/StripeiOSTests/STPNumericStringValidatorTests.swift b/Stripe/StripeiOSTests/STPNumericStringValidatorTests.swift new file mode 100644 index 00000000..8f5cdccd --- /dev/null +++ b/Stripe/StripeiOSTests/STPNumericStringValidatorTests.swift @@ -0,0 +1,49 @@ +// +// STPNumericStringValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPNumericStringValidatorTests: XCTestCase { + func testNumberSanitization() { + let tests = [ + ["4242424242424242", "4242424242424242"], + ["XXXXXX", ""], + ["424242424242424X", "424242424242424"], + ["X4242", "4242"], + ["4242 4242 4242 4242", "4242424242424242"], + ["123-456-", "123456"], + ] + for test in tests { + XCTAssertEqual(STPNumericStringValidator.sanitizedNumericString(for: test[0]), test[1]) + } + } + + func testIsStringNumeric() { + let tests = [ + ["4242424242424242", NSNumber(value: true)], + ["XXXXXX", NSNumber(value: false)], + ["424242424242424X", NSNumber(value: false)], + ["X4242", NSNumber(value: false)], + ["4242 4242 4242 4242", NSNumber(value: false)], + ["123-456-", NSNumber(value: false)], + [" 1", NSNumber(value: false)], + ["", NSNumber(value: true)], + ] + for test in tests { + let first = STPNumericStringValidator.isStringNumeric(test[0] as! String) + let second = (test[1] as! NSNumber).boolValue + XCTAssertEqual(first, second) + } + } +} diff --git a/Stripe/StripeiOSTests/STPPIIFunctionalTest.swift b/Stripe/StripeiOSTests/STPPIIFunctionalTest.swift new file mode 100644 index 00000000..e8d6a54d --- /dev/null +++ b/Stripe/StripeiOSTests/STPPIIFunctionalTest.swift @@ -0,0 +1,46 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPIIFunctionalTest.m +// Stripe +// +// Created by Charles Scalesse on 1/8/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPIIFunctionalTest: STPNetworkStubbingTestCase { + func testCreatePersonallyIdentifiableInformationToken() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "PII creation") + + client.createToken(withPersonalIDNumber: "0123456789") { token, error in + expectation.fulfill() + XCTAssertNil(error, "error should be nil \(String(describing: error?.localizedDescription))") + XCTAssertNotNil(token, "token should not be nil") + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .PII) + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testSSNLast4Token() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let expectation = self.expectation(description: "PII creation") + + client.createToken(withSSNLast4: "1234") { token, error in + expectation.fulfill() + XCTAssertNil(error, "error should be nil \(String(describing: error?.localizedDescription))") + XCTAssertNotNil(token, "token should not be nil") + XCTAssertNotNil(token?.tokenId) + XCTAssertEqual(token?.type, .PII) + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentCardTextFieldKVOTest.m b/Stripe/StripeiOSTests/STPPaymentCardTextFieldKVOTest.m new file mode 100644 index 00000000..b7a65d02 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentCardTextFieldKVOTest.m @@ -0,0 +1,83 @@ +// +// STPPaymentCardTextFieldKVOTest.m +// Stripe +// +// Created by Jack Flintermann on 8/26/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +@import UIKit; +@import XCTest; +@import OCMock; +@import StripeCoreTestUtils; +@import StripePaymentsObjcTestUtils; + +@interface STPPaymentCardTextField (Testing) +@property (nonatomic, readwrite, weak) UIImageView *brandImageView; +@property (nonatomic, readwrite, weak) STPFormTextField *numberField; +@property (nonatomic, readwrite, weak) STPFormTextField *expirationField; +@property (nonatomic, readwrite, weak) STPFormTextField *cvcField; +@property (nonatomic, readwrite, weak) STPFormTextField *postalCodeField; +@property (nonatomic, readonly, weak) STPFormTextField *currentFirstResponderField; +@property (nonatomic, copy) NSNumber *focusedTextFieldForLayout; ++ (UIImage *)cvcImageForCardBrand:(STPCardBrand)cardBrand; ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)cardBrand; +@end + +@interface STPPaymentCardTextFieldKVOUITests : XCTestCase +@property (nonatomic) UIWindow *window; +@property (nonatomic) STPPaymentCardTextField *sut; +@end + +@implementation STPPaymentCardTextFieldKVOUITests + ++ (void)setUp { + [super setUp]; + [[STPAPIClient sharedClient] setPublishableKey:STPTestingDefaultPublishableKey]; +} + +- (void)setUp { + [super setUp]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + STPPaymentCardTextField *textField = [[STPPaymentCardTextField alloc] initWithFrame:self.window.bounds]; + [self.window addSubview:textField]; + XCTAssertTrue([textField.numberField canBecomeFirstResponder], @"text field cannot become first responder"); + self.sut = textField; +} + +- (void)testIsValidKVO { + id observer = OCMClassMock([UIViewController class]); + self.sut.numberField.text = @"4242424242424242"; + self.sut.expirationField.text = @"10/50"; + self.sut.postalCodeField.text = @"90210"; + XCTAssertFalse(self.sut.isValid); + + NSString *expectedKeyPath = @"sut.isValid"; + [self addObserver:observer forKeyPath:expectedKeyPath options:NSKeyValueObservingOptionNew context:nil]; + XCTestExpectation *exp = [self expectationWithDescription:@"observeValue"]; + OCMStub([observer observeValueForKeyPath:[OCMArg any] ofObject:[OCMArg any] change:[OCMArg any] context:nil]) + .andDo(^(NSInvocation *invocation) { + NSString *keyPath; + NSDictionary *change; + [invocation getArgument:&keyPath atIndex:2]; + [invocation getArgument:&change atIndex:4]; + if ([keyPath isEqualToString:expectedKeyPath]) { + if ([change[@"new"] boolValue]) { + [exp fulfill]; + [self removeObserver:observer forKeyPath:@"sut.isValid"]; + } + } + }); + + self.sut.cvcField.text = @"123"; + + [self waitForExpectationsWithTimeout:TestConstants.STPTestingNetworkRequestTimeout handler:nil]; +} + +- (void)testPaymentCardTextFieldCanSetPreferredBrands { + STPPaymentCardTextField *textField = [[STPPaymentCardTextField alloc] initWithFrame:self.window.bounds]; + [textField setPreferredNetworks:@[[NSNumber numberWithInt:STPCardBrandVisa]]]; + XCTAssertEqual([[[textField preferredNetworks] firstObject] intValue], STPCardBrandVisa); +} + +@end diff --git a/Stripe/StripeiOSTests/STPPaymentCardTextFieldTest.swift b/Stripe/StripeiOSTests/STPPaymentCardTextFieldTest.swift new file mode 100644 index 00000000..8b717899 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentCardTextFieldTest.swift @@ -0,0 +1,1291 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentCardTextFieldTest.m +// Stripe +// +// Created by Jack Flintermann on 8/26/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +@testable @_spi(STP) import StripePayments +@testable @_spi(STP) import StripePaymentsUI +import UIKit +import XCTest + +/// Class that implements STPPaymentCardTextFieldDelegate and uses a block for each delegate method. +class PaymentCardTextFieldBlockDelegate: NSObject, STPPaymentCardTextFieldDelegate { + var didChange: ((STPPaymentCardTextField) -> Void)? + var willEndEditingForReturn: ((STPPaymentCardTextField) -> Void)? + var didEndEditing: ((STPPaymentCardTextField) -> Void)? + // add more properties for other delegate methods as this test needs them + + func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { + if let didChange { + didChange(textField) + } + } + + func paymentCardTextFieldWillEndEditing(forReturn textField: STPPaymentCardTextField) { + if let willEndEditingForReturn { + willEndEditingForReturn(textField) + } + } + + func paymentCardTextFieldDidEndEditing(_ textField: STPPaymentCardTextField) { + if let didEndEditing { + didEndEditing(textField) + } + } +} + +class STPPaymentCardTextFieldTest: XCTestCase { + override class func setUp() { + super.setUp() + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + } + + func testIntrinsicContentSize() { + let textField = STPPaymentCardTextField() + + let iOS8SystemFont = UIFont(name: "HelveticaNeue", size: 18) + textField.font = iOS8SystemFont! + XCTAssertEqual(textField.intrinsicContentSize.height, 44, accuracy: 0.1) + XCTAssertEqual(textField.intrinsicContentSize.width, 241, accuracy: 0.1) + + let iOS9SystemFont = UIFont.systemFont(ofSize: 18) + textField.font = iOS9SystemFont + XCTAssertEqual(textField.intrinsicContentSize.height, 44, accuracy: 0.1) + XCTAssertEqual(textField.intrinsicContentSize.width, 253, accuracy: 1.0) + + textField.font = UIFont(name: "Avenir", size: 44)! + XCTAssertEqual(textField.intrinsicContentSize.height, 62, accuracy: 0.1) + XCTAssertEqual(textField.intrinsicContentSize.width, 472, accuracy: 0.1) + } + + func testSetCard_numberUnknown() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "1" + card.number = number + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .unknown)!.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, 0) + XCTAssertEqual(sut.cvcField.text!.count, 0) + XCTAssertNil(sut.currentFirstResponderField()) + } + + func testSetCard_expiration() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData! == imgData) + } + XCTAssertEqual(sut.numberField.text?.count, Int(0)) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text?.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_CVC() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let cvc = "123" + card.cvc = cvc + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_updatesCVCValidity() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "378282246310005" + sut.cvcField.text = "1234" + sut.expirationField.text = "10/50" + XCTAssertTrue(sut.cvcField.validText) + sut.numberField.text = "4242424242424242" + XCTAssertFalse(sut.cvcField.validText) + } + + func testSetCard_numberVisa() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "424242" + card.number = number + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertNotNil(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.placeholder, "CVC") + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberVisaInvalid() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242111111111111" + card.number = number + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .visa)!.pngData() + + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + } + + func testSetCard_withCBCInfo() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "424242" + card.number = number + card.networks = .init(preferred: "visa") + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + XCTAssertEqual(sut.paymentMethodParams.card!.networks!.preferred, "visa") + } + + func testSetCard_numberAmex() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282" + card.number = number + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .amex)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.placeholder, "CVV") + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberAmexInvalid() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282246311111" + card.number = number + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .amex)!.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + } + + func testSetCard_numberAndExpiration() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_partialNumberAndExpiration() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "424242" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberAndCVC() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282246310005" + let cvc = "123" + card.number = number + card.cvc = cvc + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .amex)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_expirationAndCVC() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let cvc = "123" + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_completeCardCountryWithoutPostal() { + let sut = STPPaymentCardTextField() + sut.countryCode = "BZ" + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertTrue(sut.isValid) + } + + func testSetCard_completeCardNoPostal() { + let sut = STPPaymentCardTextField() + sut.postalCodeEntryEnabled = false + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertTrue(sut.isValid) + } + + func testSetCard_completeCard() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = STPPaymentMethodAddress() + billingDetails.address!.postalCode = "90210" + let params = STPPaymentMethodParams(card: card, billingDetails: billingDetails, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertTrue(sut.isValid) + } + + func testSetCard_empty() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + let card = STPPaymentMethodCardParams() + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSettingTextUpdatesViewModelText() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + XCTAssertEqual(sut.viewModel.cardNumber, sut.numberField.text) + + sut.cvcField.text = "123" + XCTAssertEqual(sut.viewModel.cvc, sut.cvcField.text) + + sut.expirationField.text = "10/50" + XCTAssertEqual(sut.viewModel.rawExpiration, sut.expirationField.text) + XCTAssertEqual(sut.viewModel.expirationMonth, "10") + XCTAssertEqual(sut.viewModel.expirationYear, "50") + } + + func testSettingTextUpdatesCardParams() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + sut.postalCodeField.text = "90210" + + let card = sut.paymentMethodParams.card + XCTAssertNotNil(card) + XCTAssertEqual(card?.number, "4242424242424242") + XCTAssertEqual(card?.cvc, "123") + XCTAssertEqual(card?.expMonth?.intValue ?? 0, 10) + XCTAssertEqual(card?.expYear?.intValue ?? 0, 50) + XCTAssertEqual(sut.paymentMethodParams.billingDetails!.address!.postalCode, "90210") + } + + func testSettingBillingDetailsRetainsBillingDetails() { + let sut = STPPaymentCardTextField() + let params = STPPaymentMethodCardParams() + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Test test" + + sut.paymentMethodParams = STPPaymentMethodParams(card: params, billingDetails: billingDetails, metadata: nil) + let actual = sut.paymentMethodParams + + XCTAssertEqual("Test test", actual.billingDetails!.name) + } + + func testSettingMetadataRetainsMetadata() { + let sut = STPPaymentCardTextField() + let params = STPPaymentMethodCardParams() + sut.paymentMethodParams = STPPaymentMethodParams(card: params, billingDetails: nil, metadata: [ + "hello": "test", + ]) + let actual = sut.paymentMethodParams + + XCTAssertEqual([ + "hello": "test", + ], actual.metadata) + } + + func testSettingPostalCodeUpdatesCardParams() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + sut.postalCodeField.text = "90210" + + let params = sut.paymentMethodParams.card + XCTAssertNotNil(params) + XCTAssertEqual(params?.number, "4242424242424242") + XCTAssertEqual(params?.cvc, "123") + XCTAssertEqual(params?.expMonth?.intValue ?? 0, 10) + XCTAssertEqual(params?.expYear?.intValue ?? 0, 50) + } + + func testEmptyPostalCodeVendsNilAddress() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + + XCTAssertNil(sut.paymentMethodParams.billingDetails?.address?.postalCode) + let params = sut.paymentMethodParams.card + XCTAssertNotNil(params) + XCTAssertEqual(params?.number, "4242424242424242") + XCTAssertEqual(params?.cvc, "123") + XCTAssertEqual(params?.expMonth?.intValue ?? 0, 10) + XCTAssertEqual(params?.expYear?.intValue ?? 0, 50) + } + + func testAccessingCardParamsDuringSettingCardParams() { + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.didChange = { textField in + // delegate reads the `cardParams` for any reason it wants + _ = textField.paymentMethodParams.card + } + let sut = STPPaymentCardTextField() + sut.delegate = delegate + + let params = STPPaymentMethodCardParams() + params.number = "4242424242424242" + params.cvc = "123" + + sut.paymentMethodParams = STPPaymentMethodParams(card: params, billingDetails: nil, metadata: nil) + let actual = sut.paymentMethodParams.card + + XCTAssertEqual("4242424242424242", actual!.number) + XCTAssertEqual("123", actual!.cvc) + } + + func testSetCardParamsCopiesObject() { + let sut = STPPaymentCardTextField() + let params = STPPaymentMethodCardParams() + + params.number = "4242424242424242" // legit + sut.paymentMethodParams = STPPaymentMethodParams(card: params, billingDetails: nil, metadata: nil) + + // fetching `sut.cardParams` returns a copy, so edits happen to caller's copy + sut.paymentMethodParams.card!.number = "number 1" + + // `sut` copied `params` (& `params.address`) when set, so edits to original don't show up + params.number = "number 2" + + XCTAssertEqual("4242424242424242", sut.paymentMethodParams.card!.number) + + XCTAssertNotEqual("number 1", sut.paymentMethodParams.card!.number, "return value from cardParams cannot be edited inline") + + XCTAssertNotEqual("number 2", sut.paymentMethodParams.card!.number, "caller changed their copy after setCardParams:") + } + + // MARK: - paymentMethodParams + + func testSetCard_numberUnknown_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "1" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .unknown)!.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + } + + func testSetCard_expiration_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_CVC_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let cvc = "123" + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberVisa_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "424242" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.placeholder, "CVC") + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberVisaInvalid_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242111111111111" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .visa)!.pngData() + + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + } + + func testSetCard_numberAmex_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .amex)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.placeholder, "CVV") + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberAmexInvalid_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282246311111" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.errorImage(for: .amex)!.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + } + + func testSetCard_numberAndExpiration_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_partialNumberAndExpiration_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "424242" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_numberAndCVC_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "378282246310005" + let cvc = "123" + card.number = number + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .amex)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_expirationAndCVC_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let cvc = "123" + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testSetCard_completeCardCountryWithoutPostal_pm() { + let sut = STPPaymentCardTextField() + sut.countryCode = "BZ" + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertTrue(sut.isValid) + } + + func testSetCard_completeCardNoPostal_pm() { + let sut = STPPaymentCardTextField() + sut.postalCodeEntryEnabled = false + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertTrue(sut.isValid) + } + + func testSetCard_completeCard_pm() { + let sut = STPPaymentCardTextField() + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: STPPaymentMethodBillingDetails(postalCode: "90210", countryCode: "US"), metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertNil(sut.currentFirstResponderField()) + let isvalid = sut.isValid + XCTAssertTrue(isvalid) + + let paymentMethodParams = sut.paymentMethodParams + XCTAssertNotNil(paymentMethodParams) + + let sutCardParams = paymentMethodParams.card + XCTAssertNotNil(sutCardParams) + + XCTAssertEqual(sutCardParams?.number, card.number) + XCTAssertEqual(sutCardParams?.expMonth, card.expMonth) + XCTAssertEqual(sutCardParams?.expYear, card.expYear) + XCTAssertEqual(sutCardParams?.cvc, card.cvc) + + let sutBillingDetails = paymentMethodParams.billingDetails + XCTAssertNotNil(sutBillingDetails) + + let sutAddress = sutBillingDetails?.address + XCTAssertNotNil(sutAddress) + + XCTAssertEqual(sutAddress?.postalCode, "90210") + XCTAssertEqual(sutAddress?.country, "US") + } + + func testSetCard_empty_pm() { + let sut = STPPaymentCardTextField() + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + let card = STPPaymentMethodCardParams() + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertNil(sut.currentFirstResponderField()) + XCTAssertFalse(sut.isValid) + } + + func testUsesPreferredNetworks() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let sut = STPPaymentCardTextField() + sut.cbcEnabledOverride = true + sut.preferredNetworks = [.visa] + let card = STPPaymentMethodCardParams() + card.number = "4973019750239993" + card.expMonth = 12 + card.expYear = 43 + card.cvc = "123" + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + XCTAssertEqual(sut.viewModel.cbcController.selectedBrand, .visa) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } + + func testOBOCBC() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let sut = STPPaymentCardTextField() + sut.onBehalfOf = "acct_abc123" + XCTAssertEqual(sut.viewModel.cbcController.onBehalfOf, "acct_abc123") + } + + func testFourDigitCVCNotAllowedUnknownCBCCard() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let sut = STPPaymentCardTextField() + sut.cbcEnabledOverride = true + sut.preferredNetworks = [.visa] + let card = STPPaymentMethodCardParams() + card.number = "4973019750239993" + card.expMonth = 12 + card.expYear = 43 + card.cvc = "1234" + let params = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + sut.paymentMethodParams = params + XCTAssertFalse(sut.isValid) + } +} + +// N.B. It is eexpected for setting the card params to generate API response errors +// because we are calling to the card metadata service without configuration STPAPIClient +class STPPaymentCardTextFieldUITests: XCTestCase { + var window: UIWindow! + var sut: STPPaymentCardTextField! + + override class func setUp() { + super.setUp() + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + } + + override func setUp() { + super.setUp() + window = UIWindow(frame: UIScreen.main.bounds) + let textField = STPPaymentCardTextField(frame: window.bounds) + window?.addSubview(textField) + XCTAssertTrue(textField.numberField.canBecomeFirstResponder, "text field cannot become first responder") + sut = textField + } + + // MARK: - UI Tests + + func testSetCard_allFields_whileEditingNumber() { + XCTAssertTrue(sut.numberField.becomeFirstResponder(), "text field is not first responder") + let card = STPPaymentMethodCardParams() + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = STPPaymentMethodAddress() + billingDetails.address!.postalCode = "90210" + let number = "4242424242424242" + let cvc = "123" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + card.cvc = cvc + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: billingDetails, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .visa)?.pngData() + + XCTAssertNil(sut.focusedTextFieldForLayout) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text, cvc) + XCTAssertEqual(sut.postalCode, "90210") + XCTAssertFalse(sut.isFirstResponder, "after `setCardParams:`, if all fields are valid, should resign firstResponder") + XCTAssertTrue(sut.isValid) + } + + func testSetCard_partialNumberAndExpiration_whileEditingExpiration() { + XCTAssertTrue(sut.expirationField.becomeFirstResponder(), "text field is not first responder") + let card = STPPaymentMethodCardParams() + let number = "42" + card.number = number + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 50) + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.cvcImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.CVC.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text, "10/50") + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertTrue(sut.cvcField.isFirstResponder, "after `setCardParams:`, when firstResponder becomes valid, first invalid field should become firstResponder") + XCTAssertFalse(sut.isValid) + } + + func testSetCard_number_whileEditingCVC() { + XCTAssertTrue(sut.cvcField.becomeFirstResponder(), "text field is not first responder") + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.cvcImage(for: .visa)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.CVC.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData == imgData) + } + XCTAssertEqual(sut.numberField.text, number) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertTrue(sut.cvcField.isFirstResponder, "after `setCardParams:`, if firstResponder is invalid, it should remain firstResponder") + XCTAssertFalse(sut.isValid) + } + + func testSetCard_empty_whileEditingNumber() { + sut.numberField.text = "4242424242424242" + sut.cvcField.text = "123" + sut.expirationField.text = "10/50" + XCTAssertTrue(sut.numberField.becomeFirstResponder(), "text field is not first responder") + let card = STPPaymentMethodCardParams() + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + + let imgData = sut.brandImageView.image?.pngData() + let expectedImgData = STPPaymentCardTextField.brandImage(for: .unknown)?.pngData() + + XCTAssertNotNil(sut.focusedTextFieldForLayout) + XCTAssertTrue(sut.focusedTextFieldForLayout?.intValue ?? 0 == STPCardFieldType.number.rawValue) + if let imgData { + XCTAssertTrue(expectedImgData! == imgData) + } + XCTAssertEqual(sut.numberField.text!.count, Int(0)) + XCTAssertEqual(sut.expirationField.text!.count, Int(0)) + XCTAssertEqual(sut.cvcField.text!.count, Int(0)) + XCTAssertTrue(sut.numberField.isFirstResponder, "after `setCardParams:` that clears the text fields, the first invalid field should become firstResponder") + XCTAssertFalse(sut.isValid) + } + + func testBecomeFirstResponder() { + sut.postalCodeEntryEnabled = false + XCTAssertTrue(sut.canBecomeFirstResponder) + XCTAssertTrue(sut.becomeFirstResponder()) + XCTAssertTrue(sut.isFirstResponder) + + XCTAssertEqual(sut.numberField, sut.currentFirstResponderField()) + + sut.becomeFirstResponder() + XCTAssertEqual( + sut.numberField, + sut.currentFirstResponderField(), + "Repeated calls to becomeFirstResponder should not change the firstResponder") + + sut.numberField.text = """ + 4242\ + 4242\ + 4242\ + 4242 + """ + + // Don't unit test auto-advance from number field here because we don't know the cache state + + XCTAssertTrue(sut.cvcField.becomeFirstResponder()) + XCTAssertEqual( + sut.cvcField, + sut.currentFirstResponderField(), + "We don't block other fields from becoming firstResponder") + + XCTAssertTrue(sut.becomeFirstResponder()) + XCTAssertEqual( + sut.cvcField, + sut.currentFirstResponderField(), + "Calling becomeFirstResponder does not change the currentFirstResponder") + + sut.expirationField.text = "10/50" + sut.cvcField.text = "123" + + sut.resignFirstResponder() + XCTAssertTrue(sut.canBecomeFirstResponder) + XCTAssertTrue(sut.becomeFirstResponder()) + + XCTAssertEqual( + sut.cvcField, + sut.currentFirstResponderField(), + "When all fields are valid, the last one should be the preferred firstResponder") + + sut.postalCodeEntryEnabled = true + XCTAssertFalse(sut.isValid) + + sut.resignFirstResponder() + XCTAssertTrue(sut.becomeFirstResponder()) + XCTAssertEqual( + sut.postalCodeField, + sut.currentFirstResponderField(), + "When postalCodeEntryEnabled=YES, it should become firstResponder after other fields are valid") + + sut.expirationField.text = "" + sut.resignFirstResponder() + XCTAssertTrue(sut.becomeFirstResponder()) + XCTAssertEqual( + sut.expirationField, + sut.currentFirstResponderField(), + "Moves firstResponder back to expiration, because it's not valid anymore") + + sut.expirationField.text = "10/50" + sut.postalCodeField.text = "90210" + + sut.resignFirstResponder() + XCTAssertTrue(sut.becomeFirstResponder()) + XCTAssertEqual( + sut.postalCodeField, + sut.currentFirstResponderField(), + "When all fields are valid, the last one should be the preferred firstResponder") + } + + func testShouldReturnCyclesThroughFields() { + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.willEndEditingForReturn = { _ in + XCTFail("Did not expect editing to end in this test") + } + sut.delegate = delegate + + sut.becomeFirstResponder() + XCTAssertTrue(sut.numberField.isFirstResponder) + + XCTAssertFalse(sut.numberField.delegate!.textFieldShouldReturn!(sut.numberField), "shouldReturn = NO") + XCTAssertTrue(sut.expirationField.isFirstResponder, "with side effect to move 1st responder to next field") + + XCTAssertFalse(sut.expirationField.delegate!.textFieldShouldReturn!(sut.expirationField), "shouldReturn = NO") + XCTAssertTrue(sut.cvcField.isFirstResponder, "with side effect to move 1st responder to next field") + + XCTAssertFalse(sut.cvcField.delegate!.textFieldShouldReturn!(sut.cvcField), "shouldReturn = NO") + XCTAssertTrue(sut.postalCodeField.isFirstResponder, "with side effect to move 1st responder to next field") + + XCTAssertFalse(sut.postalCodeField.delegate!.textFieldShouldReturn!(sut.postalCodeField), "shouldReturn = NO") + XCTAssertTrue(sut.numberField.isFirstResponder, "with side effect to move 1st responder from last field to first invalid field") + } + + func testShouldReturnCyclesThroughFieldsWithoutPostal() { + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.willEndEditingForReturn = { _ in + XCTFail("Did not expect editing to end in this test") + } + sut.delegate = delegate + sut.postalCodeEntryEnabled = false + + sut.becomeFirstResponder() + XCTAssertTrue(sut.numberField.isFirstResponder) + + XCTAssertFalse(sut.numberField.delegate!.textFieldShouldReturn!(sut.numberField), "shouldReturn = NO") + + XCTAssertTrue(sut.expirationField.isFirstResponder, "with side effect to move 1st responder to next field") + + XCTAssertFalse(sut.expirationField.delegate!.textFieldShouldReturn!(sut.expirationField), "shouldReturn = NO") + XCTAssertTrue(sut.cvcField.isFirstResponder, "with side effect to move 1st responder to next field") + + XCTAssertFalse(sut.cvcField.delegate!.textFieldShouldReturn!(sut.cvcField), "shouldReturn = NO") + XCTAssertTrue(sut.numberField.isFirstResponder, "with side effect to move 1st responder from last field to first invalid field") + } + + func testShouldReturnDismissesWhenValidNoPostalCode() { + var hasReturned = false + var didEnd = false + + sut.postalCodeEntryEnabled = false + sut.paymentMethodParams = STPPaymentMethodParams(card: STPFixtures.paymentMethodCardParams(), billingDetails: nil, metadata: nil) + + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.willEndEditingForReturn = { _ in + XCTAssertFalse(didEnd, "willEnd is called before didEnd") + XCTAssertFalse(hasReturned, "willEnd is only called once") + hasReturned = true + } + + delegate.didEndEditing = { _ in + XCTAssertTrue(hasReturned, "didEndEditing should be called after willEnd") + XCTAssertFalse(didEnd, "didEnd is only called once") + didEnd = true + } + + sut.delegate = delegate + sut.becomeFirstResponder() + XCTAssertTrue(sut.cvcField.isFirstResponder, "when textfield is filled out, default first responder is the last field") + + XCTAssertFalse(hasReturned, "willEndEditingForReturn delegate method should not have been called yet") + + XCTAssertFalse(sut.cvcField.delegate!.textFieldShouldReturn!(sut.cvcField), "shouldReturn = NO") + + XCTAssertNil(sut.currentFirstResponderField(), "Should have resigned first responder") + XCTAssertTrue(hasReturned, "delegate method has been invoked") + XCTAssertTrue(didEnd, "delegate method has been invoked") + } + + func testShouldReturnDismissesWhenValid() { + var hasReturned = false + var didEnd = false + + sut.paymentMethodParams = STPPaymentMethodParams(card: STPFixtures.paymentMethodCardParams(), billingDetails: nil, metadata: nil) + sut.postalCodeField.text = "90210" + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.willEndEditingForReturn = { _ in + XCTAssertFalse(didEnd, "willEnd is called before didEnd") + XCTAssertFalse(hasReturned, "willEnd is only called once") + hasReturned = true + } + + delegate.didEndEditing = { _ in + XCTAssertTrue(hasReturned, "didEndEditing should be called after willEnd") + XCTAssertFalse(didEnd, "didEnd is only called once") + didEnd = true + } + + sut.delegate = delegate + sut.becomeFirstResponder() + XCTAssertTrue(sut.postalCodeField.isFirstResponder, "when textfield is filled out, default first responder is the last field") + + XCTAssertFalse(hasReturned, "willEndEditingForReturn delegate method should not have been called yet") + XCTAssertFalse(sut.postalCodeField.delegate!.textFieldShouldReturn!(sut.postalCodeField), "shouldReturn = NO") + + XCTAssertNil(sut.currentFirstResponderField(), "Should have resigned first responder") + XCTAssertTrue(hasReturned, "delegate method has been invoked") + XCTAssertTrue(didEnd, "delegate method has been invoked") + } + + func testValueUpdatesWhenDeletingOnEmptyField() { + let card = STPPaymentMethodCardParams() + let number = "4242424242424242" + card.number = number + sut.paymentMethodParams = STPPaymentMethodParams(card: card, billingDetails: nil, metadata: nil) + var hasChanged = false + let delegate = PaymentCardTextFieldBlockDelegate() + delegate.didChange = { textField in + XCTAssertEqual(textField.numberField.text, "424242424242424") + XCTAssertEqual(textField.cardNumber, "424242424242424") + XCTAssertFalse(hasChanged, "didChange delegate method should not have been called yet") + hasChanged = true + } + + sut.delegate = delegate + sut.becomeFirstResponder() + sut.deleteBackward() + XCTAssertEqual(sut.numberField.text, "424242424242424") + XCTAssertEqual(sut.cardNumber, "424242424242424") + XCTAssertTrue(hasChanged, "delegate method has been invoked") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentCardTextFieldTestsSwift.swift b/Stripe/StripeiOSTests/STPPaymentCardTextFieldTestsSwift.swift new file mode 100644 index 00000000..c914d4dd --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentCardTextFieldTestsSwift.swift @@ -0,0 +1,67 @@ +// +// STPPaymentCardTextFieldTestsSwift.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 8/24/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentCardTextFieldTestsSwift: XCTestCase { + + func testClearMaintainsPostalCodeEntryEnabled() { + let textField = STPPaymentCardTextField() + let postalCodeEntryDefaultEnabled = textField.postalCodeEntryEnabled + textField.clear() + XCTAssertEqual( + postalCodeEntryDefaultEnabled, + textField.postalCodeEntryEnabled, + "clear overrode default postalCodeEntryEnabled value" + ) + + // -- + textField.postalCodeEntryEnabled = false + textField.clear() + XCTAssertFalse( + textField.postalCodeEntryEnabled, + "clear overrode custom postalCodeEntryEnabled false value" + ) + + // -- + textField.postalCodeEntryEnabled = true + // The ORs in this test are to handle if these tests are run in an environment + // where the locale doesn't require postal codes, in which case the calculated + // value for postalCodeEntryEnabled can be different than the value set + // (this is a legacy API). + let stillTrueOrRequestedButNoPostal = + textField.postalCodeEntryEnabled + || (textField.viewModel.postalCodeRequested + && STPPostalCodeValidator.postalCodeIsRequired( + forCountryCode: textField.viewModel.postalCodeCountryCode + )) + XCTAssertTrue( + stillTrueOrRequestedButNoPostal, + "clear overrode custom postalCodeEntryEnabled true value" + ) + + } + + func testPostalCodeIsValidWhenExpirationIsNot() { + let cardTextField = STPPaymentCardTextField() + + // Old expiration date + cardTextField.expirationField.text = "10/10" + XCTAssertFalse(cardTextField.expirationField.validText) + + cardTextField.postalCode = "10001" + cardTextField.formTextFieldTextDidChange(cardTextField.postalCodeField) + XCTAssertTrue(cardTextField.postalCodeField.validText) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentCardTextFieldViewModelTest.swift b/Stripe/StripeiOSTests/STPPaymentCardTextFieldViewModelTest.swift new file mode 100644 index 00000000..b10a63af --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentCardTextFieldViewModelTest.swift @@ -0,0 +1,112 @@ +// +// STPPaymentCardTextFieldViewModelTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 7/16/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentCardTextFieldViewModelTest: XCTestCase { + var viewModel: STPPaymentCardTextFieldViewModel? + + override func setUp() { + super.setUp() + viewModel = STPPaymentCardTextFieldViewModel(brandUpdateHandler: {}) + } + + func testCardNumber() { + let tests = [ + ["", ""], + ["4242", "4242"], + ["4242424242424242", "4242424242424242"], + ["4242 4242 4242 4242", "4242424242424242"], + ["4242xxx4242", "42424242"], + ["12345678901234567890", "1234567890123456789"], + ] + for test in tests { + viewModel?.cardNumber = test[0] + XCTAssertEqual(viewModel?.cardNumber, test[1]) + } + } + + func testRawExpiration() { + // swiftlint:disable:next large_tuple + let tests: [(String, String, String, String, STPCardValidationState)] = [ + ("", "", "", "", .incomplete), + ("12/25", "12/25", "12", "25", .valid), + ("1225", "12/25", "12", "25", .valid), + ("1", "1", "1", "", .incomplete), + ("2", "02/", "02", "", .incomplete), + ("12", "12/", "12", "", .incomplete), + ("12/2", "12/2", "12", "2", .incomplete), + ("99/23", "99", "99", "23", .invalid), + ("10/12", "10/12", "10", "12", .invalid), + ("12*25", "12/25", "12", "25", .valid), + ("12/*", "12/", "12", "", .incomplete), + ("*", "", "", "", .incomplete), + ] + for test in tests { + viewModel?.rawExpiration = test.0 + XCTAssertEqual(viewModel?.rawExpiration, test.1) + XCTAssertEqual(viewModel?.expirationMonth, test.2) + XCTAssertEqual(viewModel?.expirationYear, test.3) + XCTAssertEqual(viewModel?.validationStateForExpiration(), test.4) + } + } + + func testCVC() { + let tests = [["1", "1"], ["1234", "1234"], ["12345", "1234"], ["1x", "1"]] + for test in tests { + viewModel?.cvc = test[0] + XCTAssertEqual(viewModel?.cvc, test[1]) + } + } + + func testValidity() { + viewModel?.cardNumber = "4242424242424242" + viewModel?.rawExpiration = "12/24" + viewModel?.cvc = "123" + XCTAssertTrue(viewModel!.isValid) + + viewModel?.cvc = "12" + XCTAssertFalse(viewModel!.isValid) + } + + func testCompressedCardNumber() { + viewModel?.cardNumber = nil + // Should use default placeholder + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "4242") + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: "1234567812345678"), "5678") + + viewModel?.cardNumber = "424212345678" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "5678") + viewModel?.cardNumber = "42421234567" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "567") + viewModel?.cardNumber = "4242123456" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "56") + viewModel?.cardNumber = "424212345" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "5") + viewModel?.cardNumber = "42421234" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "1234") + + viewModel?.cardNumber = "12" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "12") + + viewModel?.cardNumber = "36227206271667" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "1667") + viewModel?.cardNumber = "3622720627166" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "166") + viewModel?.cardNumber = "36227206271" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "1") + viewModel?.cardNumber = "3622720627" + XCTAssertEqual(viewModel?.compressedCardNumber(withPlaceholder: nil), "720627") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentConfigurationTest.m b/Stripe/StripeiOSTests/STPPaymentConfigurationTest.m new file mode 100644 index 00000000..8f23a409 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentConfigurationTest.m @@ -0,0 +1,141 @@ +// +// STPPaymentConfigurationTest.m +// Stripe +// +// Created by Joey Dong on 7/18/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +#import +#import + + +@import StripeCore; + + + + +@interface STPPaymentConfigurationTest : XCTestCase + +@end + +@implementation STPPaymentConfigurationTest + +- (void)testSharedConfiguration { + XCTAssertEqual([STPPaymentConfiguration sharedConfiguration], [STPPaymentConfiguration sharedConfiguration]); +} + +- (void)testInit { + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + + XCTAssertFalse(paymentConfiguration.fpxEnabled); + XCTAssertEqual(paymentConfiguration.requiredBillingAddressFields, STPBillingAddressFieldsPostalCode); + XCTAssertNil(paymentConfiguration.requiredShippingAddressFields); + XCTAssert(paymentConfiguration.verifyPrefilledShippingAddress); + XCTAssertEqual(paymentConfiguration.shippingType, STPShippingTypeShipping); + XCTAssertEqualObjects(paymentConfiguration.companyName, @"xctest"); + XCTAssertNil(paymentConfiguration.appleMerchantIdentifier); + XCTAssert(paymentConfiguration.canDeletePaymentOptions); + XCTAssertFalse(paymentConfiguration.cardScanningEnabled); +} + +- (void)testApplePayEnabledSatisfied { + id stripeMock = OCMClassMock([StripeAPI class]); + OCMStub([stripeMock deviceSupportsApplePay]).andReturn(YES); + + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + paymentConfiguration.appleMerchantIdentifier = @"appleMerchantIdentifier"; + + XCTAssert([paymentConfiguration applePayEnabled]); +} + +- (void)testApplePayEnabledMissingAppleMerchantIdentifier { + id stripeMock = OCMClassMock([StripeAPI class]); + OCMStub([stripeMock deviceSupportsApplePay]).andReturn(YES); + + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + paymentConfiguration.appleMerchantIdentifier = nil; + + XCTAssertFalse([paymentConfiguration applePayEnabled]); +} + +- (void)testApplePayEnabledDisallowAdditionalPaymentOptions { + id stripeMock = OCMClassMock([StripeAPI class]); + OCMStub([stripeMock deviceSupportsApplePay]).andReturn(YES); + + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + paymentConfiguration.appleMerchantIdentifier = @"appleMerchantIdentifier"; + paymentConfiguration.applePayEnabled = false; + + XCTAssertFalse([paymentConfiguration applePayEnabled]); +} + +- (void)testApplePayEnabledMisisngDeviceSupport { + // Change the supported networks list to reset the applePayEnabled cache + StripeAPI.additionalEnabledApplePayNetworks = @[PKPaymentNetworkJCB]; + id paymentAuthControllerMock = OCMClassMock([PKPaymentAuthorizationController class]); + OCMStub([paymentAuthControllerMock canMakePaymentsUsingNetworks:[OCMArg any]]).andReturn(NO); + + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + paymentConfiguration.appleMerchantIdentifier = @"appleMerchantIdentifier"; + + XCTAssertFalse([paymentConfiguration applePayEnabled]); + [paymentAuthControllerMock stopMocking]; + // Re-reset cache: + StripeAPI.additionalEnabledApplePayNetworks = @[]; +} + +#pragma mark - Description + +- (void)testDescription { + STPPaymentConfiguration *paymentConfiguration = [[STPPaymentConfiguration alloc] init]; + XCTAssert(paymentConfiguration.description); +} + +#pragma mark - NSCopying + +- (void)testCopyWithZone { + NSSet *allFields = [NSSet setWithArray:@[STPContactField.postalAddress, + STPContactField.emailAddress, + STPContactField.phoneNumber, + STPContactField.name]]; + + STPPaymentConfiguration *paymentConfigurationA = [[STPPaymentConfiguration alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + paymentConfigurationA.publishableKey = @"publishableKey"; + paymentConfigurationA.stripeAccount = @"stripeAccount"; +#pragma clang diagnostic pop + paymentConfigurationA.applePayEnabled = YES; + paymentConfigurationA.requiredBillingAddressFields = STPBillingAddressFieldsFull; + paymentConfigurationA.requiredShippingAddressFields = allFields; + paymentConfigurationA.verifyPrefilledShippingAddress = NO; + paymentConfigurationA.availableCountries = [NSSet setWithArray:@[@"US", @"CA", @"BT"]]; + paymentConfigurationA.shippingType = STPShippingTypeDelivery; + paymentConfigurationA.companyName = @"companyName"; + paymentConfigurationA.appleMerchantIdentifier = @"appleMerchantIdentifier"; + paymentConfigurationA.canDeletePaymentOptions = NO; + paymentConfigurationA.cardScanningEnabled = NO; + + STPPaymentConfiguration *paymentConfigurationB = [paymentConfigurationA copy]; + XCTAssertNotEqual(paymentConfigurationA, paymentConfigurationB); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertEqualObjects(paymentConfigurationB.publishableKey, @"publishableKey"); + XCTAssertEqualObjects(paymentConfigurationB.stripeAccount, @"stripeAccount"); +#pragma clang diagnostic pop + XCTAssertTrue(paymentConfigurationB.applePayEnabled); + XCTAssertEqual(paymentConfigurationB.requiredBillingAddressFields, STPBillingAddressFieldsFull); + XCTAssertEqualObjects(paymentConfigurationB.requiredShippingAddressFields, allFields); + XCTAssertFalse(paymentConfigurationB.verifyPrefilledShippingAddress); + XCTAssertEqual(paymentConfigurationB.shippingType, STPShippingTypeDelivery); + XCTAssertEqualObjects(paymentConfigurationB.companyName, @"companyName"); + XCTAssertEqualObjects(paymentConfigurationB.appleMerchantIdentifier, @"appleMerchantIdentifier"); + NSSet *availableCountries = [NSSet setWithArray:@[@"US", @"CA", @"BT"]]; + XCTAssertEqualObjects(paymentConfigurationB.availableCountries, availableCountries); + XCTAssertEqual(paymentConfigurationA.canDeletePaymentOptions, paymentConfigurationB.canDeletePaymentOptions); + XCTAssertEqual(paymentConfigurationA.cardScanningEnabled, paymentConfigurationB.cardScanningEnabled); +} + +@end diff --git a/Stripe/StripeiOSTests/STPPaymentContextApplePayTest.swift b/Stripe/StripeiOSTests/STPPaymentContextApplePayTest.swift new file mode 100644 index 00000000..1e317ef3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentContextApplePayTest.swift @@ -0,0 +1,204 @@ +// +// STPPaymentContextApplePayTest.swift +// StripeiOS Tests +// +// Created by Brian Dorfman on 8/1/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +/// These tests cover STPPaymentContext's Apple Pay specific behavior: +/// - building a PKPaymentRequest +/// - determining paymentSummaryItems +class STPPaymentContextApplePayTest: XCTestCase { + func buildPaymentContext() -> STPPaymentContext { + let config = STPPaymentConfiguration() + config.appleMerchantIdentifier = "fake_merchant_id" + let theme = STPTheme.defaultTheme + let customerContext = Testing_StaticCustomerContext() + let paymentContext = STPPaymentContext( + customerContext: customerContext, + configuration: config, + theme: theme + ) + return paymentContext + } + + // MARK: - buildPaymentRequest + func testBuildPaymentRequest_totalAmount() { + let context = buildPaymentContext() + context.paymentAmount = 150 + let request = context.buildPaymentRequest() + + XCTAssertTrue( + (request?.paymentSummaryItems.last?.amount == NSDecimalNumber(string: "1.50")), + "PKPayment total is not equal to STPPaymentContext amount" + ) + } + + func testBuildPaymentRequest_USDDefault() { + let context = buildPaymentContext() + context.paymentAmount = 100 + let request = context.buildPaymentRequest() + + XCTAssertTrue( + (request?.currencyCode == "USD"), + "Default PKPaymentRequest currency code is not USD" + ) + } + + func testBuildPaymentRequest_currency() { + let context = buildPaymentContext() + context.paymentAmount = 100 + context.paymentCurrency = "GBP" + let request = context.buildPaymentRequest() + + XCTAssertTrue( + (request?.currencyCode == "GBP"), + "PKPaymentRequest currency code is not equal to STPPaymentContext currency" + ) + } + + func testBuildPaymentRequest_uppercaseCurrency() { + let context = buildPaymentContext() + context.paymentAmount = 100 + context.paymentCurrency = "eur" + let request = context.buildPaymentRequest() + + XCTAssertTrue( + (request?.currencyCode == "EUR"), + "PKPaymentRequest currency code is not uppercased" + ) + } + + func testSummaryItems() -> [PKPaymentSummaryItem]? { + return [ + PKPaymentSummaryItem( + label: "First item", + amount: NSDecimalNumber(mantissa: 20, exponent: 0, isNegative: false) + ), + PKPaymentSummaryItem( + label: "Second item", + amount: NSDecimalNumber(mantissa: 90, exponent: 0, isNegative: false) + ), + PKPaymentSummaryItem( + label: "Discount", + amount: NSDecimalNumber(mantissa: 10, exponent: 0, isNegative: true) + ), + PKPaymentSummaryItem( + label: "Total", + amount: NSDecimalNumber(mantissa: 100, exponent: 0, isNegative: false) + ), + ] + } + + func testBuildPaymentRequest_summaryItems() { + let context = buildPaymentContext() + context.paymentSummaryItems = testSummaryItems()! + let request = context.buildPaymentRequest() + + XCTAssertTrue((request?.paymentSummaryItems == context.paymentSummaryItems)) + } + + // MARK: - paymentSummaryItems + func testSetPaymentAmount_generateSummaryItems() { + let context = buildPaymentContext() + context.paymentAmount = 10000 + context.paymentCurrency = "USD" + let itemTotalAmount = context.paymentSummaryItems.last?.amount + let correctTotalAmount = NSDecimalNumber.stp_decimalNumber( + withAmount: context.paymentAmount, + currency: context.paymentCurrency + ) + + XCTAssertTrue((itemTotalAmount == correctTotalAmount)) + } + + func testSetPaymentAmount_generateSummaryItemsShippingMethod() { + let context = buildPaymentContext() + context.paymentAmount = 100 + context.configuration.companyName = "Foo Company" + let method = PKShippingMethod() + method.amount = NSDecimalNumber(string: "5.99") + method.label = "FedEx" + method.detail = "foo" + method.identifier = "123" + context.selectedShippingMethod = method + + let items = context.paymentSummaryItems + XCTAssertEqual(Int(items.count), 2) + let item1 = items[0] + XCTAssertEqual(item1.label, "FedEx") + XCTAssertEqual(item1.amount, NSDecimalNumber(string: "5.99")) + let item2 = items[1] + XCTAssertEqual(item2.label, "Foo Company") + XCTAssertEqual(item2.amount, NSDecimalNumber(string: "6.99")) + } + + func testSummaryItemsToSummaryItems_shippingMethod() { + let context = buildPaymentContext() + let item1 = PKPaymentSummaryItem() + item1.amount = NSDecimalNumber(string: "1.00") + item1.label = "foo" + let item2 = PKPaymentSummaryItem() + item2.amount = NSDecimalNumber(string: "9.00") + item2.label = "bar" + let item3 = PKPaymentSummaryItem() + item3.amount = NSDecimalNumber(string: "10.00") + item3.label = "baz" + context.paymentSummaryItems = [item1, item2, item3] + let method = PKShippingMethod() + method.amount = NSDecimalNumber(string: "5.99") + method.label = "FedEx" + method.detail = "foo" + method.identifier = "123" + context.selectedShippingMethod = method + + let items = context.paymentSummaryItems + XCTAssertEqual(Int(items.count), 4) + let resultItem1 = items[0] + XCTAssertEqual(resultItem1.label, "foo") + XCTAssertEqual(resultItem1.amount, NSDecimalNumber(string: "1.00")) + let resultItem2 = items[1] + XCTAssertEqual(resultItem2.label, "bar") + XCTAssertEqual(resultItem2.amount, NSDecimalNumber(string: "9.00")) + let resultItem3 = items[2] + XCTAssertEqual(resultItem3.label, "FedEx") + XCTAssertEqual(resultItem3.amount, NSDecimalNumber(string: "5.99")) + let resultItem4 = items[3] + XCTAssertEqual(resultItem4.label, "baz") + XCTAssertEqual(resultItem4.amount, NSDecimalNumber(string: "15.99")) + } + + func testAmountToAmount_shippingMethod_usd() { + let context = buildPaymentContext() + context.paymentAmount = 100 + let method = PKShippingMethod() + method.amount = NSDecimalNumber(string: "5.99") + method.label = "FedEx" + method.detail = "foo" + method.identifier = "123" + context.selectedShippingMethod = method + let amount = context.paymentAmount + XCTAssertEqual(amount, 699) + } + + func testSummaryItems_generateAmountDecimalCurrency() { + let context = buildPaymentContext() + context.paymentSummaryItems = testSummaryItems()! + context.paymentCurrency = "USD" + XCTAssertTrue(context.paymentAmount == 10000) + } + + func testSummaryItems_generateAmountNoDecimalCurrency() { + let context = buildPaymentContext() + context.paymentSummaryItems = testSummaryItems()! + context.paymentCurrency = "JPY" + XCTAssertTrue(context.paymentAmount == 100) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentContextSnapshotTests.swift b/Stripe/StripeiOSTests/STPPaymentContextSnapshotTests.swift new file mode 100644 index 00000000..319e9b96 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentContextSnapshotTests.swift @@ -0,0 +1,84 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentContextSnapshotTests.m +// StripeiOS Tests +// +// Created by Ben Guo on 12/13/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCaseCore +import StripeCoreTestUtils + +class STPPaymentContextSnapshotTests: STPSnapshotTestCase { + var customerContext: STPCustomerContext? + var config: STPPaymentConfiguration? + var hostViewController: UINavigationController? + var paymentContext: STPPaymentContext? + + override func setUp() { + super.setUp() + let config = STPPaymentConfiguration() + config.companyName = "Test Company" + config.requiredBillingAddressFields = .full + config.shippingType = .shipping + self.config = config + let customerContext = Testing_StaticCustomerContext_Objc.init(customer: STPFixtures.customerWithCardTokenAndSourceSources(), paymentMethods: [STPFixtures.paymentMethod(), STPFixtures.paymentMethod()]) + self.customerContext = customerContext + + let viewController = UIViewController() + hostViewController = stp_navigationControllerForSnapshotTest(withRootVC: viewController) + } + + func buildPaymentContext() { + let context = STPPaymentContext(customerContext: customerContext!) + context.hostViewController = hostViewController + context.configuration.requiredShippingAddressFields = Set([STPContactField.emailAddress]) + paymentContext = context + } + + func testPushPaymentOptionsSmallTitle() { + buildPaymentContext() + + hostViewController?.navigationBar.prefersLargeTitles = false + paymentContext?.largeTitleDisplayMode = UINavigationItem.LargeTitleDisplayMode.automatic + paymentContext?.pushPaymentOptionsViewController() + let view = stp_preparedAndSizedViewForSnapshotTest(from: hostViewController)! + STPSnapshotVerifyView(view, identifier: nil) + } + + // This test renders at a slightly larger size half the time. + // We're deprecating Basic Integration soon, and we've spent enough time on this, + // so these tests are being disabled for now. + // - (void)testPushPaymentOptionsLargeTitle { + // [self buildPaymentContext]; + // + // self.hostViewController.navigationBar.prefersLargeTitles = YES; + // self.paymentContext.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeAutomatic; + // [self.paymentContext pushPaymentOptionsViewController]; + // UIView *view = [self stp_preparedAndSizedViewForSnapshotTestFromNavigationController:self.hostViewController]; + // STPSnapshotVerifyView(view, nil); + // } + + func testPushShippingAddressSmallTitle() { + buildPaymentContext() + + hostViewController?.navigationBar.prefersLargeTitles = false + paymentContext?.largeTitleDisplayMode = UINavigationItem.LargeTitleDisplayMode.automatic + paymentContext?.pushShippingViewController() + let view = stp_preparedAndSizedViewForSnapshotTest(from: hostViewController)! + STPSnapshotVerifyView(view, identifier: nil) + } + // This test renders at a slightly larger size half the time. + // We're deprecating Basic Integration soon, and we've spent enough time on this, + // so these tests are being disabled for now. + // - (void)testPushShippingAddressLargeTitle { + // [self buildPaymentContext]; + // + // self.hostViewController.navigationBar.prefersLargeTitles = YES; + // self.paymentContext.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeAutomatic; + // [self.paymentContext pushShippingViewController]; + // UIView *view = [self stp_preparedAndSizedViewForSnapshotTestFromNavigationController:self.hostViewController]; + // STPSnapshotVerifyView(view, nil); + // } +} diff --git a/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.m b/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.m new file mode 100644 index 00000000..699a7ca4 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.m @@ -0,0 +1,136 @@ +// +// STPPaymentHandlerFunctionalTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/14/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +#import +@import Stripe; +#import +#import + +@import StripePaymentsObjcTestUtils; + +@interface STPPaymentHandlerFunctionalTest : XCTestCase +@property (nonatomic) id presentingViewController; +@property (nonatomic) id applicationMock; +@end + +@interface STPPaymentHandler (Test) +- (BOOL)_canPresentWithAuthenticationContext:(id)authenticationContext error:(NSError **)error; +@end + +@implementation STPPaymentHandlerFunctionalTest + +- (void)setUp { + self.presentingViewController = OCMClassMock([UIViewController class]); + // Mock UIApplication.shared, which is otherwise not available in XCTestCase, to always call its completion block with @NO (i.e. it couldn't open a native app with the URL) + self.applicationMock = OCMClassMock([UIApplication class]); + OCMStub([self.applicationMock sharedApplication]).andReturn(self.applicationMock); + OCMStub([self.applicationMock openURL:[OCMArg any] + options:[OCMArg any] + completionHandler:([OCMArg invokeBlockWithArgs:@NO, nil])]); + [STPAPIClient sharedClient].publishableKey = STPTestingDefaultPublishableKey; +} + +// N.B. Test mode alipay PaymentIntent's never have a native redirect so we can't test that here +- (void)testAlipayOpensWebviewAfterNativeURLUnavailable { + __block NSString *clientSecret = @"pi_123_secret_456"; + + id apiClient = OCMPartialMock(STPAPIClient.sharedClient); + NSMutableDictionary *paymentIntentJSON = [[STPTestUtils jsonNamed:@"PaymentIntent"] mutableCopy]; + paymentIntentJSON[@"payment_method"] = [STPTestUtils jsonNamed:STPTestJSONPaymentMethodCard]; + STPPaymentIntent *paymentIntent = [STPPaymentIntent decodedObjectFromAPIResponse:paymentIntentJSON]; + + OCMStub([apiClient confirmPaymentIntentWithParams:[OCMArg any] expand:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + void (^handler)(STPPaymentIntent *paymentIntent, __unused NSError * _Nullable error); + [invocation getArgument:&handler atIndex:4]; + handler(paymentIntent, nil); + }); + + OCMStub([apiClient retrievePaymentIntentWithClientSecret:[OCMArg any] expand:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + void (^handler)(STPPaymentIntent *paymentIntent, __unused NSError * _Nullable error); + [invocation getArgument:&handler atIndex:4]; + handler(paymentIntent, nil); + }); + + id paymentHandler = OCMPartialMock(STPPaymentHandler.sharedHandler); + OCMStub([paymentHandler apiClient]).andReturn(apiClient); + + // Simulate the safari VC finishing after presenting it + OCMStub([self.presentingViewController presentViewController:[OCMArg any] animated:YES completion:[OCMArg any]]).andDo(^(__unused NSInvocation *_) { + [paymentHandler safariViewControllerDidFinish:self.presentingViewController]; + }); + + STPPaymentIntentParams *confirmParams = [[STPPaymentIntentParams alloc] initWithClientSecret:clientSecret]; + confirmParams.paymentMethodOptions = [STPConfirmPaymentMethodOptions new]; + confirmParams.paymentMethodOptions.alipayOptions = [STPConfirmAlipayOptions new]; + confirmParams.paymentMethodParams = [STPPaymentMethodParams paramsWithAlipay:[STPPaymentMethodAlipayParams new] billingDetails:nil metadata:nil]; + confirmParams.returnURL = @"foo://bar"; + + XCTestExpectation *e = [self expectationWithDescription:@""]; + [paymentHandler confirmPayment:confirmParams withAuthenticationContext:self completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent * __unused _, __unused NSError * _Nullable error) { + // ...shouldn't attempt to open the native URL (ie the alipay app) + OCMReject([self.applicationMock openURL:[OCMArg any] + options:[OCMArg any] + completionHandler:[OCMArg isNotNil]]); + // ...and then open UIViewController + OCMVerify([self.presentingViewController presentViewController:[OCMArg any] animated:YES completion:[OCMArg any]]); + + // ...and since we didn't actually authenticate, the final state is canceled + XCTAssertEqual(status, STPPaymentHandlerActionStatusCanceled); + [e fulfill]; + }]; + [self waitForExpectationsWithTimeout:4 handler:nil]; + [paymentHandler stopMocking]; // paymentHandler is a singleton, so we need to manually call `stopMocking` +} + +- (void)test_oxxo_payment_intent_server_side_confirmation { + // OXXO is interesting b/c the PI status after handling next actions is requires_action, not succeeded. + id paymentHandler = OCMPartialMock(STPPaymentHandler.sharedHandler); + + // Simulate the safari VC finishing after presenting it + OCMStub([self.presentingViewController presentViewController:[OCMArg any] animated:YES completion:[OCMArg any]]).andDo(^(__unused NSInvocation *_) { + [paymentHandler safariViewControllerDidFinish:self.presentingViewController]; + }); + + STPAPIClient *apiClient = [[STPAPIClient alloc] initWithPublishableKey: STPTestingMEXPublishableKey]; + [STPAPIClient sharedClient].publishableKey = STPTestingMEXPublishableKey; + + STPPaymentMethodBillingDetails *billingDetails = [STPPaymentMethodBillingDetails new]; + billingDetails.name = @"Test Customer"; + billingDetails.email = @"test@example.com"; + + XCTestExpectation *e = [self expectationWithDescription:@""]; + STPPaymentMethodParams *params = [[STPPaymentMethodParams alloc] initWithOxxo:[STPPaymentMethodOXXOParams new] billingDetails:billingDetails metadata:nil]; + [apiClient createPaymentMethodWithParams:params completion:^(STPPaymentMethod * paymentMethod, NSError * error) { + XCTAssertNil(error); + NSDictionary *pi_params = @{ + @"confirm": @"true", + @"payment_method_types": @[@"oxxo"], + @"currency": @"mxn", + @"amount": @1099, + @"payment_method": paymentMethod.stripeId, + @"return_url": @"foo://z" + }; + [[STPTestingAPIClient new] createPaymentIntentWithParams:pi_params account:@"mex" apiVersion:nil completion:^(NSString * clientSecret, NSError * error2) { + XCTAssertNil(error2); + [paymentHandler handleNextActionForPayment:clientSecret withAuthenticationContext:self returnURL:@"foo://z" completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent * paymentIntent, NSError * error3) { + XCTAssertNil(error3); + XCTAssertEqual(paymentIntent.status, STPPaymentIntentStatusRequiresAction); + XCTAssertEqual(status, STPPaymentHandlerActionStatusSucceeded); + [e fulfill]; + }]; + }]; + }]; + [self waitForExpectationsWithTimeout:4 handler:nil]; + [paymentHandler stopMocking]; // paymentHandler is a singleton, so we need to manually call `stopMocking` +} + +- (UIViewController *)authenticationPresentingViewController { + return self.presentingViewController; +} + +@end diff --git a/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.swift b/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.swift new file mode 100644 index 00000000..92956ac8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.swift @@ -0,0 +1,413 @@ +// +// STPPaymentHandlerFunctionalTest.swift +// StripeiOSTests +// +// Created by Yuki Tokuhiro on 4/24/23. +// + +@testable import Stripe +@_spi(STP) @testable import StripeCore +@_spi(STP) @testable import StripePayments +@_spi(STP) @testable import StripePaymentsTestUtils +import XCTest + +// You can add tests in here for payment methods that don't require customer actions (i.e. don't open webviews for customer authentication). +// If they require customer action, use STPPaymentHandlerFunctionalTest.m instead +final class STPPaymentHandlerFunctionalSwiftTest: STPNetworkStubbingTestCase, STPAuthenticationContext { + // MARK: - STPAuthenticationContext + func authenticationPresentingViewController() -> UIViewController { + return UIViewController() + } + + // MARK: - PaymentIntent tests + + func test_card_payment_intent_server_side_confirmation() { + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let e = self.expectation(description: "") + apiClient.createPaymentMethod(with: ._testValidCardValue()) { paymentMethod, error in + guard let paymentMethod = paymentMethod else { + XCTFail(String(describing: error)) + return + } + STPTestingAPIClient.shared().createPaymentIntent(withParams: [ + "confirm": "true", + "payment_method_types": ["card"], + "currency": "usd", + "payment_method": paymentMethod.stripeId, + "return_url": "foo://z", + ]) { clientSecret, error in + guard let clientSecret = clientSecret else { + XCTFail(String(describing: error)) + return + } + let sut = STPPaymentHandler(apiClient: apiClient) + // Note: `waitForExpectations` can deadlock if this test is async. When we can use Xcode 14.3, we can switch this test to async and use fulfillment(of:) instead of waitForExpectations + sut.handleNextAction(forPayment: clientSecret, with: self, returnURL: "foo://z") { status, intent, _ in + XCTAssertEqual(sut.apiClient, apiClient) // Reference sut in the closure so it doesn't get deallocated + XCTAssertEqual(intent?.status, .succeeded) + XCTAssertEqual(status, .succeeded) + e.fulfill() + } + } + } + self.waitForExpectations(timeout: 10) + } + + func test_sepa_debit_payment_intent_server_side_confirmation() { + // SEPA Debit is a good payment method to test here because + // - it's a "delayed" or "asynchronous" payment method + // - it doesn't require customer actions (we can't simulate customer actions in XCTestCase) + + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "SEPA Test Customer" + billingDetails.email = "test@example.com" + + let sepaDebitDetails = STPPaymentMethodSEPADebitParams() + sepaDebitDetails.iban = "DE89370400440532013000" + + let e = self.expectation(description: "") + apiClient.createPaymentMethod(with: .init(sepaDebit: sepaDebitDetails, billingDetails: billingDetails, metadata: nil)) { paymentMethod, error in + guard let paymentMethod = paymentMethod else { + XCTFail(String(describing: error)) + return + } + STPTestingAPIClient.shared().createPaymentIntent(withParams: [ + "confirm": "true", + "payment_method_types": ["sepa_debit"], + "currency": "eur", + "payment_method": paymentMethod.stripeId, + "return_url": "foo://z", + "mandate_data": [ + "customer_acceptance": [ + "type": "online", + "online": [ + "user_agent": "123", + "ip_address": "172.18.117.125", + ], + ], + ], + ]) { clientSecret, error in + guard let clientSecret = clientSecret else { + XCTFail(String(describing: error)) + return + } + let sut = STPPaymentHandler(apiClient: apiClient) + // Note: `waitForExpectations` can deadlock if this test is async. When we can use Xcode 14.3, we can switch this test to async and use fulfillment(of:) instead of waitForExpectations + sut.handleNextAction(forPayment: clientSecret, with: self, returnURL: "foo://z") { status, intent, _ in + XCTAssertEqual(sut.apiClient, apiClient) // Reference sut in the closure so it doesn't get deallocated + XCTAssertEqual(intent?.status, .processing) + XCTAssertEqual(status, .succeeded) + e.fulfill() + } + } + } + self.waitForExpectations(timeout: 10) + } + + // MARK: - SetupIntent tests + + func test_card_setup_intent_server_side_confirmation() { + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let e = self.expectation(description: "") + apiClient.createPaymentMethod(with: ._testValidCardValue()) { paymentMethod, error in + guard let paymentMethod = paymentMethod else { + XCTFail(String(describing: error)) + return + } + STPTestingAPIClient.shared().createSetupIntent(withParams: [ + "confirm": "true", + "payment_method_types": ["card"], + "payment_method": paymentMethod.stripeId, + "return_url": "foo://z", + ]) { clientSecret, error in + guard let clientSecret = clientSecret else { + XCTFail(String(describing: error)) + return + } + let sut = STPPaymentHandler(apiClient: apiClient) + // Note: `waitForExpectations` can deadlock if this test is async. When we can use Xcode 14.3, we can switch this test to async and use fulfillment(of:) instead of waitForExpectations + sut.handleNextAction(forSetupIntent: clientSecret, with: self, returnURL: "foo://z") { status, intent, _ in + XCTAssertEqual(sut.apiClient, apiClient) // Reference sut in the closure so it doesn't get deallocated + XCTAssertEqual(intent?.status, .succeeded) + XCTAssertEqual(status, .succeeded) + e.fulfill() + } + } + } + self.waitForExpectations(timeout: 10) + } + + func test_sepa_debit_setup_intent_server_side_confirmation() { + // SEPA Debit is a good payment method to test here because + // - it's a "delayed" or "asynchronous" payment method + // - it doesn't require customer actions (we can't simulate customer actions in XCTestCase) + + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "SEPA Test Customer" + billingDetails.email = "test@example.com" + + let sepaDebitDetails = STPPaymentMethodSEPADebitParams() + sepaDebitDetails.iban = "DE89370400440532013000" + + let e = self.expectation(description: "") + apiClient.createPaymentMethod(with: .init(sepaDebit: sepaDebitDetails, billingDetails: billingDetails, metadata: nil)) { paymentMethod, error in + guard let paymentMethod = paymentMethod else { + XCTFail() + return + } + STPTestingAPIClient.shared().createSetupIntent(withParams: [ + "confirm": "true", + "payment_method_types": ["sepa_debit"], + "payment_method": paymentMethod.stripeId, + "return_url": "foo://z", + "mandate_data": [ + "customer_acceptance": [ + "type": "online", + "online": [ + "user_agent": "123", + "ip_address": "172.18.117.125", + ], + ], + ], + ]) { clientSecret, error in + guard let clientSecret = clientSecret else { + XCTFail("\(String(describing: error))") + return + } + let sut = STPPaymentHandler(apiClient: apiClient) + // Note: `waitForExpectations` can deadlock if this test is async. When we can use Xcode 14.3, we can switch this test to async and use fulfillment(of:) instead of waitForExpectations + sut.handleNextAction(forSetupIntent: clientSecret, with: self, returnURL: "foo://z") { status, intent, _ in + XCTAssertEqual(sut.apiClient, apiClient) // Reference sut in the closure so it doesn't get deallocated + XCTAssertEqual(intent?.status, .succeeded) // Note: I think this should be .processing, but testmode disagrees + XCTAssertEqual(status, .succeeded) + e.fulfill() + } + } + } + self.waitForExpectations(timeout: 10) + } + + // MARK: - Test payment handler sends analytics + + func test_confirm_payment_intent_sends_analytic() { + // Confirming a hardcoded already-confirmed PI with invalid params... + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_3P20wFFY0qyl6XeW0dSOQ6W7_secret_9V8GkrCOt1MEW8SBmAaGnmT6A", paymentMethodType: .card) + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.confirmPayment(paymentIntentParams, with: self) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7") + XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "payment_intent_unexpected_state") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_confirm_payment_intent_savedpm_sends_analytic() { + // Confirming a hardcoded already-confirmed PI with invalid params... + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_3P20wFFY0qyl6XeW0dSOQ6W7_secret_9V8GkrCOt1MEW8SBmAaGnmT6A") + paymentIntentParams.paymentMethodId = "pm_123" + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.confirmPayment(paymentIntentParams, with: self) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7") + XCTAssertEqual(firstAnalytic?["payment_method_id"] as? String, "pm_123") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue) + XCTAssertEqual(lastAnalytic?["payment_method_id"] as? String, "pm_123") + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "resource_missing") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_confirm_setup_intent_sends_analytic() { + let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: "seti_1P1xLBFY0qyl6XeWc7c2LrMK_secret_PrgithiYFFPH0NVGP1BK7Oy9OU3mrDT", paymentMethodType: .card) + // Confirming a hardcoded already-confirmed SI with invalid params... + setupIntentParams.paymentMethodParams = STPPaymentMethodParams(type: .card) + + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK") + XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "parameter_missing") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_confirm_setup_intent_savedpm_sends_analytic() { + let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: "seti_1P1xLBFY0qyl6XeWc7c2LrMK_secret_PrgithiYFFPH0NVGP1BK7Oy9OU3mrDT") + // Confirming a hardcoded already-confirmed SI with invalid params... + setupIntentParams.paymentMethodID = "pm_123" + + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK") + XCTAssertEqual(firstAnalytic?["payment_method_id"] as? String, "pm_123") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK") + XCTAssertEqual(lastAnalytic?["payment_method_id"] as? String, "pm_123") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "resource_missing") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_handle_next_action_payment_intent_sends_analytic() { + // Calling handleNextAction(forPayment:) with an invalid PI client secret... + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.handleNextAction(forPayment: "pi_3P232pFY0qyl6XeW0FFRtE0A_secret_foo", with: self, returnURL: nil) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_3P232pFY0qyl6XeW0FFRtE0A") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_3P232pFY0qyl6XeW0FFRtE0A") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "payment_intent_invalid_parameter") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_handle_next_action_2_payment_intent_sends_analytic() { + // Calling handleNextAction(for:) with a STPPaymentIntent w/ an unknown next action... + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentIntent = STPFixtures.paymentIntent(paymentMethodTypes: ["card"], status: .requiresAction, paymentMethod: STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard), nextAction: .unknown) + + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.handleNextAction(for: paymentIntent, with: self, returnURL: nil) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "123") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue) + XCTAssertEqual(lastAnalytic?["payment_method_id"] as? String, "pm_123456789") + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "123") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "unsupportedAuthenticationErrorCode") + XCTAssertEqual(lastAnalytic?["error_details"] as? [String: String], [ + "NSLocalizedDescription": "There was an unexpected error -- try again in a few seconds", + "com.stripe.lib:ErrorMessageKey": "Unknown authentication action type", + ]) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_handle_next_action_setup_intent_sends_analytic() { + // Calling handleNextAction(forSetupIntent:) with an invalid SI client secret... + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.handleNextAction(forSetupIntent: "seti_3P232pFY0qyl6XeW0FFRtE0A_secret_foo", with: self, returnURL: nil) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_3P232pFY0qyl6XeW0FFRtE0A") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_3P232pFY0qyl6XeW0FFRtE0A") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "resource_missing") + XCTAssertTrue((lastAnalytic?["request_id"] as? String)!.starts(with: "req_")) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } + + func test_handle_next_action_2_setup_intent_sends_analytic() { + // Calling handleNextAction(for:) with a STPSetupIntent w/ an unknown next action... + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + var siJSON = STPTestUtils.jsonNamed("SetupIntent")! + siJSON[jsonDict: "next_action"]!["type"] = "foo" + siJSON["payment_method"] = STPTestUtils.jsonNamed("CardPaymentMethod")! + let setupIntent = STPSetupIntent.decodedObject(fromAPIResponse: siJSON)! + + let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)) + let analyticsClient = STPAnalyticsClient() + paymentHandler.analyticsClient = analyticsClient + paymentHandler.handleNextAction(for: setupIntent, with: self, returnURL: nil) { (_, _, _) in + // ...should send these analytics + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_123456789") + XCTAssertEqual(firstAnalytic?["payment_method_id"] as? String, "pm_123456789") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue) + XCTAssertEqual(lastAnalytic?["payment_method_id"] as? String, "pm_123456789") + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_123456789") + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "unsupportedAuthenticationErrorCode") + XCTAssertEqual(lastAnalytic?["error_details"] as? [String: String], [ + "NSLocalizedDescription": "There was an unexpected error -- try again in a few seconds", + "com.stripe.lib:ErrorMessageKey": "Unknown authentication action type", + ]) + paymentHandlerExpectation.fulfill() + } + waitForExpectations(timeout: 10) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentHandlerRefreshTests.swift b/Stripe/StripeiOSTests/STPPaymentHandlerRefreshTests.swift new file mode 100644 index 00000000..b5fcd7b2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentHandlerRefreshTests.swift @@ -0,0 +1,151 @@ +// +// STPPaymentHandlerRefreshTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 5/13/24. +// + +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import Stripe3DS2 +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentHandlerRefreshTests: XCTestCase { + + func testPaymentIntentShouldHitRefreshEndpoint() { + let shouldRefresh: [STPPaymentMethodType] = [.cashApp] + + for paymentMethodType in STPPaymentMethodType.allCases { + let paymentMethodDict: [AnyHashable: Any] = [ + "id": "pm_test", + "type": paymentMethodType.identifier, + ] + let paymentIntent = STPFixtures.paymentIntent(paymentMethodTypes: [paymentMethodType.identifier], + status: .requiresAction, + paymentMethod: paymentMethodDict, + nextAction: .useStripeSDK) + + let apiClientMock = STPAPIClientMock() + let currentAction = STPPaymentHandlerPaymentIntentActionParams.makeTestable(apiClient: apiClientMock, + paymentMethodTypes: [paymentMethodType.identifier], + paymentIntent: paymentIntent) + + let paymentHandler = STPPaymentHandler(apiClient: apiClientMock) + paymentHandler._retrieveAndCheckIntentForCurrentAction(currentAction: currentAction) + + let requiresRefresh = shouldRefresh.contains(paymentMethodType) + XCTAssertEqual(apiClientMock.refreshPaymentIntentCalled, requiresRefresh, + "\(paymentMethodType.displayName) should \(requiresRefresh ? "" : "not ")hit the refresh endpoint when using a PaymentIntent") + XCTAssertEqual(apiClientMock.retrievePaymentIntentCalled, !requiresRefresh, + "\(paymentMethodType.displayName) should \(requiresRefresh ? "" : "not ")hit the retrieve endpoint when using a PaymentIntent") + } + } + + func testSetupIntentShouldHitRefreshEndpoint() { + let shouldRefresh: [STPPaymentMethodType] = [.cashApp] + + for paymentMethodType in STPPaymentMethodType.allCases { + let paymentMethodDict: [AnyHashable: Any] = [ + "id": "pm_test", + "type": paymentMethodType.identifier, + ] + + let setupIntent = STPFixtures.setupIntent(paymentMethodTypes: [paymentMethodType.identifier], + status: .requiresAction, + paymentMethod: paymentMethodDict, + nextAction: .useStripeSDK) + + let apiClientMock = STPAPIClientMock() + let currentAction = STPPaymentHandlerSetupIntentActionParams.makeTestable(apiClient: apiClientMock, + paymentMethodTypes: [paymentMethodType.identifier], + setupIntent: setupIntent) + + let paymentHandler = STPPaymentHandler(apiClient: apiClientMock) + paymentHandler._retrieveAndCheckIntentForCurrentAction(currentAction: currentAction) + + let requiresRefresh = shouldRefresh.contains(paymentMethodType) + XCTAssertEqual(apiClientMock.refreshSetupIntentCalled, requiresRefresh, + "\(paymentMethodType.displayName) should \(requiresRefresh ? "" : "not ")hit the refresh endpoint when using a SetupIntent") + XCTAssertEqual(apiClientMock.retrieveSetupIntentCalled, !requiresRefresh, + "\(paymentMethodType.displayName) should \(requiresRefresh ? "" : "not ")hit the retrieve endpoint when using a SetupIntent") + } + } +} + +// MARK: - Mocks and helpers + +class STPAPIClientMock: STPAPIClient { + var refreshPaymentIntentCalled = false + var refreshSetupIntentCalled = false + var retrievePaymentIntentCalled = false + var retrieveSetupIntentCalled = false + + override func refreshPaymentIntent(withClientSecret secret: String, completion: @escaping STPPaymentIntentCompletionBlock) { + refreshPaymentIntentCalled = true + } + + override func refreshSetupIntent(withClientSecret secret: String, completion: @escaping STPSetupIntentCompletionBlock) { + refreshSetupIntentCalled = true + } + + override func retrievePaymentIntent( + withClientSecret secret: String, + expand: [String]?, + completion: @escaping STPPaymentIntentCompletionBlock + ) { + retrievePaymentIntentCalled = true + } + + override func retrieveSetupIntent( + withClientSecret secret: String, + expand: [String]?, + completion: @escaping STPSetupIntentCompletionBlock + ) { + retrieveSetupIntentCalled = true + } +} + +extension STPPaymentHandlerPaymentIntentActionParams { + static func makeTestable(apiClient: STPAPIClient, + paymentMethodTypes: [String], + paymentIntent: STPPaymentIntent) -> STPPaymentHandlerPaymentIntentActionParams { + + return .init(apiClient: apiClient, + authenticationContext: STPAuthenticationContextMock(), + threeDSCustomizationSettings: .init(), + paymentIntent: paymentIntent, + returnURL: nil) { _, _, _ in + // no-op + } + } +} + +extension STPPaymentHandlerSetupIntentActionParams { + static func makeTestable(apiClient: STPAPIClient, + paymentMethodTypes: [String], + setupIntent: STPSetupIntent) -> STPPaymentHandlerSetupIntentActionParams { + + return .init(apiClient: apiClient, + authenticationContext: STPAuthenticationContextMock(), + threeDSCustomizationSettings: .init(), + setupIntent: setupIntent, + returnURL: nil) { _, _, _ in + // no-op + } + } +} + +class STPAuthenticationContextMock: NSObject, STPAuthenticationContext { + func authenticationPresentingViewController() -> UIViewController { + return UIViewController() + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentHandlerStubbedMockedFilesTests.swift b/Stripe/StripeiOSTests/STPPaymentHandlerStubbedMockedFilesTests.swift new file mode 100644 index 00000000..e8577932 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentHandlerStubbedMockedFilesTests.swift @@ -0,0 +1,453 @@ +// +// STPPaymentHandlerStubbedMockedFilesTests.swift +// StripeiOS Tests +// +// Copyright © 2022 Stripe, Inc. All rights reserved. +// +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentHandlerStubbedMockedFilesTests: APIStubbedTestCase, STPAuthenticationContext { + func testCallConfirmAfterpay_Redirect_thenCanceled() { + let nextActionData = """ + { + "redirect_to_url": { + "return_url": "payments-example://stripe-redirect", + "url": "https://hooks.stripe.com/afterpay_clearpay/acct_123/pa_nonce_321/redirect" + }, + "type": "redirect_to_url" + } + """ + let paymentMethod = """ + { + "id": "pm_123123123123123", + "object": "payment_method", + "afterpay_clearpay": {}, + "billing_details": { + "address": { + "city": "San Francisco", + "country": "AT", + "line1": "510 Townsend St.", + "line2": "", + "postal_code": "94102", + "state": null + }, + "email": "foo@bar.com", + "name": "Jane Doe", + "phone": null + }, + "created": 1658187899, + "customer": null, + "livemode": false, + "type": "afterpay_clearpay" + } + """ + let paymentHandler = stubbedPaymentHandler(formSpecProvider: formSpecProvider()) + stubConfirm( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethod, + "": "\"requires_action\"", + ] + ) + } + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_123456_secret_654321") + paymentIntentParams.returnURL = "payments-example://stripe-redirect" + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + afterpayClearpay: STPPaymentMethodAfterpayClearpayParams(), + billingDetails: STPPaymentMethodBillingDetails(), + metadata: nil + ) + paymentIntentParams.paymentMethodParams?.afterpayClearpay = + STPPaymentMethodAfterpayClearpayParams() + let didRedirect = expectation(description: "didRedirect") + paymentHandler._redirectShim = { redirectTo, returnToURL, isStandardRedirect in + XCTAssertEqual( + redirectTo.absoluteString, + "https://hooks.stripe.com/afterpay_clearpay/acct_123/pa_nonce_321/redirect" + ) + XCTAssertEqual(returnToURL?.absoluteString, "payments-example://stripe-redirect") + XCTAssert(isStandardRedirect) + didRedirect.fulfill() + } + let expectConfirmWasCanceled = expectation(description: "didCancel") + paymentHandler.confirmPayment(paymentIntentParams, with: self) { + status, + _, + _ in + if case .canceled = status { + expectConfirmWasCanceled.fulfill() + } + } + guard XCTWaiter.wait(for: [didRedirect], timeout: 2.0) != .timedOut else { + XCTFail("Unable to redirect") + return + } + + // Test the cancel case + stubRetrievePaymentIntent( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethod, + "": "\"requires_action\"", + ] + ) + } + ) + paymentHandler._retrieveAndCheckIntentForCurrentAction() + wait(for: [expectConfirmWasCanceled], timeout: 2.0) + } + + func testCallConfirmAfterpay_Redirect_thenSucceeded() { + let nextActionData = """ + { + "redirect_to_url": { + "return_url": "payments-example://stripe-redirect", + "url": "https://hooks.stripe.com/afterpay_clearpay/acct_123/pa_nonce_321/redirect" + }, + "type": "redirect_to_url" + } + """ + let paymentMethodData = """ + { + "id": "pm_123123123123123", + "object": "payment_method", + "afterpay_clearpay": {}, + "billing_details": { + "address": { + "city": "San Francisco", + "country": "AT", + "line1": "510 Townsend St.", + "line2": "", + "postal_code": "94102", + "state": null + }, + "email": "foo@bar.com", + "name": "Jane Doe", + "phone": null + }, + "created": 1658187899, + "customer": null, + "livemode": false, + "type": "afterpay_clearpay" + } + """ + let paymentHandler = stubbedPaymentHandler(formSpecProvider: formSpecProvider()) + stubConfirm( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethodData, + "": "\"requires_action\"", + ] + ) + } + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_123456_secret_654321") + paymentIntentParams.returnURL = "payments-example://stripe-redirect" + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + afterpayClearpay: STPPaymentMethodAfterpayClearpayParams(), + billingDetails: STPPaymentMethodBillingDetails(), + metadata: nil + ) + paymentIntentParams.paymentMethodParams?.afterpayClearpay = + STPPaymentMethodAfterpayClearpayParams() + let didRedirect = expectation(description: "didRedirect") + paymentHandler._redirectShim = { redirectTo, returnToURL, isStandardRedirect in + XCTAssertEqual( + redirectTo.absoluteString, + "https://hooks.stripe.com/afterpay_clearpay/acct_123/pa_nonce_321/redirect" + ) + XCTAssertEqual(returnToURL?.absoluteString, "payments-example://stripe-redirect") + XCTAssert(isStandardRedirect) + didRedirect.fulfill() + } + confirmPaymentWithSucceed(nextActionData: nextActionData, + paymentMethodData: paymentMethodData, + didRedirect: didRedirect, + paymentHandler: paymentHandler, + paymentIntentParams: paymentIntentParams) + } + + func testCallConfirmAfterpay_Redirect() { + let formSpecProvider = formSpecProvider() + let paymentHandler = stubbedPaymentHandler(formSpecProvider: formSpecProvider) + + // Override it with a spec that doesn't define a next action so that we force the SDK to default behavior + let updatedSpecJson = + """ + [{ + "type": "affirm", + "async": false, + "fields": [ + { + "type": "name" + } + ] + }] + """.data(using: .utf8)! + let formSpec = try! JSONSerialization.jsonObject(with: updatedSpecJson) as! [NSDictionary] + XCTAssert(formSpecProvider.loadFrom(formSpec)) + guard formSpecProvider.formSpec(for: "affirm") != nil else { + XCTFail() + return + } + + let nextActionData = """ + { + "redirect_to_url": { + "return_url": "payments-example://stripe-redirect", + "url": "https://hooks.stripe.com/affirm/acct_123/pa_nonce_321/redirect" + }, + "type": "redirect_to_url" + } + """ + let paymentMethodData = """ + { + "id": "pm_123123123123123", + "object": "payment_method", + "afterpay_clearpay": {}, + "billing_details": { + "address": { + "city": "San Francisco", + "country": "AT", + "line1": "510 Townsend St.", + "line2": "", + "postal_code": "94102", + "state": null + }, + "email": "foo@bar.com", + "name": "Jane Doe", + "phone": null + }, + "created": 1658187899, + "customer": null, + "livemode": false, + "type": "afterpay_clearpay" + } + """ + stubConfirm( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethodData, + "": "\"requires_action\"", + ] + ) + } + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_123456_secret_654321") + paymentIntentParams.returnURL = "payments-example://stripe-redirect" + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + afterpayClearpay: STPPaymentMethodAfterpayClearpayParams(), + billingDetails: STPPaymentMethodBillingDetails(), + metadata: nil + ) + paymentIntentParams.paymentMethodParams?.afterpayClearpay = + STPPaymentMethodAfterpayClearpayParams() + let didRedirect = expectation(description: "didRedirect") + paymentHandler._redirectShim = { redirectTo, returnToURL, isStandardRedirect in + XCTAssertEqual( + redirectTo.absoluteString, + "https://hooks.stripe.com/affirm/acct_123/pa_nonce_321/redirect" + ) + XCTAssertEqual(returnToURL?.absoluteString, "payments-example://stripe-redirect") + XCTAssert(isStandardRedirect) + didRedirect.fulfill() + } + confirmPaymentWithSucceed(nextActionData: nextActionData, + paymentMethodData: paymentMethodData, + didRedirect: didRedirect, + paymentHandler: paymentHandler, + paymentIntentParams: paymentIntentParams) + } + + func testCallConfirmBlikSucceeds() { + let nextActionData = """ + { + "type": "blik_authorize" + } + """ + let paymentMethodData = """ + { + "id": "pm_123123123123123", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "blik": {}, + "created": 1658187899, + "customer": null, + "livemode": false, + "type": "blik" + } + """ + let paymentHandler = stubbedPaymentHandler(formSpecProvider: formSpecProvider()) + let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_123456_secret_654321") + paymentIntentParams.returnURL = "payments-example://stripe-redirect" + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + blik: STPPaymentMethodBLIKParams(), + billingDetails: nil, + metadata: nil + ) + + stubRetrievePaymentIntent( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethodData, + "": "\"requires_action\"", + ] + ) + } + ) + + let expectConfirmSucceeded = expectation(description: "didSucceed") + paymentHandler.confirmPayment( + paymentIntentParams, + with: self) { status, _, _ in + if case .succeeded = status { + expectConfirmSucceeded.fulfill() + } + } + waitForExpectations(timeout: 2.0) + } + + private func confirmPaymentWithSucceed( + nextActionData: String, + paymentMethodData: String, + didRedirect: XCTestExpectation, + paymentHandler: STPPaymentHandler, + paymentIntentParams: STPPaymentIntentParams + ) { + let expectConfirmSucceeded = expectation(description: "didSucceed") + paymentHandler.confirmPayment(paymentIntentParams, with: self) { + status, + _, + _ in + if case .succeeded = status { + expectConfirmSucceeded.fulfill() + } + } + + guard XCTWaiter.wait(for: [didRedirect], timeout: 2.0) != .timedOut else { + XCTFail("Unable to redirect") + return + } + + // Test status as succeeded + stubRetrievePaymentIntent( + fileMock: .paymentIntentResponse, + responseCallback: { data in + self.replaceData( + data: data, + variables: [ + "": nextActionData, + "": paymentMethodData, + "": "\"succeeded\"", + ] + ) + } + ) + paymentHandler._retrieveAndCheckIntentForCurrentAction() + wait(for: [expectConfirmSucceeded], timeout: 2.0) + } + + private func formSpecProvider() -> FormSpecProvider { + let expectation = expectation(description: "Load Specs") + let formSpecProvider = FormSpecProvider() + formSpecProvider.load { _ in + expectation.fulfill() + } + wait(for: [expectation], timeout: 5.0) + return formSpecProvider + } + + private func stubbedPaymentHandler(formSpecProvider: FormSpecProvider) -> STPPaymentHandler { + return STPPaymentHandler(apiClient: stubbedAPIClient()) + } + + private func replaceData(data: Data, variables: [String: String]) -> Data { + var template = String(data: data, encoding: .utf8)! + for (templateKey, templateValue) in variables { + let translated = template.replacingOccurrences(of: templateKey, with: templateValue) + template = translated + } + return template.data(using: .utf8)! + } + + private func stubConfirm(fileMock: FileMock, responseCallback: ((Data) -> Data)? = nil) { + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/confirm") ?? false + } response: { _ in + let mockResponseData = try! fileMock.data() + let data = responseCallback?(mockResponseData) ?? mockResponseData + return HTTPStubsResponse(data: data, statusCode: 200, headers: nil) + } + } + private func stubRetrievePaymentIntent( + fileMock: FileMock, + responseCallback: ((Data) -> Data)? = nil + ) { + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/payment_intents") ?? false + } response: { _ in + let mockResponseData = try! fileMock.data() + let data = responseCallback?(mockResponseData) ?? mockResponseData + return HTTPStubsResponse(data: data, statusCode: 200, headers: nil) + } + } +} +extension STPPaymentHandlerStubbedMockedFilesTests { + func authenticationPresentingViewController() -> UIViewController { + return UIViewController() + } +} + +public class ClassForBundle {} +@_spi(STP) public enum FileMock: String, MockData { + public typealias ResponseType = StripeFile + public var bundle: Bundle { return Bundle(for: ClassForBundle.self) } + + case paymentIntentResponse = "MockFiles/paymentIntentResponse" +} diff --git a/Stripe/StripeiOSTests/STPPaymentHandlerTests.swift b/Stripe/StripeiOSTests/STPPaymentHandlerTests.swift new file mode 100644 index 00000000..48c7bfcb --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentHandlerTests.swift @@ -0,0 +1,287 @@ +// +// STPPaymentHandlerTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/8/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import Stripe3DS2 +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentHandlerStubbedTests: STPNetworkStubbingTestCase { + override func setUp() { + self.recordingMode = false + super.setUp() + } + + func testCanPresentErrorsAreReported() { + let createPaymentIntentExpectation = expectation( + description: "createPaymentIntentExpectation" + ) + var retrievedClientSecret: String? + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { + (createdPIClientSecret, _) in + if let createdPIClientSecret = createdPIClientSecret { + retrievedClientSecret = createdPIClientSecret + createPaymentIntentExpectation.fulfill() + } else { + XCTFail() + } + } + wait(for: [createPaymentIntentExpectation], timeout: 8) // STPTestingNetworkRequestTimeout + guard let clientSecret = retrievedClientSecret, + let currentYear = Calendar.current.dateComponents([.year], from: Date()).year + else { + XCTFail() + return + } + + let expiryYear = NSNumber(value: currentYear + 2) + let expiryMonth = NSNumber(1) + + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4000000000003220" + cardParams.expYear = expiryYear + cardParams.expMonth = expiryMonth + cardParams.cvc = "123" + + let address = STPPaymentMethodAddress() + address.postalCode = "12345" + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = address + + let paymentMethodParams = STPPaymentMethodParams.paramsWith( + card: cardParams, + billingDetails: billingDetails, + metadata: nil + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret) + paymentIntentParams.paymentMethodParams = paymentMethodParams + + // STPTestingDefaultPublishableKey + STPAPIClient.shared.publishableKey = "pk_test_ErsyMEOTudSjQR8hh0VrQr5X008sBXGOu6" + + let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation") + STPPaymentHandler.shared().checkCanPresentInTest = true + let analyticsClient = STPAnalyticsClient() + STPPaymentHandler.sharedHandler.analyticsClient = analyticsClient + STPPaymentHandler.shared().confirmPayment(paymentIntentParams, with: self) { + (status, paymentIntent, error) in + let firstAnalytic = analyticsClient._testLogHistory.first + XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue) + XCTAssertEqual(firstAnalytic?["intent_id"] as? String, paymentIntentParams.stripeId) + XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card") + let lastAnalytic = analyticsClient._testLogHistory.last + XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue) + XCTAssertEqual(lastAnalytic?["intent_id"] as? String, paymentIntentParams.stripeId) + XCTAssertEqual(lastAnalytic?["status"] as? String, "failed") + XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card") + XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain") + XCTAssertEqual(lastAnalytic?["error_code"] as? String, "requiresAuthenticationContextErrorCode") + XCTAssertEqual(lastAnalytic?[jsonDict: "error_details"]?["com.stripe.lib:ErrorMessageKey"] as? String, "authenticationPresentingViewController is not in the window hierarchy. You should probably return the top-most view controller instead.") + XCTAssertTrue(status == .failed) + XCTAssertNotNil(paymentIntent) + XCTAssertNotNil(error) + XCTAssertEqual( + error?.userInfo[STPError.errorMessageKey] as? String, + "authenticationPresentingViewController is not in the window hierarchy. You should probably return the top-most view controller instead." + ) + paymentHandlerExpectation.fulfill() + } + // 2*STPTestingNetworkRequestTimeout payment handler needs to make an ares for this + // test in addition to fetching the payment intent + wait(for: [paymentHandlerExpectation], timeout: 2 * 8) + } +} + +class STPPaymentHandlerTests: APIStubbedTestCase { + + func testPaymentHandlerRetriesWithBackoff() { + let oldMaxRetries = StripeAPI.maxRetries + StripeAPI.maxRetries = 1 + STPPaymentHandler.sharedHandler.apiClient = stubbedAPIClient() + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("3ds2/authenticate") ?? false + } response: { _ in + let jsonText = """ + { + "state": "challenge_required", + "livemode": "false", + "ares" : { + "dsTransID": "4e4750e7-6ab5-45a4-accf-9c668ed3b5a7", + "acsTransID": "fa695a82-a48c-455d-9566-a652058dda27", + "p_messageVersion": "1.0.5", + "acsOperatorID": "acsOperatorUL", + "sdkTransID": "D77EB83F-F317-4E29-9852-EBAAB55515B7", + "eci": "00", + "dsReferenceNumber": "3DS_LOA_DIS_PPFU_020100_00010", + "acsReferenceNumber": "3DS_LOA_ACS_PPFU_020100_00009", + "threeDSServerTransID": "fc7a39de-dc41-4b65-ba76-a322769b2efc", + "messageVersion": "2.1.0", + "authenticationValue": "AABBCCDDEEFFAABBCCDDEEFFAAA=", + "messageType": "pArs", + "transStatus": "C", + "acsChallengeMandated": "NO" + } + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("3ds2/challenge_complete") ?? false + } response: { _ in + let errorResponse = [ + "error": + [ + "message": "This is intentionally failing for this test.", + "type": "invalid_request_error", + ], + ] + return HTTPStubsResponse(jsonObject: errorResponse, statusCode: 400, headers: nil) + } + + // Stub the fetch SetupIntent request, which should be called after the failed challenge_complete + let fetchedSetupIntentExpectation = expectation(description: "Fetched SetupIntent") + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("setup_intents/seti_123") ?? false + } response: { _ in + fetchedSetupIntentExpectation.fulfill() + return HTTPStubsResponse(jsonObject: STPTestUtils.jsonNamed("SetupIntent")!, statusCode: 400, headers: nil) + } + + let paymentHandlerExpectation = expectation( + description: "paymentHandlerFinished" + ) + var inProgress = true + + // Meaningless cert, generated for this test + // Expires 3/2/2121: Apologies to future engineers! + let cert = """ + MIIBijCB9AIBATANBgkqhkiG9w0BAQUFADANMQswCQYDVQQGEwJVUzAgFw0yMTAz + MjYxODQyNDVaGA8yMTIxMDMwMjE4NDI0NVowDTELMAkGA1UEBhMCVVMwgZ8wDQYJ + KoZIhvcNAQEBBQADgY0AMIGJAoGBAL6rIW6t+8eo1exqhvYt8H1vM+TyHNNychlD + hILw745yXZQAy9ByRG3euYEydE3SFINgWBCUuwWmkNfsZUW7Uci1PBMglBFHJrE8 + 8ZvtuJgnPkqmu97a9JkyROiaqAmqoMDP95HiZG5i3a1E/QPpPyYA3VJ/El17Qqkl + aHN32qzjAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAUhxbGQ5sQMDUqFTvibU7RzqL + dTaFhdjTDBu5YeIbXXUrJSG2AydXRq7OacRksnQhvNYXimfcgfse46XQG7rKUCfj + kbazRiRxMZylTz8zbePAFcVq6zxJ+RBVrv51D+/JgbCcQ50nZiocllR0J9UL8CKZ + obaUC2OjBbSuCZwF8Ig= + """ + let rootCA = """ + MIIBkDCB+gIJAJ3pmjFOkxTXMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAYTAlVT + MB4XDTIxMDMyNjE4NDEzMVoXDTIyMDMyNjE4NDEzMVowDTELMAkGA1UEBhMCVVMw + gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKmFDGPV77Fk/wgUMwbxjQk+bpUY + cTjNBsjK3xMaUWeE17Sry6IguO1iWaXVey9YJ1Dm83PNO/5i9nHh3gmFhEJmc55T + g+0tZQigjTcs5/BfmWtrfPYIWqKvIJqkkHrIEJnwavAS5OFGyDArHLwUtsgJbDmW + tIeQg3EH/8BSWR0BAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEATY2aQvZZJLPgUr1/ + oDvRy6KZ6p7n3+jXF8DNvVOIaQRD4Ndk5NfStteIT5XvzfmD6QqpG3nlJ6Wy3oSP + 03KvO4GWIyP9cuP/QLaEmxJIYKwPrdxLkUHFfzyy8tN54xOWPxN4Up9gVN6pSdVk + KWrsPfhPs3G57wir370Q69lV/8A= + """ + let iauss = STPIntentActionUseStripeSDK( + encryptionInfo: [ + "certificate": cert, + "directory_server_id": "0000000000", + "root_certificate_authorities": [rootCA], + ], + directoryServerName: "none", + directoryServerKeyID: "none", + serverTransactionID: "none", + threeDSSourceID: "none", + publishableKeyOverride: nil, + threeDS2IntentOverride: nil, + allResponseFields: [:] + ) + let action = STPIntentAction( + type: .useStripeSDK, + redirectToURL: nil, + alipayHandleRedirect: nil, + useStripeSDK: iauss, + oxxoDisplayDetails: nil, + weChatPayRedirectToApp: nil, + boletoDisplayDetails: nil, + verifyWithMicrodeposits: nil, + cashAppRedirectToApp: nil, + payNowDisplayQrCode: nil, + konbiniDisplayDetails: nil, + promptPayDisplayQrCode: nil, + swishHandleRedirect: nil, + multibancoDisplayDetails: nil, + allResponseFields: [:] + ) + let setupIntent = STPSetupIntent( + stripeID: "test", + clientSecret: "seti_123_secret_123", + created: Date(), + customerID: nil, + stripeDescription: nil, + livemode: false, + nextAction: action, + paymentMethodID: "test", + paymentMethod: nil, + paymentMethodOptions: nil, + paymentMethodTypes: [], + status: .requiresAction, + usage: .none, + lastSetupError: nil, + allResponseFields: [:] + ) + + // We expect this request to retry a few times with exponential backoff before calling the completion handler. + STPPaymentHandler.sharedHandler._handleNextAction( + for: setupIntent, + with: self, + returnURL: nil + ) { (status, _, _) in + XCTAssertEqual(status, .failed) + inProgress = false + paymentHandlerExpectation.fulfill() + } + + let checkedStillInProgress = expectation( + description: "Checked that we're still in progress after 2s" + ) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) { + // Make sure we're still in progress after 2 seconds + // This shows that we're retrying the 3DS2 request a few times + // while applying an appropriate amount of backoff. + XCTAssertEqual(inProgress, true) + checkedStillInProgress.fulfill() + } + + wait(for: [paymentHandlerExpectation, checkedStillInProgress, fetchedSetupIntentExpectation], timeout: 60) + STPPaymentHandler.sharedHandler.apiClient = STPAPIClient.shared + StripeAPI.maxRetries = oldMaxRetries + } +} + +extension STPPaymentHandlerTests: STPAuthenticationContext { + func authenticationPresentingViewController() -> UIViewController { + return UIViewController() + } +} + +extension STPPaymentHandlerStubbedTests: STPAuthenticationContext { + func authenticationPresentingViewController() -> UIViewController { + return UIViewController() + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentIntentEnumsTest.swift b/Stripe/StripeiOSTests/STPPaymentIntentEnumsTest.swift new file mode 100644 index 00000000..32ed6d52 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentIntentEnumsTest.swift @@ -0,0 +1,220 @@ +// +// STPPaymentIntentEnumsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 9/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentIntentEnumsTest: XCTestCase { + + func textStatusFromString() { + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "requires_payment_method"), + .requiresPaymentMethod + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "REQUIRES_PAYMENT_METHOD"), + .requiresPaymentMethod + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "requires_confirmation"), + .requiresConfirmation + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "REQUIRES_CONFIRMATION"), + .requiresConfirmation + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "requires_action"), + .requiresAction + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "REQUIRES_ACTION"), + .requiresAction + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "processing"), + .processing + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "PROCESSING"), + .processing + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "succeeded"), + .succeeded + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "SUCCEEDED"), + .succeeded + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "requires_capture"), + .requiresCapture + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "REQUIRES_CAPTURE"), + .requiresCapture + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "canceled"), + .canceled + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "CANCELED"), + .canceled + ) + + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "garbage"), + .unknown + ) + XCTAssertEqual( + STPPaymentIntentStatus.status(from: "GARBAGE"), + .unknown + ) + } + + func testStringFromStatus() { + + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .requiresPaymentMethod), + "requires_payment_method" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .requiresConfirmation), + "requires_confirmation" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .requiresAction), + "requires_action" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .processing), + "processing" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .succeeded), + "succeeded" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .requiresCapture), + "requires_capture" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .canceled), + "canceled" + ) + XCTAssertEqual( + STPPaymentIntentStatus.string(from: .unknown), + "unknown" + ) + } + + func testCaptureMethodFromString() { + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "manual"), + .manual + ) + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "MANUAL"), + .manual + ) + + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "automatic"), + .automatic + ) + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "AUTOMATIC"), + .automatic + ) + + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "garbage"), + .unknown + ) + XCTAssertEqual( + STPPaymentIntentCaptureMethod.captureMethod(from: "GARBAGE"), + .unknown + ) + } + + func testConfirmationMethodFromString() { + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "automatic"), + .automatic + ) + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "AUTOMATIC"), + .automatic + ) + + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "manual"), + .manual + ) + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "MANUAL"), + .manual + ) + + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "garbage"), + .unknown + ) + XCTAssertEqual( + STPPaymentIntentConfirmationMethod.confirmationMethod(from: "GARBAGE"), + .unknown + ) + } + + func testSetupFutureUsageFromString() { + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "on_session"), + .onSession + ) + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "ON_SESSION"), + .onSession + ) + + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "off_session"), + .offSession + ) + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "OFF_SESSION"), + .offSession + ) + + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "garbage"), + .unknown + ) + XCTAssertEqual( + STPPaymentIntentSetupFutureUsage(string: "GARBAGE"), + .unknown + ) + } + + func testStringConvertible() { + XCTAssertEqual(String(describing: STPPaymentIntentStatus.requiresAction), "requiresAction") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentIntentFunctionalTest.swift b/Stripe/StripeiOSTests/STPPaymentIntentFunctionalTest.swift new file mode 100644 index 00000000..8c04be34 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentIntentFunctionalTest.swift @@ -0,0 +1,1529 @@ +// +// STPPaymentIntentFunctionalTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/2/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentIntentFunctionalTest: STPNetworkStubbingTestCase { + func testCreatePaymentIntentWithTestingServer() { + let expectation = self.expectation(description: "PaymentIntent create.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: nil) { clientSecret, error in + XCTAssertNotNil(clientSecret) + XCTAssertNil(error) + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreatePaymentIntentWithInvalidCurrency() { + let expectation = self.expectation(description: "PaymentIntent create.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: [ + "payment_method_types": ["bancontact"], + ]) { clientSecret, error in + XCTAssertNil(clientSecret) + XCTAssertNotNil(error) + let errorString = (error! as NSError).userInfo[STPError.errorMessageKey] as! String + XCTAssertTrue(errorString.hasPrefix("Error creating PaymentIntent: The currency provided (usd) is invalid. Payments with bancontact support the following currencies: eur.")) + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRetrievePreviousCreatedPaymentIntent() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent retrieve") + + client.retrievePaymentIntent( + withClientSecret: "pi_1GGCGfFY0qyl6XeWbSAsh2hn_secret_jbhwsI0DGWhKreJs3CCrluUGe") { paymentIntent, error in + XCTAssertNil(error) + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, "pi_1GGCGfFY0qyl6XeWbSAsh2hn") + XCTAssertEqual(paymentIntent?.amount, 100) + XCTAssertEqual(paymentIntent?.currency, "usd") + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNil(paymentIntent?.sourceId) + XCTAssertNil(paymentIntent?.paymentMethodId) + XCTAssertEqual(paymentIntent?.status, .canceled) + XCTAssertEqual(paymentIntent?.setupFutureUsage, STPPaymentIntentSetupFutureUsage.none) + XCTAssertNil(paymentIntent?.perform(NSSelectorFromString("nextSourceAction"))) + // #pragma clang diagnostic pop + XCTAssertNil(paymentIntent!.nextAction) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRetrieveWithWrongSecret() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent retrieve") + + client.retrievePaymentIntent( + withClientSecret: "pi_1GGCGfFY0qyl6XeWbSAsh2hn_secret_bad-secret") { paymentIntent, error in + XCTAssertNil(paymentIntent) + + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + XCTAssertEqual((error as NSError?)?.code, STPErrorCode.invalidRequestError.rawValue) + XCTAssertEqual( + (error as NSError?)?.userInfo[STPError.errorParameterKey] as! String, + "clientSecret") + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRetrieveMismatchedPublishableKey() { + // Given an API Client with a publishable key for a test account A... + let client = STPAPIClient(publishableKey: "pk_test_51JtgfQKG6vc7r7YCU0qQNOkDaaHrEgeHgGKrJMNfuWwaKgXMLzPUA1f8ZlCNPonIROLOnzpUnJK1C1xFH3M3Mz8X00Q6O4GfUt") + let expectation = self.expectation(description: "Payment Intent retrieve") + + // ...retrieving a PI attached to a *different* account + client.retrievePaymentIntent( + withClientSecret: "pi_1GGCGfFY0qyl6XeWbSAsh2hn_secret_jbhwsI0DGWhKreJs3CCrluUGe") { paymentIntent, error in + // ...should fail. + XCTAssertNil(paymentIntent) + + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + XCTAssertEqual((error as NSError?)?.code, STPErrorCode.invalidRequestError.rawValue) + XCTAssertEqual( + (error as NSError?)?.userInfo[STPError.errorParameterKey] as! String, + "intent") + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmCanceledPaymentIntentFails() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: "pi_1GGCGfFY0qyl6XeWbSAsh2hn_secret_jbhwsI0DGWhKreJs3CCrluUGe") + params.sourceParams = cardSourceParams() + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(paymentIntent) + + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError?)?.domain, STPError.stripeDomain) + XCTAssertEqual((error as NSError?)?.code, STPErrorCode.invalidRequestError.rawValue) + let errorString = (error! as NSError).userInfo[STPError.errorMessageKey] as! String + XCTAssertTrue(errorString.hasPrefix("This PaymentIntent's source could not be updated because it has a status of canceled. You may only update the source of a PaymentIntent with one of the following statuses: requires_payment_method, requires_confirmation, requires_action."), + "Expected error message to complain about status being canceled. Actual msg: \(errorString)" + ) + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmPaymentIntentWith3DSCardSucceeds() { + + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + params.sourceParams = cardSourceParams() + // returnURL must be passed in while confirming (not creation time) + params.returnURL = "example-app-scheme://authorized" + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, params.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + + // sourceParams is the 3DS-required test card + XCTAssertEqual(paymentIntent?.status, .requiresAction) + + // STPRedirectContext is relying on receiving returnURL + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + // Test deprecated property still works too + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertNotNil(paymentIntent?.nextSourceAction?.authorizeWithURL?.returnURL) + XCTAssertEqual( + paymentIntent?.nextSourceAction?.authorizeWithURL?.returnURL, + URL(string: "example-app-scheme://authorized")) + // #pragma clang diagnostic pop + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmPaymentIntentWith3DSCardPaymentMethodSucceeds() { + + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4000000000003220" + cardParams.expMonth = NSNumber(value: 7) + cardParams.expYear = NSNumber(value: Calendar.current.component(.year, from: Date()) + 5) + + let billingDetails = STPPaymentMethodBillingDetails() + + params.paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil) + // returnURL must be passed in while confirming (not creation time) + params.returnURL = "example-app-scheme://authorized" + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, params.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // sourceParams is the 3DS-required test card + XCTAssertEqual(paymentIntent?.status, .requiresAction) + + // STPRedirectContext is relying on receiving returnURL + + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + // Going to log all the fields so that you, the developer manually running this test, can inspect them + if let allResponseFields = paymentIntent?.allResponseFields { + print("Confirmed PaymentIntent: \(allResponseFields)") + } + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmPaymentIntentWithShippingDetailsSucceeds() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4242424242424242" + cardParams.expMonth = NSNumber(value: 7) + cardParams.expYear = NSNumber(value: Calendar.current.component(.year, from: Date()) + 5) + + let billingDetails = STPPaymentMethodBillingDetails() + + params.paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil) + + let addressParams = STPPaymentIntentShippingDetailsAddressParams(line1: "123 Main St") + addressParams.line2 = "Apt 2" + addressParams.city = "San Francisco" + addressParams.state = "CA" + addressParams.country = "US" + addressParams.postalCode = "94106" + params.shipping = STPPaymentIntentShippingDetailsParams(address: addressParams, name: "Jane") + params.shipping?.carrier = "UPS" + params.shipping?.phone = "555-555-5555" + params.shipping?.trackingNumber = "123abc" + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, params.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // Address + XCTAssertEqual(paymentIntent?.shipping!.address!.line1, "123 Main St") + XCTAssertEqual(paymentIntent?.shipping!.address!.line2, "Apt 2") + XCTAssertEqual(paymentIntent?.shipping!.address!.city, "San Francisco") + XCTAssertEqual(paymentIntent?.shipping!.address!.state, "CA") + XCTAssertEqual(paymentIntent?.shipping!.address!.country, "US") + XCTAssertEqual(paymentIntent?.shipping!.address!.postalCode, "94106") + + XCTAssertEqual(paymentIntent?.shipping!.name, "Jane") + XCTAssertEqual(paymentIntent?.shipping!.carrier, "UPS") + XCTAssertEqual(paymentIntent?.shipping!.phone, "555-555-5555") + XCTAssertEqual(paymentIntent?.shipping!.trackingNumber, "123abc") + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmCardWithoutNetworkParam() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4242424242424242" + cardParams.expMonth = NSNumber(value: 7) + cardParams.expYear = NSNumber(value: Calendar.current.component(.year, from: Date()) + 5) + + let billingDetails = STPPaymentMethodBillingDetails() + + params.paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil) + + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, params.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .succeeded) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmCardWithNetworkParam() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4242424242424242" + cardParams.expMonth = NSNumber(value: 7) + cardParams.expYear = NSNumber(value: Calendar.current.component(.year, from: Date()) + 5) + + let billingDetails = STPPaymentMethodBillingDetails() + + params.paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil) + + let cardOptions = STPConfirmCardOptions() + cardOptions.network = "visa" + let paymentMethodOptions = STPConfirmPaymentMethodOptions() + paymentMethodOptions.cardOptions = cardOptions + params.paymentMethodOptions = paymentMethodOptions + + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, params.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .succeeded) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmCardWithInvalidNetworkParam() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let params = STPPaymentIntentParams(clientSecret: clientSecret!) + let cardParams = STPPaymentMethodCardParams() + cardParams.number = "4242424242424242" + cardParams.expMonth = NSNumber(value: 7) + cardParams.expYear = NSNumber(value: Calendar.current.component(.year, from: Date()) + 5) + + let billingDetails = STPPaymentMethodBillingDetails() + + params.paymentMethodParams = STPPaymentMethodParams( + card: cardParams, + billingDetails: billingDetails, + metadata: nil) + + let cardOptions = STPConfirmCardOptions() + cardOptions.network = "fake_network" + let paymentMethodOptions = STPConfirmPaymentMethodOptions() + paymentMethodOptions.cardOptions = cardOptions + params.paymentMethodOptions = paymentMethodOptions + + client.confirmPaymentIntent( + with: params) { paymentIntent, error in + XCTAssertNotNil(error, "Confirming with invalid network should result in an error") + + XCTAssertNil(paymentIntent) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - AU BECS Debit + + func testConfirmAUBECSDebitPaymentIntent() { + + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "currency": "aud", + "amount": NSNumber(value: 2000), + "payment_method_types": ["au_becs_debit"], + ], + account: "au") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let becsParams = STPPaymentMethodAUBECSDebitParams() + becsParams.bsbNumber = "000000" // Stripe test bank + becsParams.accountNumber = "000123456" // test account + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + billingDetails.email = "jrosen@example.com" + + let params = STPPaymentMethodParams( + aubecsDebit: becsParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = params + + let client = STPAPIClient(publishableKey: STPTestingAUPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // AU BECS Debit should be in Processing + XCTAssertEqual(paymentIntent?.status, .processing) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Przelewy24 + + func testConfirmPaymentIntentWithPrzelewy24() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["p24"], + "currency": "eur", + ]) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let przelewy24Params = STPPaymentMethodPrzelewy24Params() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.email = "email@email.com" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + przelewy24: przelewy24Params, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + paymentIntentParams.returnURL = "example-app-scheme://authorized" + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // Przelewy24 requires a redirect + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Bancontact + + func testConfirmPaymentIntentWithBancontact() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["bancontact"], + "currency": "eur", + ]) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let bancontact = STPPaymentMethodBancontactParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + bancontact: bancontact, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + paymentIntentParams.returnURL = "example-app-scheme://authorized" + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // Bancontact requires a redirect + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - OXXO + + func testConfirmPaymentIntentWithOXXO() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["oxxo"], + "amount": NSNumber(value: 2000), + "currency": "mxn", + ], + account: "mex") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingMEXPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let oxxo = STPPaymentMethodOXXOParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + billingDetails.email = "email@email.com" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + oxxo: oxxo, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // OXXO requires display the voucher as next step + let oxxoDisplayDetails = paymentIntent!.nextAction!.allResponseFields["oxxo_display_details"] as? [AnyHashable: Any] + XCTAssertNotNil(oxxoDisplayDetails?["expires_after"]) + XCTAssertNotNil(oxxoDisplayDetails?["number"]) + XCTAssertEqual(paymentIntent?.status, .requiresAction) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - EPS + + func testConfirmPaymentIntentWithEPS() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["eps"], + "currency": "eur", + ]) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let epsParams = STPPaymentMethodEPSParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + eps: epsParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + paymentIntentParams.returnURL = "example-app-scheme://authorized" + + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // EPS requires a redirect + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Alipay + + func testConfirmAlipayPaymentIntent() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "currency": "usd", + "amount": NSNumber(value: 2000), + "payment_method_types": ["alipay"], + ]) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let params = STPPaymentMethodParams(alipay: STPPaymentMethodAlipayParams(), billingDetails: nil, metadata: nil) + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = params + paymentIntentParams.returnURL = "foo://bar" + paymentIntentParams.paymentMethodOptions = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions!.alipayOptions = STPConfirmAlipayOptions() + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .alipayHandleRedirect) + XCTAssertNotNil(paymentIntent!.nextAction) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - GrabPay + + func testConfirmPaymentIntentWithGrabPay() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["grabpay"], + "currency": "sgd", + ], + account: "sg") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingSGPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let grabpay = STPPaymentMethodGrabPayParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + grabPay: grabpay, + billingDetails: billingDetails, + metadata: nil) + paymentIntentParams.returnURL = "example-app-scheme://authorized" + + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // GrabPay requires a redirect + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertNotNil(paymentIntent!.nextAction?.redirectToURL?.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - PayPal + + func testConfirmPaymentIntentWithPayPal() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["paypal"], + "currency": "eur", + ], + account: "be") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingBEPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let payPal = STPPaymentMethodPayPalParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + payPal: payPal, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + paymentIntentParams.returnURL = "example-app-scheme://authorized" + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // PayPal requires a redirect + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertNotNil(paymentIntent!.nextAction!.redirectToURL!.returnURL) + XCTAssertEqual( + paymentIntent!.nextAction!.redirectToURL!.returnURL, + URL(string: "example-app-scheme://authorized")) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - BLIK + + func testConfirmPaymentIntentWithBLIK() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["blik"], + "currency": "pln", + "amount": NSNumber(value: 1000), + ], + account: "be") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingBEPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let blik = STPPaymentMethodBLIKParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + blik: blik, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + let options = STPConfirmPaymentMethodOptions() + options.blikOptions = STPConfirmBLIKOptions(code: "123456") + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + // Blik transitions to requires_action until the customer authorizes the transaction or 1 minute passes + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .BLIKAuthorize) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Affirm + + func testConfirmPaymentIntentWithAffirm() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["affirm"], + "currency": "usd", + "amount": NSNumber(value: 6000), + ]) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + let affirm = STPPaymentMethodAffirmParams() + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + affirm: affirm, + metadata: [ + "test_key": "test_value", + ]) + + let addressParams = STPPaymentIntentShippingDetailsAddressParams(line1: "123 Main St") + addressParams.line2 = "Apt 2" + addressParams.city = "San Francisco" + addressParams.state = "CA" + addressParams.country = "US" + addressParams.postalCode = "94106" + paymentIntentParams.shipping = STPPaymentIntentShippingDetailsParams(address: addressParams, name: "Jane Doe") + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent( + with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - MobilePay + + func testConfirmPaymentIntentWithMobilePay() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["mobilepay"], + "currency": "dkk", + "amount": NSNumber(value: 6000), + ], + account: "fr" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + mobilePay: STPPaymentMethodMobilePayParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Amazon Pay + + func testConfirmPaymentIntentWithAmazonPay() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["amazon_pay"], + "currency": "usd", + "amount": NSNumber(value: 6000), + ], + account: "us" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + amazonPay: STPPaymentMethodAmazonPayParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Alma + + func testConfirmPaymentIntentWithAlma() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["alma"], + "currency": "eur", + "amount": NSNumber(value: 6000), + ], + account: "fr" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + alma: STPPaymentMethodAlmaParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Sunbit + + func testConfirmPaymentIntentWithSunbit() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["sunbit"], + "currency": "usd", + "amount": NSNumber(value: 6000), + ], + account: "us" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + sunbit: STPPaymentMethodSunbitParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Billie + + func testConfirmPaymentIntentWithBillie() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["billie"], + "currency": "eur", + "amount": NSNumber(value: 6000), + ], + account: "de" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDEPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + billie: STPPaymentMethodBillieParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Satispay + + func testConfirmPaymentIntentWithSatispay() { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["satispay"], + "currency": "eur", + "amount": NSNumber(value: 6000), + ], + account: "it" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingITPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret!) + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + satispay: STPPaymentMethodSatispayParams(), + billingDetails: nil, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent!.livemode) + XCTAssertNotNil(paymentIntent?.paymentMethodId) + + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent!.nextAction?.type, .redirectToURL) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - Multibanco + + func testConfirmPaymentIntentWithMultibanco() throws { + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["multibanco"], + "currency": "eur", + "amount": NSNumber(value: 6000), + ], + account: "us" + ) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + clientSecret = createdClientSecret + createExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Payment Intent confirm") + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: try XCTUnwrap(clientSecret)) + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.email = "tester@example.com" + + paymentIntentParams.paymentMethodParams = STPPaymentMethodParams( + multibanco: STPPaymentMethodMultibancoParams(), + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ] + ) + + let options = STPConfirmPaymentMethodOptions() + paymentIntentParams.paymentMethodOptions = options + paymentIntentParams.returnURL = "example-app-scheme://unused" + client.confirmPaymentIntent(with: paymentIntentParams) { paymentIntent, error in + guard let paymentIntent = paymentIntent else { + XCTFail() + return + } + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + XCTAssertEqual(paymentIntent.stripeId, paymentIntentParams.stripeId) + XCTAssertFalse(paymentIntent.livemode) + XCTAssertNotNil(paymentIntent.paymentMethodId) + + XCTAssertEqual(paymentIntent.status, .requiresAction) + XCTAssertEqual(paymentIntent.nextAction?.type, .multibancoDisplayDetails) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - US Bank Account + func createAndConfirmPaymentIntentWithUSBankAccount( + paymentMethodOptions: STPConfirmUSBankAccountOptions? = nil, + completion: @escaping (String?) -> Void + ) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + var clientSecret: String? + let createPIExpectation = expectation(description: "Create PaymentIntent") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["us_bank_account"], + "currency": "usd", + "amount": 1000, + ], + account: nil + ) { intentClientSecret, error in + XCTAssertNil(error) + XCTAssertNotNil(intentClientSecret) + clientSecret = intentClientSecret + createPIExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + guard let clientSecret = clientSecret else { + XCTFail("Failed to create PaymentIntent") + return + } + + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + usBankAccountParams.accountHolderType = .individual + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let paymentMethodParams = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let paymentIntentParams = STPPaymentIntentParams(clientSecret: clientSecret) + paymentIntentParams.paymentMethodParams = paymentMethodParams + if let paymentMethodOptions = paymentMethodOptions { + let pmo = STPConfirmPaymentMethodOptions() + pmo.usBankAccountOptions = paymentMethodOptions + paymentIntentParams.paymentMethodOptions = pmo + } + + let confirmPIExpectation = expectation(description: "Confirm PaymentIntent") + client.confirmPaymentIntent(with: paymentIntentParams, expand: ["payment_method"]) { + paymentIntent, + error in + XCTAssertNil(error) + XCTAssertNotNil(paymentIntent) + XCTAssertNotNil(paymentIntent?.paymentMethod) + XCTAssertNotNil(paymentIntent?.paymentMethod?.usBankAccount) + XCTAssertEqual(paymentIntent?.paymentMethod?.usBankAccount?.last4, "6789") + XCTAssertEqual(paymentIntent?.status, .requiresAction) + XCTAssertEqual(paymentIntent?.nextAction?.type, .verifyWithMicrodeposits) + if let paymentMethodOptions = paymentMethodOptions { + XCTAssertEqual( + paymentIntent?.paymentMethodOptions?.usBankAccount?.setupFutureUsage, + paymentMethodOptions.setupFutureUsage + ) + } + confirmPIExpectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + completion(clientSecret) + } + + func testConfirmPaymentIntentWithUSBankAccount_verifyWithAmounts() { + createAndConfirmPaymentIntentWithUSBankAccount { [self] clientSecret in + guard let clientSecret = clientSecret else { + XCTFail("Failed to create PaymentIntent") + return + } + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationExpectation = expectation(description: "Verify with microdeposits") + client.verifyPaymentIntentWithMicrodeposits( + clientSecret: clientSecret, + firstAmount: 32, + secondAmount: 45 + ) { paymentIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.status, .processing) + verificationExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + } + + func testConfirmPaymentIntentWithUSBankAccount_verifyWithDescriptorCode() { + createAndConfirmPaymentIntentWithUSBankAccount { [self] clientSecret in + guard let clientSecret = clientSecret else { + XCTFail("Failed to create PaymentIntent") + return + } + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationExpectation = expectation(description: "Verify with microdeposits") + client.verifyPaymentIntentWithMicrodeposits( + clientSecret: clientSecret, + descriptorCode: "SM11AA" + ) { paymentIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentIntent) + XCTAssertEqual(paymentIntent?.status, .processing) + verificationExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + } + + func testConfirmUSBankAccountWithPaymentMethodOptions() { + createAndConfirmPaymentIntentWithUSBankAccount( + paymentMethodOptions: STPConfirmUSBankAccountOptions(setupFutureUsage: .offSession) + ) { clientSecret in + XCTAssertNotNil(clientSecret) + } + } + + // MARK: - Helpers + + func cardSourceParams() -> STPSourceParams { + let card = STPCardParams() + card.number = "4000 0000 0000 3220" // Test 3DS required card + card.expMonth = 7 + card.expYear = UInt(Calendar.current.component(.year, from: Date()) + 5) + card.currency = "usd" + + return .cardParams(withCard: card) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentIntentLastPaymentErrorTest.swift b/Stripe/StripeiOSTests/STPPaymentIntentLastPaymentErrorTest.swift new file mode 100644 index 00000000..208b9a2d --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentIntentLastPaymentErrorTest.swift @@ -0,0 +1,60 @@ +// +// STPPaymentIntentLastPaymentErrorTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 9/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentIntentLastPaymentErrorTest: XCTestCase { + + func testErrorType() { + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "api_connection_error"), + .apiConnection + ) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "API_CONNECTION_ERROR"), + .apiConnection + ) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "api_error"), .api) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "API_ERROR"), .api) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "authentication_error"), + .authentication + ) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "AUTHENTICATION_ERROR"), + .authentication + ) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "card_error"), .card) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "CARD_ERROR"), .card) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "idempotency_error"), + .idempotency + ) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "IDEMPOTENCY_ERROR"), + .idempotency + ) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "invalid_request_error"), + .invalidRequest + ) + XCTAssertEqual( + STPPaymentIntentLastPaymentErrorType(string: "INVALID_REQUEST_ERROR"), + .invalidRequest + ) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "rate_limit_error"), .rateLimit) + XCTAssertEqual(STPPaymentIntentLastPaymentErrorType(string: "RATE_LIMIT_ERROR"), .rateLimit) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentIntentParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentIntentParamsTest.swift new file mode 100644 index 00000000..ba390eaf --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentIntentParamsTest.swift @@ -0,0 +1,215 @@ +// +// STPPaymentIntentParamsTest.swift +// StripeiOS Tests +// +// Created by Daniel Jackson on 7/5/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentIntentParamsTest: XCTestCase { + func testInit() { + for params in [ + STPPaymentIntentParams(clientSecret: "secret"), + STPPaymentIntentParams(), + STPPaymentIntentParams(), + ] { + XCTAssertNotNil(params) + XCTAssertNotNil(params.clientSecret) + XCTAssertNotNil(params.additionalAPIParameters) + XCTAssertEqual(params.additionalAPIParameters.count, 0) + + XCTAssertNil(params.stripeId, "invalid secrets, no stripeId") + XCTAssertNil(params.sourceParams) + XCTAssertNil(params.sourceId) + XCTAssertNil(params.receiptEmail) + XCTAssertNil(params.perform(NSSelectorFromString("saveSourceToCustomer"))) + XCTAssertNil(params.savePaymentMethod) + XCTAssertNil(params.returnURL) + XCTAssertNil(params.setupFutureUsage) + XCTAssertNil(params.useStripeSDK) + XCTAssertNil(params.mandateData) + XCTAssertNil(params.paymentMethodOptions) + XCTAssertNil(params.shipping) + } + } + + func testDescription() { + let params = STPPaymentIntentParams() + XCTAssertNotNil(params.description) + } + + // MARK: Deprecated Property + + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + func testReturnURLRenaming() { + let params = STPPaymentIntentParams() + + XCTAssertNil(params.returnURL) + XCTAssertNil(params.perform(NSSelectorFromString("returnUrl"))) + + params.returnURL = "set via new name" + XCTAssertEqual(params.perform(NSSelectorFromString("returnUrl")).takeUnretainedValue() as? NSString, "set via new name") + + params.perform(NSSelectorFromString("setReturnUrl:"), with: "set via old name") + XCTAssertEqual(params.returnURL, "set via old name") + } + + func testSaveSourceToCustomerRenaming() { + let params = STPPaymentIntentParams() + + XCTAssertNil(params.perform(NSSelectorFromString("saveSourceToCustomer"))) + XCTAssertNil(params.savePaymentMethod) + + params.savePaymentMethod = NSNumber(value: false) + XCTAssertEqual(params.perform(NSSelectorFromString("saveSourceToCustomer")).takeUnretainedValue() as? NSNumber, NSNumber(value: false)) + + params.perform(NSSelectorFromString("setSaveSourceToCustomer:"), with: NSNumber(value: true)) + XCTAssertEqual(params.savePaymentMethod, NSNumber(value: true)) + } + + func testDefaultMandateData() { + let params = STPPaymentIntentParams() + + // no configuration should have no mandateData + XCTAssertNil(params.mandateData) + + params.paymentMethodParams = STPPaymentMethodParams() + + params.paymentMethodParams!.rawTypeString = "card" + // card type should have no default mandateData + XCTAssertNil(params.mandateData) + + for type in ["sepa_debit", "au_becs_debit", "bacs_debit"] { + params.mandateData = nil + params.paymentMethodParams!.rawTypeString = type + // Mandate-required type should have mandateData + XCTAssertNotNil(params.mandateData) + XCTAssertEqual( + params.mandateData!.customerAcceptance.onlineParams!.inferFromClient, + NSNumber(value: true) + ) + + params.mandateData = STPMandateDataParams( + customerAcceptance: STPMandateCustomerAcceptanceParams( + type: .offline, + onlineParams: nil + )! + ) + // Default behavior should not override custom setting + XCTAssertNotNil(params.mandateData) + XCTAssertNil(params.mandateData!.customerAcceptance.onlineParams) + } + } + + // #pragma clang diagnostic pop + + // MARK: STPFormEncodable Tests + func testRootObjectName() { + XCTAssertNil(STPPaymentIntentParams.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let params = STPPaymentIntentParams() + + let mapping = STPPaymentIntentParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(params.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual( + mapping.values.count, + NSSet(array: (mapping as NSDictionary).allValues).count + ) + } + + func testCopy() { + let params = STPPaymentIntentParams(clientSecret: "test_client_secret") + params.paymentMethodParams = STPPaymentMethodParams() + params.paymentMethodId = "test_payment_method_id" + params.savePaymentMethod = NSNumber(value: true) + params.returnURL = "fake://testing_only" + params.setupFutureUsage = STPPaymentIntentSetupFutureUsage( + rawValue: Int(truncating: NSNumber(value: 1)) + ) + params.useStripeSDK = NSNumber(value: true) + params.mandateData = STPMandateDataParams( + customerAcceptance: STPMandateCustomerAcceptanceParams( + type: .offline, + onlineParams: nil + )! + ) + params.paymentMethodOptions = STPConfirmPaymentMethodOptions() + params.additionalAPIParameters = [ + "other_param": "other_value" + ] + params.shipping = STPPaymentIntentShippingDetailsParams( + address: STPPaymentIntentShippingDetailsAddressParams(line1: ""), + name: "" + ) + + let paramsCopy = params.copy() as! STPPaymentIntentParams + XCTAssertEqual(params.clientSecret, paramsCopy.clientSecret) + XCTAssertEqual(params.paymentMethodId, paramsCopy.paymentMethodId) + + // assert equal, not equal objects, because this is a shallow copy + XCTAssertEqual(params.paymentMethodParams, paramsCopy.paymentMethodParams) + XCTAssertEqual(params.mandateData, paramsCopy.mandateData) + XCTAssertEqual(params.shipping, paramsCopy.shipping) + + XCTAssertEqual(params.setupFutureUsage, STPPaymentIntentSetupFutureUsage.none) + XCTAssertEqual(params.savePaymentMethod, paramsCopy.savePaymentMethod) + XCTAssertEqual(params.returnURL, paramsCopy.returnURL) + XCTAssertEqual(params.useStripeSDK, paramsCopy.useStripeSDK) + XCTAssertEqual(params.paymentMethodOptions, paramsCopy.paymentMethodOptions) + XCTAssertEqual( + params.additionalAPIParameters as NSDictionary, + paramsCopy.additionalAPIParameters as NSDictionary + ) + + } + + func testClientSecretValidation() { + XCTAssertFalse( + STPPaymentIntentParams.isClientSecretValid("pi_12345"), + "'pi_12345' is not a valid client secret." + ) + XCTAssertFalse( + STPPaymentIntentParams.isClientSecretValid("pi_12345_secret_"), + "'pi_12345_secret_' is not a valid client secret." + ) + XCTAssertFalse( + STPPaymentIntentParams.isClientSecretValid( + "pi_a1b2c3_secret_x7y8z9pi_a1b2c3_secret_x7y8z9" + ), + "'pi_a1b2c3_secret_x7y8z9pi_a1b2c3_secret_x7y8z9' is not a valid client secret." + ) + XCTAssertFalse( + STPPaymentIntentParams.isClientSecretValid("seti_a1b2c3_secret_x7y8z9"), + "'seti_a1b2c3_secret_x7y8z9' is not a valid client secret." + ) + + XCTAssertTrue( + STPPaymentIntentParams.isClientSecretValid("pi_a1b2c3_secret_x7y8z9"), + "'pi_a1b2c3_secret_x7y8z9' is a valid client secret." + ) + XCTAssertTrue( + STPPaymentIntentParams.isClientSecretValid( + "pi_1CkiBMLENEVhOs7YMtUehLau_secret_s4O8SDh7s6spSmHDw1VaYPGZA" + ), + "'pi_1CkiBMLENEVhOs7YMtUehLau_secret_s4O8SDh7s6spSmHDw1VaYPGZA' is a valid client secret." + ) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentIntentTest.swift b/Stripe/StripeiOSTests/STPPaymentIntentTest.swift new file mode 100644 index 00000000..4f5bdfbe --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentIntentTest.swift @@ -0,0 +1,162 @@ +// +// STPPaymentIntentTest.swift +// StripeiOS Tests +// +// Created by Daniel Jackson on 6/27/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentIntentTest: XCTestCase { + func testIdentifierFromSecret() { + XCTAssertEqual( + STPPaymentIntent.id(fromClientSecret: "pi_123_secret_XYZ"), + "pi_123" + ) + XCTAssertEqual( + STPPaymentIntent.id( + fromClientSecret: "pi_123_secret_RandomlyContains_secret_WhichIsFine" + ), + "pi_123" + ) + + XCTAssertNil(STPPaymentIntent.id(fromClientSecret: "")) + XCTAssertNil(STPPaymentIntent.id(fromClientSecret: "po_123_secret_HasBadPrefix")) + XCTAssertNil(STPPaymentIntent.id(fromClientSecret: "MissingSentinalForSplitting")) + } + + // MARK: - Description Tests + func testDescription() { + let paymentIntent = STPFixtures.paymentIntent() + + XCTAssertNotNil(paymentIntent) + let desc = paymentIntent.description + XCTAssertTrue(desc.contains(NSStringFromClass(type(of: paymentIntent).self))) + XCTAssertGreaterThan((desc.count), 500, "Custom description should be long") + } + + // MARK: - STPAPIResponseDecodable Tests + func testDecodedObjectFromAPIResponseRequiredFields() { + let fullJson = STPTestUtils.jsonNamed(STPTestJSONPaymentIntent) + + XCTAssertNotNil( + STPPaymentIntent.decodedObject(fromAPIResponse: fullJson), + "can decode with full json" + ) + + let requiredFields = ["id", "client_secret", "amount", "currency", "livemode", "status"] + + for field in requiredFields { + var partialJson = fullJson + + XCTAssertNotNil(partialJson?[field]) + partialJson?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentIntent.decodedObject(fromAPIResponse: partialJson)) + } + } + + func testDecodedObjectFromAPIResponseMapping() { + let paymentIntentJson = STPTestUtils.jsonNamed("PaymentIntent")! + let paymentIntent = STPPaymentIntent.decodedObject(fromAPIResponse: paymentIntentJson)! + + XCTAssertEqual(paymentIntent.stripeId, "pi_1Cl15wIl4IdHmuTbCWrpJXN6") + XCTAssertEqual( + paymentIntent.clientSecret, + "pi_1Cl15wIl4IdHmuTbCWrpJXN6_secret_EkKtQ7Sg75hLDFKqFG8DtWcaK" + ) + XCTAssertEqual(paymentIntent.amount, 2345) + XCTAssertEqual(paymentIntent.canceledAt, Date(timeIntervalSince1970: 1_530_911_045)) + XCTAssertEqual(paymentIntent.captureMethod, .manual) + XCTAssertEqual(paymentIntent.confirmationMethod, .automatic) + XCTAssertEqual(paymentIntent.created, Date(timeIntervalSince1970: 1_530_911_040)) + XCTAssertEqual(paymentIntent.currency, "usd") + XCTAssertEqual(paymentIntent.stripeDescription, "My Sample PaymentIntent") + XCTAssertFalse(paymentIntent.livemode) + XCTAssertEqual(paymentIntent.receiptEmail, "danj@example.com") + + // Deprecated: `nextSourceAction` & `authorizeWithURL` should just be aliases for `nextAction` & `redirectToURL` + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertEqual( + paymentIntent.nextAction, + paymentIntent.nextAction, + "Should be the same object." + ) + XCTAssertEqual( + paymentIntent.nextAction!.redirectToURL!, + paymentIntent.nextAction!.redirectToURL, + "Should be the same object." + ) + // #pragma clang diagnostic pop + + // nextAction + XCTAssertNotNil(paymentIntent.nextAction) + XCTAssertEqual(paymentIntent.nextAction!.type, .redirectToURL) + XCTAssertNotNil(paymentIntent.nextAction!.redirectToURL) + XCTAssertNotNil(paymentIntent.nextAction!.redirectToURL!.url) + let returnURL = paymentIntent.nextAction!.redirectToURL!.returnURL + XCTAssertNotNil(returnURL) + XCTAssertEqual(returnURL, URL(string: "payments-example://stripe-redirect")) + let url = paymentIntent.nextAction!.redirectToURL!.url + XCTAssertNotNil(url) + + XCTAssertEqual( + url, + URL( + string: + "https://hooks.stripe.com/redirect/authenticate/src_1Cl1AeIl4IdHmuTb1L7x083A?client_secret=src_client_secret_DBNwUe9qHteqJ8qQBwNWiigk" + ) + ) + XCTAssertEqual(paymentIntent.sourceId, "src_1Cl1AdIl4IdHmuTbseiDWq6m") + XCTAssertEqual(paymentIntent.status, .requiresAction) + XCTAssertEqual(paymentIntent.setupFutureUsage, .none) + + XCTAssertEqual( + paymentIntent.paymentMethodTypes, + [NSNumber(value: STPPaymentMethodType.card.rawValue)] + ) + + // lastPaymentError + + XCTAssertNotNil(paymentIntent.lastPaymentError) + XCTAssertEqual( + paymentIntent.lastPaymentError!.code, + "payment_intent_authentication_failure" + ) + XCTAssertEqual( + paymentIntent.lastPaymentError!.docURL, + "https://stripe.com/docs/error-codes#payment-intent-authentication-failure" + ) + XCTAssertEqual( + paymentIntent.lastPaymentError!.message, + "The provided PaymentMethod has failed authentication. You can provide payment_method_data or a new PaymentMethod to attempt to fulfill this PaymentIntent again." + ) + XCTAssertNotNil(paymentIntent.lastPaymentError!.paymentMethod) + XCTAssertEqual(paymentIntent.lastPaymentError!.type, .invalidRequest) + + // Shipping + XCTAssertNotNil(paymentIntent.shipping) + XCTAssertEqual(paymentIntent.shipping!.carrier, "USPS") + XCTAssertEqual(paymentIntent.shipping!.name, "Dan") + XCTAssertEqual(paymentIntent.shipping!.phone, "1-415-555-1234") + XCTAssertEqual(paymentIntent.shipping!.trackingNumber, "xyz123abc") + XCTAssertNotNil(paymentIntent.shipping!.address) + XCTAssertEqual(paymentIntent.shipping!.address!.city, "San Francisco") + XCTAssertEqual(paymentIntent.shipping!.address!.country, "USA") + XCTAssertEqual(paymentIntent.shipping!.address!.line1, "123 Main St") + XCTAssertEqual(paymentIntent.shipping!.address!.line2, "Apt 456") + XCTAssertEqual(paymentIntent.shipping!.address!.postalCode, "94107") + XCTAssertEqual(paymentIntent.shipping!.address!.state, "CA") + + XCTAssertEqual( + paymentIntent.allResponseFields as NSDictionary, + paymentIntentJson as NSDictionary + ) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitParamsTests.swift new file mode 100644 index 00000000..8541bab6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitParamsTests.swift @@ -0,0 +1,56 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodAUBECSDebitParamsTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/4/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodAUBECSDebitParamsTests: STPNetworkStubbingTestCase { + func testCreateAUBECSPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingAUPublishableKey) + let becsParams = STPPaymentMethodAUBECSDebitParams() + becsParams.bsbNumber = "000000" // Stripe test bank + becsParams.accountNumber = "000123456" // test account + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + billingDetails.email = "jrosen@example.com" + + let params = STPPaymentMethodParams( + aubecsDebit: becsParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method AU BECS Debit create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating AU BECS Debit PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create AU BECS Debit PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .AUBECSDebit, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.email, "jrosen@example.com") + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // AU BECS Debit + XCTAssertEqual(paymentMethod?.auBECSDebit!.bsbNumber, "000000") + XCTAssertEqual(paymentMethod?.auBECSDebit!.last4, "3456") + XCTAssertNotNil(paymentMethod?.auBECSDebit!.fingerprint, "Missing fingerprint") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitTests.swift new file mode 100644 index 00000000..198332ee --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAUBECSDebitTests.swift @@ -0,0 +1,85 @@ +// +// STPPaymentMethodAUBECSDebitTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/4/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +private var kAUBECSDebitPaymentIntentClientSecret = + "pi_1GaRLjF7QokQdxByYgFPQEi0_secret_z76otRQH2jjOIEQYsA9vxhuKn" +class STPPaymentMethodAUBECSDebitTests: STPNetworkStubbingTestCase { + private(set) var auBECSDebitJSON: [AnyHashable: Any]? + + func _retrieveAUBECSDebitJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let auBECSDebitJSON = auBECSDebitJSON { + completion(auBECSDebitJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingAUPublishableKey) + client.retrievePaymentIntent( + withClientSecret: kAUBECSDebitPaymentIntentClientSecret, + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + auBECSDebitJSON = paymentIntent?.paymentMethod?.auBECSDebit?.allResponseFields + completion(auBECSDebitJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + _retrieveAUBECSDebitJSON({ json in + let auBECSDebit = STPPaymentMethodAUBECSDebit.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(auBECSDebit, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + + func testFailWithoutRequired() { + var retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + _retrieveAUBECSDebitJSON({ json in + var auBECSDebitJSON = json + auBECSDebitJSON?["bsb_number"] = nil + XCTAssertNil( + STPPaymentMethodAUBECSDebit.decodedObject(fromAPIResponse: auBECSDebitJSON), + "Should not intialize with missing `bsb_number`" + ) + retrieveJSON.fulfill() + }) + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + + retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + _retrieveAUBECSDebitJSON({ json in + var auBECSDebitJSON = json + auBECSDebitJSON?["last4"] = nil + XCTAssertNil( + STPPaymentMethodAUBECSDebit.decodedObject(fromAPIResponse: auBECSDebitJSON), + "Should not intialize with missing `last4`" + ) + retrieveJSON.fulfill() + }) + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + + retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + _retrieveAUBECSDebitJSON({ json in + var auBECSDebitJSON = json + auBECSDebitJSON?["fingerprint"] = nil + XCTAssertNil( + STPPaymentMethodAUBECSDebit.decodedObject(fromAPIResponse: auBECSDebitJSON), + "Should not intialize with missing `fingerprint`" + ) + retrieveJSON.fulfill() + }) + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAddressTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodAddressTest.swift new file mode 100644 index 00000000..da7d044a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAddressTest.swift @@ -0,0 +1,34 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodAddressTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/6/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodAddressTest: XCTestCase { + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = (STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as! [AnyHashable: Any])["address"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodAddress.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPPaymentMethodAddress.decodedObject(fromAPIResponse: (STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as? [AnyHashable: Any])!["address"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = (STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as? [AnyHashable: Any])!["address"] as? [AnyHashable: Any] + let address = STPPaymentMethodAddress.decodedObject(fromAPIResponse: response) + XCTAssertEqual(address?.city, "München") + XCTAssertEqual(address?.country, "DE") + XCTAssertEqual(address?.postalCode, "80337") + XCTAssertEqual(address?.line1, "Marienplatz") + XCTAssertEqual(address?.line2, "8") + XCTAssertEqual(address?.state, "Bayern") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAffirmParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodAffirmParamsTest.swift new file mode 100644 index 00000000..4651065a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAffirmParamsTest.swift @@ -0,0 +1,43 @@ +// +// STPPaymentMethodAffirmParamsTest.swift +// StripeiOS Tests +// +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodAffirmParamsTests: STPNetworkStubbingTestCase { + + func testCreateAffirmPaymentMethod() throws { + let affirmParams = STPPaymentMethodAffirmParams() + + let params = STPPaymentMethodParams( + affirm: affirmParams, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Affirm create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .affirm, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.affirm, "The `affirm` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAffirmTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAffirmTests.swift new file mode 100644 index 00000000..f36d6e8c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAffirmTests.swift @@ -0,0 +1,47 @@ +// +// STPPaymentMethodAffirmTests.swift +// StripeiOS Tests +// +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodAffirmTests: STPNetworkStubbingTestCase { + + static let affirmPaymentIntentClientSecret = + "pi_3KUFbTFY0qyl6XeW1oDBbiQk_secret_8kdpLx37oa5WMrI2xoXThCK9s" + + func _retrieveAffirmJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.affirmPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + let affirmJson = paymentIntent?.paymentMethod?.affirm?.allResponseFields + XCTAssertNotNil(paymentIntent?.paymentMethod?.affirm) + completion(affirmJson ?? [:]) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveAffirmJSON({ json in + let affirm = STPPaymentMethodAffirm.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(affirm, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayParamsTest.swift new file mode 100644 index 00000000..fad741a1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayParamsTest.swift @@ -0,0 +1,59 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodAfterpayClearpayParamsTest.m +// StripeiOS Tests +// +// Created by Ali Riaz on 1/14/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodAfterpayClearpayParamsTest: STPNetworkStubbingTestCase { + func testCreateAfterpayClearpayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let afterpayClearpayParams = STPPaymentMethodAfterpayClearpayParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + billingDetails.email = "jrosen@example.com" + billingDetails.address = STPPaymentMethodAddress() + billingDetails.address!.line1 = "510 Townsend St." + billingDetails.address!.postalCode = "94102" + billingDetails.address!.country = "US" + + let params = STPPaymentMethodParams( + afterpayClearpay: afterpayClearpayParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method AfterpayClearpay create") + + client.createPaymentMethod(with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating AfterpayClearpay Payment Method") + XCTAssertNotNil(paymentMethod, "Failed to create AfterpayClearpay PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .afterpayClearpay, "Incorrect PaymentMethod type") + XCTAssertNil(paymentMethod?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.email, "jrosen@example.com") + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.line1, "510 Townsend St.") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.postalCode, "94102") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.country, "US") + + XCTAssertNotNil(paymentMethod?.afterpayClearpay, "") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayTest.swift new file mode 100644 index 00000000..a28dbd61 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAfterpayClearpayTest.swift @@ -0,0 +1,38 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodAfterpayClearpayTest.m +// StripeiOS Tests +// +// Created by Ali Riaz on 1/14/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodAfterpayClearpayTest: STPNetworkStubbingTestCase { + var afterpayJSON: [AnyHashable: Any]? + + func _retrieveAfterpayJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let afterpayJSON { + completion(afterpayJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent(withClientSecret: "pi_1HbSAfFY0qyl6XeWRnlezJ7K_secret_t6Ju9Z0hxOvslawK34uC1Wm2b", expand: ["payment_method"]) { paymentIntent, _ in + self.afterpayJSON = paymentIntent?.paymentMethod?.afterpayClearpay?.allResponseFields + completion(self.afterpayJSON) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch Afterpay Clearpay JSON") + _retrieveAfterpayJSON({ json in + let afterpay = STPPaymentMethodAfterpayClearpay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(afterpay, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAlmaParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAlmaParamsTests.swift new file mode 100644 index 00000000..275e334b --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAlmaParamsTests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodAlmaParamsTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 3/27/24. +// + +import Foundation +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodAlmaParamsTests: STPNetworkStubbingTestCase { + + func testCreateAlmaPaymentMethod() throws { + let almaParams = STPPaymentMethodAlmaParams() + + let params = STPPaymentMethodParams( + alma: almaParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Alma create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .alma, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.alma, "The `alma` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAlmaTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAlmaTests.swift new file mode 100644 index 00000000..aeaceae1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAlmaTests.swift @@ -0,0 +1,41 @@ +// +// STPPaymentMethodAlmaTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 3/27/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodAlmaTests: STPNetworkStubbingTestCase { + + static let almaPaymentIntentClientSecret = "pi_3Oz1AfKG6vc7r7YC0VaP6KiE_secret_SxVptpJ5PaAceAYCGetQh8FVv" + + func _retrieveAlmaJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.almaPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["alma"]) + let almaJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.alma?.allResponseFields) + completion(almaJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveAlmaJSON({ json in + let alma = STPPaymentMethodAlma.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(alma, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayParamsTests.swift new file mode 100644 index 00000000..0ecdd00d --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayParamsTests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodAmazonPayParamsTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 2/21/24. +// + +import Foundation +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodAmazonPayParamsTests: STPNetworkStubbingTestCase { + + func testCreateAmazonPayPaymentMethod() throws { + let amazonPayParams = STPPaymentMethodAmazonPayParams() + + let params = STPPaymentMethodParams( + amazonPay: amazonPayParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Amazon Pay create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .amazonPay, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.amazonPay, "The `amazonPay` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayTests.swift new file mode 100644 index 00000000..82b29857 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodAmazonPayTests.swift @@ -0,0 +1,41 @@ +// +// STPPaymentMethodAmazonPayTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 2/21/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodAmazonPayTests: STPNetworkStubbingTestCase { + + static let amazonPayPaymentIntentClientSecret = "pi_3OmQQ0FY0qyl6XeW0H4X6eI0_secret_BerPIzUf8vFy1KXG53iYvX2Zb" + + func _retrieveAmazonPayJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.amazonPayPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["amazon_pay"]) + let amazonPayJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.amazonPay?.allResponseFields) + completion(amazonPayJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveAmazonPayJSON({ json in + let amazonPay = STPPaymentMethodAmazonPay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(amazonPay, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBacsDebitTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodBacsDebitTest.swift new file mode 100644 index 00000000..ceeb58b5 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBacsDebitTest.swift @@ -0,0 +1,36 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodBacsDebitTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 1/28/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodBacsDebitTest: XCTestCase { + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let paymentMethodJSON = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodBacsDebit) + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = paymentMethodJSON?["bacs_debit"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodBacsDebit.decodedObject(fromAPIResponse: response)) + } + + let paymentMethod = STPPaymentMethod.decodedObject(fromAPIResponse: paymentMethodJSON) + XCTAssertNotNil(paymentMethod) + XCTAssertNotNil(paymentMethod?.bacsDebit) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodBacsDebit)["bacs_debit"] as? [AnyHashable: Any] + let bacs = STPPaymentMethodBacsDebit.decodedObject(fromAPIResponse: response) + XCTAssertEqual(bacs?.fingerprint, "9eMbmctOrd8i7DYa") + XCTAssertEqual(bacs?.last4, "2345") + XCTAssertEqual(bacs?.sortCode, "108800") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBancontactParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBancontactParamsTests.swift new file mode 100644 index 00000000..01b705e4 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBancontactParamsTests.swift @@ -0,0 +1,50 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodBancontactParamsTests.m +// StripeiOS Tests +// +// Created by Vineet Shah on 4/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodBancontactParamsTests: STPNetworkStubbingTestCase { + func testCreateBancontactPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let bancontactParams = STPPaymentMethodBancontactParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + + let params = STPPaymentMethodParams( + bancontact: bancontactParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method Bancontact create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating Bancontact PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create Bancontact PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .bancontact, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jane Doe") + + // Bancontact Details + XCTAssertNotNil(paymentMethod?.bancontact, "Missing Bancontact") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBancontactTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBancontactTests.swift new file mode 100644 index 00000000..50be8c06 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBancontactTests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodBancontactTests.swift +// StripeiOS Tests +// +// Created by Vineet Shah on 4/29/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodBancontactTests: STPNetworkStubbingTestCase { + private(set) var bancontactJSON: [AnyHashable: Any]? + + func _retrieveBancontactJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let bancontactJSON = bancontactJSON { + completion(bancontactJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1GdPnbFY0qyl6XeW8Ezvxe87_secret_Fxi2EZBQ0nInHumvvezcTRWF4", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + bancontactJSON = paymentIntent?.paymentMethod?.bancontact?.allResponseFields + completion(bancontactJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let expectation = self.expectation(description: "Retrieve payment intent") + _retrieveBancontactJSON({ json in + let bancontact = STPPaymentMethodBancontact.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(bancontact, "Failed to decode JSON") + expectation.fulfill() + }) + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBillieParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBillieParamsTests.swift new file mode 100644 index 00000000..0b20095e --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBillieParamsTests.swift @@ -0,0 +1,44 @@ +// +// STPPaymentMethodBillieParamsTests.swift +// StripeiOSTests +// +// Created by Eric Geniesse on 6/28/24. +// + +import Foundation +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodBillieParamsTests: XCTestCase { + + func testCreateBilliePaymentMethod() throws { + let billieParams = STPPaymentMethodBillieParams() + + let params = STPPaymentMethodParams( + billie: billieParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Billie create") + + let client = STPAPIClient(publishableKey: STPTestingDEPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .billie, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.billie, "The `billie` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBillieTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBillieTests.swift new file mode 100644 index 00000000..dea803fc --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBillieTests.swift @@ -0,0 +1,39 @@ +// +// STPPaymentMethodBillieTests.swift +// StripeiOSTests +// +// Created by Eric Geniesse on 6/28/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import XCTest + +class STPPaymentMethodBillieTests: XCTestCase { + + static let billiePaymentIntentClientSecret = "pi_3PWj22Alz2yHYCNZ0vnWGOMN_secret_8eUe1QkN5OVMY0323P4rsYPvv" + + func _retrieveBillieJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDEPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.billiePaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["billie"]) + let billieJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.billie?.allResponseFields) + completion(billieJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveBillieJSON({ json in + let billie = STPPaymentMethodBillie.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(billie, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTest.swift new file mode 100644 index 00000000..92c6546a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTest.swift @@ -0,0 +1,34 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodBillingDetailsTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/6/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodBillingDetailsTest: XCTestCase { + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodBillingDetails.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPPaymentMethodBillingDetails.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["billing_details"] as? [AnyHashable: Any] + let billingDetails = STPPaymentMethodBillingDetails.decodedObject(fromAPIResponse: response) + XCTAssertEqual(billingDetails?.email, "jenny@example.com") + XCTAssertEqual(billingDetails?.name, "jenny") + XCTAssertEqual(billingDetails?.phone, "+15555555555") + XCTAssertNotNil(billingDetails?.address) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTests+Link.swift b/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTests+Link.swift new file mode 100644 index 00000000..c7274014 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBillingDetailsTests+Link.swift @@ -0,0 +1,41 @@ +// +// STPPaymentMethodBillingDetailsTests.swift +// StripeiOSTests +// +// Created by Eduardo Urias on 2/27/23. +// + +@testable import StripePaymentSheet +import XCTest + +// Link mapping tests +final class STPPaymentMethodBillingDetailsTests: XCTestCase { + func testConsumersAPIParamsMapping() { + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Name" + billingDetails.email = "Email" + billingDetails.phone = "Phone" + billingDetails.address = STPPaymentMethodAddress() + billingDetails.address?.line1 = "Line 1" + billingDetails.address?.line2 = "" + billingDetails.address?.city = "City" + billingDetails.address?.state = "State" + billingDetails.address?.country = "Country" + + let params = billingDetails.consumersAPIParams + XCTAssertEqual(params["name"] as? String, "Name") + XCTAssertEqual(params["line_1"] as? String, "Line 1") + XCTAssertNil(params["line_2"]) + XCTAssertEqual(params["locality"] as? String, "City") + XCTAssertEqual(params["administrative_area"] as? String, "State") + XCTAssertEqual(params["country_code"] as? String, "Country") + + XCTAssertNil(params["email"]) + XCTAssertNil(params["phone"]) + XCTAssertNil(params["line1"]) + XCTAssertNil(params["line2"]) + XCTAssertNil(params["city"]) + XCTAssertNil(params["state"]) + XCTAssertNil(params["country"]) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBoletoParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBoletoParamsTests.swift new file mode 100644 index 00000000..b67a74ab --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBoletoParamsTests.swift @@ -0,0 +1,72 @@ +// +// STPPaymentMethodBoletoParamsTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 9/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodBoletoParamsTests: STPNetworkStubbingTestCase { + + func testCreateBoletoPaymentMethod() throws { + let boletoParams = STPPaymentMethodBoletoParams() + boletoParams.taxID = "00.000.000/0001-91" + + let address = STPPaymentMethodAddress() + address.line1 = "Av. Do Brasil 1374" + address.city = "Sao Paulo" + address.state = "SP" + address.postalCode = "01310100" + address.country = "BR" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Diaz" + billingDetails.email = "jane@example.com" + billingDetails.address = address + + let params = STPPaymentMethodParams( + boleto: boletoParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Boleto create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .boleto, "Incorrect PaymentMethod type") + + XCTAssertEqual( + paymentMethod?.billingDetails?.name, + "Jane Diaz", + "Billing name should match the name provided during creation" + ) + + XCTAssertEqual( + paymentMethod?.billingDetails?.email, + "jane@example.com", + "Billing email should match the name provided during creation" + ) + + XCTAssertNotNil(paymentMethod?.boleto, "The `boleto` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodBoletoTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodBoletoTests.swift new file mode 100644 index 00000000..a72f4cdb --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodBoletoTests.swift @@ -0,0 +1,54 @@ +// +// STPPaymentMethodBoletoTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 9/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodBoletoTests: STPNetworkStubbingTestCase { + + private(set) var boletoJSON: [AnyHashable: Any]? + + static let boletoPaymentIntentClientSecret = + "pi_3JYFj9JQVROkWvqT0d2HYaTk_secret_c2PniS4q2A7XhZ9mbFwOTpN08" + + func _retrieveBoletoJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let boletoJSON = boletoJSON { + completion(boletoJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingBRPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.boletoPaymentIntentClientSecret, + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + boletoJSON = paymentIntent?.paymentMethod?.boleto?.allResponseFields + completion(boletoJSON ?? [:]) + } + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveBoletoJSON({ json in + let boleto = STPPaymentMethodBoleto.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(boleto, "Failed to decode JSON") + XCTAssertEqual(boleto?.taxID, "00.000.000/0001-91", "It must properly decode `taxID`") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardChecksTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardChecksTest.swift new file mode 100644 index 00000000..164dde11 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardChecksTest.swift @@ -0,0 +1,45 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodCardChecksTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/5/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +import XCTest + +class STPPaymentMethodCardChecksTest: XCTestCase { + func testDecodedObjectFromAPIResponse() { + let response = [ + "address_line1_check": NSNull(), + "address_postal_code_check": NSNull(), + "cvc_check": NSNull(), + ] + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var mutableResponse = response + mutableResponse.removeValue(forKey: field) + XCTAssertNil(STPPaymentMethodCardChecks.decodedObject(fromAPIResponse: mutableResponse)) + } + let checks = STPPaymentMethodCardChecks.decodedObject(fromAPIResponse: response) + XCTAssertNotNil(checks) + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertEqual(checks?.addressLine1Check, .unknown) + XCTAssertEqual(checks?.addressPostalCodeCheck, .unknown) + XCTAssertEqual(checks?.cvcCheck, .unknown) + // #pragma clang diagnostic pop + } + + func testCheckResultFromString() { + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: "pass"), .pass) + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: "failed"), .failed) + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: "unavailable"), .unavailable) + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: "unchecked"), .unchecked) + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: "unknown_string"), .unknown) + XCTAssertEqual(STPPaymentMethodCardChecks.checkResult(from: nil), .unknown) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift new file mode 100644 index 00000000..b684214e --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift @@ -0,0 +1,99 @@ +// +// STPPaymentMethodCardParamsTest.swift +// StripeiOS Tests +// +// Created by David Estes on 2/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodCardParamsTest: XCTestCase { + func testEqualityCheck() { + let params1 = STPPaymentMethodCardParams() + params1.number = "4242424242424242" + params1.cvc = "123" + params1.expYear = 22 + params1.expMonth = 12 + params1.networks = .init(preferred: "visa") + let params2 = STPPaymentMethodCardParams() + params2.number = "4242424242424242" + params2.cvc = "123" + params2.expYear = 22 + params2.expMonth = 12 + params2.networks = .init(preferred: "visa") + XCTAssertEqual(params1, params2) + params1.additionalAPIParameters["test"] = "bla" + XCTAssertNotEqual(params1, params2) + params2.additionalAPIParameters["test"] = "bla" + XCTAssertEqual(params1, params2) + } + + func testCardParamsFromPaymentMethodParams() { + let pmCardParams = STPPaymentMethodCardParams() + pmCardParams.number = "4242424242424242" + pmCardParams.cvc = "123" + pmCardParams.expYear = 22 + pmCardParams.expMonth = 12 + let addressParams = STPPaymentMethodBillingDetails() + addressParams.name = "Tester McTestington" + let address = STPPaymentMethodAddress() + address.line1 = "123 Fake St" + address.line2 = "Apt 123" + address.city = "City" + address.state = "NY" + address.country = "US" + address.postalCode = "12345" + addressParams.address = address + let pmParams = STPPaymentMethodParams( + card: pmCardParams, + billingDetails: addressParams, + metadata: nil + ) + let cardParams = STPCardParams(paymentMethodParams: pmParams) + XCTAssertEqual(cardParams.number, "4242424242424242") + XCTAssertEqual(cardParams.cvc, "123") + XCTAssertEqual(cardParams.expYear, 22) + XCTAssertEqual(cardParams.expMonth, 12) + XCTAssertEqual(cardParams.name, "Tester McTestington") + XCTAssertEqual(cardParams.addressLine1, "123 Fake St") + XCTAssertEqual(cardParams.addressLine2, "Apt 123") + XCTAssertEqual(cardParams.addressCity, "City") + XCTAssertEqual(cardParams.addressState, "NY") + XCTAssertEqual(cardParams.addressCountry, "US") + XCTAssertEqual(cardParams.addressZip, "12345") + } + + func testPropertyNamesToFormFieldsMapping() { + // Test for STPPaymentMethodCardParams + let cardParams = STPPaymentMethodCardParams() + + let cardParamsExpectedMapping = [ + "number": "number", + "expMonth": "exp_month", + "expYear": "exp_year", + "cvc": "cvc", + "token": "token", + "networks": "networks", + ] + + let cardParamsMapping = type(of: cardParams).propertyNamesToFormFieldNamesMapping() + + XCTAssertEqual(cardParamsMapping, cardParamsExpectedMapping) + + // Test for STPPaymentMethodCardNetworksParams + let networksParams = STPPaymentMethodCardNetworksParams() + + let networksParamsExpectedMapping = [ + "preferred": "preferred", + ] + + let networksParamsMapping = type(of: networksParams).propertyNamesToFormFieldNamesMapping() + + XCTAssertEqual(networksParamsMapping, networksParamsExpectedMapping) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardTest.swift new file mode 100644 index 00000000..b3d15f66 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardTest.swift @@ -0,0 +1,93 @@ +// +// STPPaymentMethodCardTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/6/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI +private let kCardPaymentIntentClientSecret = + "pi_1H5J4RFY0qyl6XeWFTpgue7g_secret_1SS59M0x65qWMaX2wEB03iwVE" + +class STPPaymentMethodCardTest: STPNetworkStubbingTestCase { + private(set) var cardJSON: [AnyHashable: Any]? + + func _retrieveCardJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let cardJSON = cardJSON { + completion(cardJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: kCardPaymentIntentClientSecret, + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + cardJSON = paymentIntent?.paymentMethod?.card?.allResponseFields + completion(cardJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + _retrieveCardJSON({ json in + let card = STPPaymentMethodCard.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(card, "Failed to decode JSON") + retrieveJSON.fulfill() + XCTAssertEqual(card?.brand, .visa) + XCTAssertEqual(card?.country, "US") + XCTAssertNotNil(card?.checks) + XCTAssertEqual(card?.expMonth, 7) + XCTAssertEqual(card?.expYear, 2021) + XCTAssertEqual(card?.funding, "credit") + XCTAssertEqual(card?.last4, "4242") + XCTAssertNotNil(card?.threeDSecureUsage) + XCTAssertEqual(card?.threeDSecureUsage?.supported, true) + XCTAssertNotNil(card?.networks) + XCTAssertEqual(card?.networks?.available, ["visa"]) + XCTAssertNil(card?.networks?.preferred) + }) + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = + STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)?["card"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodCard.decodedObject(fromAPIResponse: response)) + } + let json = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)?["card"] + let decoded = STPPaymentMethodCard.decodedObject( + fromAPIResponse: json as? [AnyHashable: Any] + ) + XCTAssertNotNil(decoded) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = + STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)?["card"] as? [AnyHashable: Any] + let card = STPPaymentMethodCard.decodedObject(fromAPIResponse: response) + XCTAssertEqual(card?.brand, .visa) + XCTAssertEqual(card?.country, "US") + XCTAssertNotNil(card?.checks) + XCTAssertEqual(card?.expMonth, 8) + XCTAssertEqual(card?.expYear, 2020) + XCTAssertEqual(card?.funding, "credit") + XCTAssertEqual(card?.last4, "4242") + XCTAssertEqual(card?.fingerprint, "6gVyxfIhqc8Z0g0X") + XCTAssertNotNil(card?.threeDSecureUsage) + XCTAssertEqual(card?.threeDSecureUsage?.supported, true) + XCTAssertEqual(card?.displayBrand, "cartes_bancaires") + XCTAssertNotNil(card?.wallet) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardWalletMasterpassTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletMasterpassTest.swift new file mode 100644 index 00000000..7054d01b --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletMasterpassTest.swift @@ -0,0 +1,21 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodCardWalletMasterpassTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodCardWalletMasterpassTest: XCTestCase { + func testDecodedObjectFromAPIResponseMapping() { + // We reuse the visa checkout JSON because it's identical to the masterpass version + let response = ((STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["card"] as! [AnyHashable: Any])["wallet"] as! [AnyHashable: Any])["visa_checkout"] as? [AnyHashable: Any] + let masterpass = STPPaymentMethodCardWalletMasterpass.decodedObject(fromAPIResponse: response) + XCTAssertNotNil(masterpass) + XCTAssertEqual(masterpass?.name, "Jenny") + XCTAssertEqual(masterpass?.email, "jenny@example.com") + XCTAssertNotNil(masterpass?.billingAddress) + XCTAssertNotNil(masterpass?.shippingAddress) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardWalletTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletTest.swift new file mode 100644 index 00000000..4058033c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletTest.swift @@ -0,0 +1,40 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodCardWalletTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments + +class STPPaymentMethodCardWalletTest: XCTestCase { + // MARK: - STPPaymentMethodCardWalletType Tests + + func testTypeFromString() { + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "amex_express_checkout"), .amexExpressCheckout) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "AMEX_EXPRESS_CHECKOUT"), .amexExpressCheckout) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "apple_pay"), .applePay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "APPLE_PAY"), .applePay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "google_pay"), .googlePay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "GOOGLE_PAY"), .googlePay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "masterpass"), .masterpass) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "MASTERPASS"), .masterpass) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "samsung_pay"), .samsungPay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "SAMSUNG_PAY"), .samsungPay) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "visa_checkout"), .visaCheckout) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "VISA_CHECKOUT"), .visaCheckout) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "link"), .link) + XCTAssertEqual(STPPaymentMethodCardWallet.type(from: "LINK"), .link) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseMapping() { + let response = (STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["card"] as! [AnyHashable: Any]) ["wallet"] as? [AnyHashable: Any] + let wallet = STPPaymentMethodCardWallet.decodedObject(fromAPIResponse: response) + XCTAssertNotNil(wallet) + XCTAssertEqual(wallet?.type, .visaCheckout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardWalletVisaCheckoutTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletVisaCheckoutTest.swift new file mode 100644 index 00000000..c389521f --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardWalletVisaCheckoutTest.swift @@ -0,0 +1,20 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodCardWalletVisaCheckoutTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodCardWalletVisaCheckoutTest: XCTestCase { + func testDecodedObjectFromAPIResponseMapping() { + let response = ((STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard)["card"] as! [AnyHashable: Any])["wallet"] as! [AnyHashable: Any])["visa_checkout"] as? [AnyHashable: Any] + let visaCheckout = STPPaymentMethodCardWalletVisaCheckout.decodedObject(fromAPIResponse: response) + XCTAssertNotNil(visaCheckout) + XCTAssertEqual(visaCheckout?.name, "Jenny") + XCTAssertEqual(visaCheckout?.email, "jenny@example.com") + XCTAssertNotNil(visaCheckout?.billingAddress) + XCTAssertNotNil(visaCheckout?.shippingAddress) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCashAppParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodCashAppParamsTests.swift new file mode 100644 index 00000000..b7eb3d08 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCashAppParamsTests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodCashAppParamsTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 1/4/23. +// + +import Foundation +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodCashAppParamsTests: STPNetworkStubbingTestCase { + + func testCreateCashAppPaymentMethod() throws { + let cashAppParams = STPPaymentMethodCashAppParams() + + let params = STPPaymentMethodParams( + cashApp: cashAppParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Cash App create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .cashApp, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.cashApp, "The `cashApp` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCashAppTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodCashAppTests.swift new file mode 100644 index 00000000..2b0a621f --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodCashAppTests.swift @@ -0,0 +1,41 @@ +// +// STPPaymentMethodCashAppTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 1/4/23. +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodCashAppTests: STPNetworkStubbingTestCase { + + static let cashAppPaymentIntentClientSecret = "pi_3MMa4NFY0qyl6XeW1FM3HOts_secret_b4HQ5YksK3mfe7zZaxBlWCark" + + func _retrieveCashAppJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.cashAppPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["cashapp"]) + let cashAppJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.cashApp?.allResponseFields) + completion(cashAppJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveCashAppJSON({ json in + let cashApp = STPPaymentMethodCashApp.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(cashApp, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodEPSParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodEPSParamsTests.swift new file mode 100644 index 00000000..96b68c96 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodEPSParamsTests.swift @@ -0,0 +1,51 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodEPSParamsTests.m +// StripeiOS Tests +// +// Created by Shengwei Wu on 5/15/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodEPSParamsTests: STPNetworkStubbingTestCase { + func testCreateEPSPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let epsParams = STPPaymentMethodEPSParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + eps: epsParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method EPS create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating EPS PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create EPS PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .EPS, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // EPS Details + XCTAssertNotNil(paymentMethod?.eps, "Missing eps") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodEPSTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodEPSTests.swift new file mode 100644 index 00000000..ce20092b --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodEPSTests.swift @@ -0,0 +1,44 @@ +// +// STPPaymentMethodEPSTests.swift +// StripeiOS Tests +// +// Created by Shengwei Wu on 5/15/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodEPSTests: STPNetworkStubbingTestCase { + private(set) var epsJSON: [AnyHashable: Any]? + + func _retrieveEPSJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let epsJSON = epsJSON { + completion(epsJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1Gj0rqFY0qyl6XeWrug30CPz_secret_tKyf8QOKtiIrE3NSEkWCkBbyy", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + epsJSON = paymentIntent?.paymentMethod?.eps?.allResponseFields + completion(epsJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch EPS JSON") + _retrieveEPSJSON({ json in + let eps = STPPaymentMethodEPS.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(eps, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodFPXTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodFPXTest.swift new file mode 100644 index 00000000..81a19c9c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodFPXTest.swift @@ -0,0 +1,35 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodFPXTest.m +// StripeiOS Tests +// +// Created by David Estes on 8/26/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodFPXTest: XCTestCase { + func exampleJson() -> [AnyHashable: Any]? { + return [ + "bank": "maybank2u", + ] + } + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = exampleJson() + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodFPX.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPPaymentMethodFPX.decodedObject(fromAPIResponse: exampleJson())) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = exampleJson() + let fpx = STPPaymentMethodFPX.decodedObject(fromAPIResponse: response) + XCTAssertEqual(fpx?.bankIdentifierCode, "maybank2u") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodFunctionalTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodFunctionalTest.swift new file mode 100644 index 00000000..740bbc67 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodFunctionalTest.swift @@ -0,0 +1,355 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodFunctionalTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/6/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import Stripe +import StripeCoreTestUtils +@_spi(STP) import StripePayments +@testable @_spi(CustomerSessionBetaAccess) import StripePaymentSheet +@testable import StripePaymentsTestUtils + +class STPPaymentMethodFunctionalTest: STPNetworkStubbingTestCase { + func testCreateCardPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let card = STPPaymentMethodCardParams() + card.number = "4242424242424242" + card.expMonth = NSNumber(value: 10) + card.expYear = NSNumber(value: 2028) + card.cvc = "100" + + let billingAddress = STPPaymentMethodAddress() + billingAddress.city = "San Francisco" + billingAddress.country = "US" + billingAddress.line1 = "150 Townsend St" + billingAddress.line2 = "4th Floor" + billingAddress.postalCode = "94103" + billingAddress.state = "CA" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = billingAddress + billingDetails.email = "email@email.com" + billingDetails.name = "Isaac Asimov" + billingDetails.phone = "555-555-5555" + + let params = STPPaymentMethodParams( + card: card, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + let expectation = self.expectation(description: "Payment Method Card create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertNotNil(paymentMethod?.stripeId) + XCTAssertNotNil(paymentMethod?.created) + XCTAssertFalse(paymentMethod!.liveMode) + XCTAssertEqual(paymentMethod?.type, .card) + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.email, "email@email.com") + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Isaac Asimov") + XCTAssertEqual(paymentMethod?.billingDetails!.phone, "555-555-5555") + + // Billing Details Address + XCTAssertEqual(paymentMethod?.billingDetails!.address!.line1, "150 Townsend St") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.line2, "4th Floor") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.city, "San Francisco") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.country, "US") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.state, "CA") + XCTAssertEqual(paymentMethod?.billingDetails!.address!.postalCode, "94103") + + // Card + XCTAssertEqual(paymentMethod?.card!.brand, .visa) + XCTAssertEqual(paymentMethod?.card!.checks!.cvcCheck, .unknown) + XCTAssertEqual(paymentMethod?.card!.checks!.addressLine1Check, .unknown) + XCTAssertEqual(paymentMethod?.card!.checks!.addressPostalCodeCheck, .unknown) + XCTAssertEqual(paymentMethod?.card!.country, "US") + XCTAssertEqual(paymentMethod?.card!.expMonth, 10) + XCTAssertEqual(paymentMethod?.card!.expYear, 2028) + XCTAssertEqual(paymentMethod?.card!.funding, "credit") + XCTAssertEqual(paymentMethod?.card!.last4, "4242") + XCTAssertTrue(paymentMethod!.card!.threeDSecureUsage!.supported) + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testUpdateCardPaymentMethod() async throws { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + + // A hardcoded test Customer + let testCustomerID = "cus_PTf9mhkFv9ZGXl" + + // Create a new EK for the Customer + let customerAndEphemeralKey = try await STPTestingAPIClient.shared().fetchCustomerAndEphemeralKey(customerID: testCustomerID, merchantCountry: "fr") + + // Create a new payment method + let paymentMethod = try await client.createPaymentMethod(with: ._testCardValue(), additionalPaymentUserAgentValues: []) + + // Attach the payment method to the customer + try await client.attachPaymentMethod(paymentMethod.stripeId, + customerID: customerAndEphemeralKey.customer, + ephemeralKeySecret: customerAndEphemeralKey.ephemeralKeySecret) + + // Update the expiry year for the card by 1 year + let card = STPPaymentMethodCardParams() + card.expYear = (paymentMethod.card!.expYear + 1) as NSNumber + + let params = STPPaymentMethodUpdateParams(card: card, billingDetails: nil) + + let updatedPaymentMethod = try await client.updatePaymentMethod(with: paymentMethod.stripeId, + paymentMethodUpdateParams: params, + ephemeralKeySecret: customerAndEphemeralKey.ephemeralKeySecret) + + // Verify + XCTAssertEqual(updatedPaymentMethod.card!.expYear, (paymentMethod.card!.expYear + 1)) + + // Clean up, detach the payment method as a customer can only have 400 payment methods saved + try await client.detachPaymentMethod(paymentMethod.stripeId, + fromCustomerUsing: customerAndEphemeralKey.ephemeralKeySecret) + } + + func testMulitpleCardCreationWithCustomerSessionAndMultiDelete() async throws { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + // Create a new customer and new key + let customerAndEphemeralKey = try await STPTestingAPIClient.shared().fetchCustomerAndEphemeralKey(customerID: nil, merchantCountry: nil) + + // Create a new payment method 1 + let paymentMethod1 = try await client.createPaymentMethod(with: ._testCardValue(), additionalPaymentUserAgentValues: []) + + // Attach the payment method 1 to the customer + try await client.attachPaymentMethod(paymentMethod1.stripeId, + customerID: customerAndEphemeralKey.customer, + ephemeralKeySecret: customerAndEphemeralKey.ephemeralKeySecret) + + // Create a new payment method 2 + let paymentMethod2 = try await client.createPaymentMethod(with: ._testCardValue(), additionalPaymentUserAgentValues: []) + + // Attach the payment method 2 to the customer + try await client.attachPaymentMethod(paymentMethod2.stripeId, + customerID: customerAndEphemeralKey.customer, + ephemeralKeySecret: customerAndEphemeralKey.ephemeralKeySecret) + + // Element/Sessions endpoint should de-dupe payment methods with CustomerSesssion + let cscs = try await STPTestingAPIClient.shared().fetchCustomerAndCustomerSessionClientSecret(customerID: customerAndEphemeralKey.customer, + merchantCountry: nil) + var configuration = PaymentSheet.Configuration() + configuration.customer = PaymentSheet.CustomerConfiguration(id: cscs.customer, customerSessionClientSecret: cscs.customerSessionClientSecret) + let elementSession = try await client.retrieveDeferredElementsSession( + withIntentConfig: .init(mode: .payment(amount: 5000, currency: "usd", setupFutureUsage: .offSession, captureMethod: .automatic), + confirmHandler: { _, _, _ in + // no-op + }), + clientDefaultPaymentMethod: paymentMethod2.stripeId, + configuration: configuration) + + // Requires FF: elements_enable_read_allow_redisplay, to return "1", otherwise 0 + XCTAssertEqual(elementSession.customer?.paymentMethods.count, 1) + XCTAssertEqual(elementSession.customer?.paymentMethods.first?.stripeId, paymentMethod2.stripeId) + XCTAssertEqual(elementSession.customer?.defaultPaymentMethod, paymentMethod2.stripeId) + + // Official endpoint should have two payment methods + let fetchedPaymentMethods = try await fetchPaymentMethods(client: client, customerAndEphemeralKey: customerAndEphemeralKey) + XCTAssertEqual(fetchedPaymentMethods.count, 2) + + // Clean up, detach both payment methods + try await client.detachPaymentMethodRemoveDuplicates(paymentMethod2.stripeId, + customerId: customerAndEphemeralKey.customer, + fromCustomerUsing: customerAndEphemeralKey.ephemeralKeySecret) + + let reFetchedPaymentMethods = try await fetchPaymentMethods(client: client, customerAndEphemeralKey: customerAndEphemeralKey) + XCTAssertEqual(reFetchedPaymentMethods.count, 0) + } + + func testCreateBacsPaymentMethod() { + let client = STPAPIClient(publishableKey: "pk_test_z6Ct4bpx0NUjHii0rsi4XZBf00jmM8qA28") + + let bacs = STPPaymentMethodBacsDebitParams() + bacs.sortCode = "108800" + bacs.accountNumber = "00012345" + + let billingAddress = STPPaymentMethodAddress() + billingAddress.city = "London" + billingAddress.country = "GB" + billingAddress.line1 = "Stripe, 7th Floor The Bower Warehouse" + billingAddress.postalCode = "EC1V 9NR" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = billingAddress + billingDetails.email = "email@email.com" + billingDetails.name = "Isaac Asimov" + billingDetails.phone = "555-555-5555" + + let params = STPPaymentMethodParams(bacsDebit: bacs, billingDetails: billingDetails, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod( + with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .bacsDebit) + + // Bacs Debit + XCTAssertEqual(paymentMethod!.bacsDebit!.fingerprint, "UkSG0HfCGxxrja1H") + XCTAssertEqual(paymentMethod!.bacsDebit!.last4, "2345") + XCTAssertEqual(paymentMethod!.bacsDebit!.sortCode, "108800") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateAlipayPaymentMethod() { + let client = STPAPIClient(publishableKey: "pk_test_JBVAMwnBuzCdmsgN34jfxbU700LRiPqVit") + + let params = STPPaymentMethodParams(alipay: STPPaymentMethodAlipayParams(), billingDetails: nil, metadata: nil) + + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod( + with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .alipay) + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateBLIKPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let params = STPPaymentMethodParams(blik: STPPaymentMethodBLIKParams(), billingDetails: nil, metadata: nil) + + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod( + with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .blik + ) + XCTAssertNotNil(paymentMethod?.blik) + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateMobilePayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + let params = STPPaymentMethodParams(mobilePay: STPPaymentMethodMobilePayParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .mobilePay) + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateAmazonPayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let params = STPPaymentMethodParams(amazonPay: STPPaymentMethodAmazonPayParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .amazonPay) + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateAlmaPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + let params = STPPaymentMethodParams(alma: STPPaymentMethodAlmaParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .alma) + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateSunbitPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let params = STPPaymentMethodParams(sunbit: STPPaymentMethodSunbitParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .sunbit) + XCTAssertNotNil(paymentMethod?.sunbit, "The `sunbit` property must be populated") + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateBilliePaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDEPublishableKey) + let params = STPPaymentMethodParams(billie: STPPaymentMethodBillieParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .billie) + XCTAssertNotNil(paymentMethod?.billie, "The `billie` property must be populated") + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateSatispayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingITPublishableKey) + let params = STPPaymentMethodParams(satispay: STPPaymentMethodSatispayParams(), billingDetails: nil, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .satispay) + XCTAssertNotNil(paymentMethod?.satispay, "The `satispay` property must be populated") + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testCreateMultibancoPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.email = "tester@example.com" + let params = STPPaymentMethodParams(alma: STPPaymentMethodAlmaParams(), billingDetails: billingDetails, metadata: nil) + let expectation = self.expectation(description: "Payment Method create") + client.createPaymentMethod(with: params) { paymentMethod, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod) + XCTAssertEqual(paymentMethod?.type, .alma) + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func fetchPaymentMethods(client: STPAPIClient, + customerAndEphemeralKey: STPTestingAPIClient.CreateEphemeralKeyResponse) async throws -> [STPPaymentMethod] { + try await withCheckedThrowingContinuation { continuation in + client.listPaymentMethods(forCustomer: customerAndEphemeralKey.customer, + using: customerAndEphemeralKey.ephemeralKeySecret, + types: [.card]) { paymentMethods, error in + guard let paymentMethods, error == nil else { + continuation.resume(throwing: error!) + return + } + continuation.resume(returning: paymentMethods) + } + } + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodGiropayParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodGiropayParamsTests.swift new file mode 100644 index 00000000..4df28201 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodGiropayParamsTests.swift @@ -0,0 +1,50 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodGiropayParamsTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 4/21/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodGiropayParamsTests: STPNetworkStubbingTestCase { + func testCreateGiropayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let giropayParams = STPPaymentMethodGiropayParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + giropay: giropayParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method giropay create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating giropay PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create giropay PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .giropay, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // giropay Details + XCTAssertNotNil(paymentMethod?.giropay, "Missing giropay") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodGiropayTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodGiropayTests.swift new file mode 100644 index 00000000..4bdd9854 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodGiropayTests.swift @@ -0,0 +1,44 @@ +// +// STPPaymentMethodGiropayTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 4/21/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodGiropayTests: STPNetworkStubbingTestCase { + private(set) var giropayJSON: [AnyHashable: Any]? + + func _retrieveGiropayDebitJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let giropayJSON = giropayJSON { + completion(giropayJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1GfsdtFY0qyl6XeWLnepIXCI_secret_bLJRSeSY7fBjDXnwh9BUKilMW", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + giropayJSON = paymentIntent?.paymentMethod?.giropay?.allResponseFields + completion(giropayJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch Giropay JSON") + _retrieveGiropayDebitJSON({ json in + let giropay = STPPaymentMethodGiropay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(giropay, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodGrabPayParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodGrabPayParamsTest.swift new file mode 100644 index 00000000..f4c37e7d --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodGrabPayParamsTest.swift @@ -0,0 +1,51 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodGrabPayParamsTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 7/21/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodGrabPayParamsTest: STPNetworkStubbingTestCase { + func testCreateGrabPayPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingSGPublishableKey) + let grabPayParams = STPPaymentMethodGrabPayParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + grabPay: grabPayParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method GrabPay create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating GrabPay PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create GrabPay PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .grabPay, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // GrabPay Details + XCTAssertNotNil(paymentMethod?.grabPay, "Missing grabPay") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodKlarnaParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodKlarnaParamsTests.swift new file mode 100644 index 00000000..2e42ec46 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodKlarnaParamsTests.swift @@ -0,0 +1,72 @@ +// +// STPPaymentMethodKlarnaParamsTests.swift +// StripeiOS Tests +// +// Created by Nick Porter on 10/20/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodKlarnaParamsTests: STPNetworkStubbingTestCase { + + func testCreateKlarnaPaymentMethod() throws { + let klarnaParams = STPPaymentMethodKlarnaParams() + + let address = STPPaymentMethodAddress() + address.line1 = "55 John St" + address.line2 = "#3B" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "John Smith" + billingDetails.email = "foo@example.com" + billingDetails.address = address + + let params = STPPaymentMethodParams( + klarna: klarnaParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Klarna create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .klarna, "Incorrect PaymentMethod type") + + XCTAssertEqual( + paymentMethod?.billingDetails?.name, + "John Smith", + "Billing name should match the name provided during creation" + ) + + XCTAssertEqual( + paymentMethod?.billingDetails?.email, + "foo@example.com", + "Billing email should match the name provided during creation" + ) + + XCTAssertNotNil(paymentMethod?.klarna, "The `klarna` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodKlarnaTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodKlarnaTests.swift new file mode 100644 index 00000000..553d5552 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodKlarnaTests.swift @@ -0,0 +1,47 @@ +// +// STPPaymentMethodKlarnaTests.swift +// StripeiOS Tests +// +// Created by Nick Porter on 10/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodKlarnaTests: STPNetworkStubbingTestCase { + + static let klarnaPaymentIntentClientSecret = + "pi_3Jn3kUFY0qyl6XeW0mCp95UD_secret_28aNjjd1zsySFWvGoSzgcR5Qw" + + func _retrieveKlarnaJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.klarnaPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + let klarnaJson = paymentIntent?.paymentMethod?.klarna?.allResponseFields + completion(klarnaJson ?? [:]) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveKlarnaJSON({ json in + let klarna = STPPaymentMethodKlarna.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(klarna, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodMobilePayParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodMobilePayParamsTests.swift new file mode 100644 index 00000000..8b28b9b7 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodMobilePayParamsTests.swift @@ -0,0 +1,43 @@ +// +// STPPaymentMethodMobilePayParamsTests.swift +// StripeiOSTests +// + +import Foundation +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodMobilePayParamsTests: STPNetworkStubbingTestCase { + + func testCreateMobilePayPaymentMethod() throws { + let mobilePayParams = STPPaymentMethodMobilePayParams() + + let params = STPPaymentMethodParams( + mobilePay: mobilePayParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method MobilePay create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .mobilePay, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.mobilePay, "The `mobilepay` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodMobilePayTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodMobilePayTests.swift new file mode 100644 index 00000000..5361dcc6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodMobilePayTests.swift @@ -0,0 +1,39 @@ +// +// STPPaymentMethodMobilePayTests.swift +// StripeiOSTests +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodMobilePayTests: STPNetworkStubbingTestCase { + + static let mobilePayPaymentIntentClientSecret = "pi_3PGVQJKG6vc7r7YC1Xs7oiWw_secret_5cqzEtQ059azmV1GmkLRA7Lvt" + + func _retrieveMobilePayJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.mobilePayPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["mobilepay"]) + let mobilePayJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.mobilePay?.allResponseFields) + completion(mobilePayJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveMobilePayJSON({ json in + let mobilePay = STPPaymentMethodMobilePay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(mobilePay, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodMultibancoParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodMultibancoParamsTests.swift new file mode 100644 index 00000000..528144ba --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodMultibancoParamsTests.swift @@ -0,0 +1,47 @@ +// +// STPPaymentMethodMultibancoParamsTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 4/22/24. +// + +import Foundation +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodMultibancoParamsTests: STPNetworkStubbingTestCase { + + func testCreateMultibancoPaymentMethod() throws { + let multibancoParams = STPPaymentMethodMultibancoParams() + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + multibanco: multibancoParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Multibanco create") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .multibanco, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.multibanco, "The `multibanco` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodMultibancoTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodMultibancoTests.swift new file mode 100644 index 00000000..20f70b13 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodMultibancoTests.swift @@ -0,0 +1,41 @@ +// +// STPPaymentMethodMultibancoTests.swift +// StripeiOSTests +// +// Created by Nick Porter on 4/22/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodMultibancoTests: STPNetworkStubbingTestCase { + + static let multibancoPaymentIntentClientSecret = "pi_3P8R5lFY0qyl6XeW0byterUo_secret_seOTE1wwZqkjBte83FjHalgsW" + + func _retrieveMultibancoJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.multibancoPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["multibanco"]) + let multibancoJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.multibanco?.allResponseFields) + completion(multibancoJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveMultibancoJSON({ json in + let multibanco = STPPaymentMethodMultibanco.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(multibanco, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodNetBankingParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodNetBankingParamsTest.swift new file mode 100644 index 00000000..6ec5fb2a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodNetBankingParamsTest.swift @@ -0,0 +1,47 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodNetBankingParamsTest.m +// StripeiOS +// +// Created by Anirudh Bhargava on 11/19/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodNetBankingParamsTests: STPNetworkStubbingTestCase { + func testCreateNetBankingPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingINPublishableKey) + let netbankingParams = STPPaymentMethodNetBankingParams() + netbankingParams.bank = "icici" + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + netBanking: netbankingParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method NetBanking create") + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + XCTAssertNil(error, "Unexpected error creating NetBanking PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create NetBanking PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .netBanking, "Incorrect PaymentMethod type") + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + // UPI Details + XCTAssertNotNil(paymentMethod?.netBanking, "Missing NetBanking") + XCTAssertEqual(paymentMethod?.netBanking!.bank, "icici") + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodNetBankingTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodNetBankingTests.swift new file mode 100644 index 00000000..a98ef0fe --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodNetBankingTests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodNetBankingTests.swift +// StripeiOS Tests +// +// Created by Anirudh Bhargava on 11/19/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodNetBankingTests: STPNetworkStubbingTestCase { + private(set) var netbankingJSON: [AnyHashable: Any]? + + func _retrieveNetBankingJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let netbankingJSON = netbankingJSON { + completion(netbankingJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingINPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1HoPqsBte6TMTRd4jX0PwrFa_secret_ThiIwyssre9qjJ6gtmghC21fk", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + netbankingJSON = paymentIntent?.paymentMethod?.netBanking?.allResponseFields + completion(netbankingJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch NetBanking JSON") + _retrieveNetBankingJSON({ json in + let netbanking = STPPaymentMethodNetBanking.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(netbanking, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodOXXOParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodOXXOParamsTests.swift new file mode 100644 index 00000000..5c4c5de8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodOXXOParamsTests.swift @@ -0,0 +1,53 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodOXXOParamsTests.m +// StripeiOS Tests +// +// Created by Polo Li on 6/16/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodOXXOParamsTests: STPNetworkStubbingTestCase { + func testCreateOXXOPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let oxxoParams = STPPaymentMethodOXXOParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + billingDetails.email = "test@test.com" + + let params = STPPaymentMethodParams( + oxxo: oxxoParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method OXXO create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating OXXO PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create OXXO PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .OXXO, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails?.name, "Jane Doe") + XCTAssertEqual(paymentMethod?.billingDetails?.email, "test@test.com") + + // OXXO Details + XCTAssertNotNil(paymentMethod?.oxxo, "Missing OXXO") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodOXXOTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodOXXOTests.swift new file mode 100644 index 00000000..276d5829 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodOXXOTests.swift @@ -0,0 +1,38 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodOXXOTests.m +// StripeiOS Tests +// +// Created by Polo Li on 6/16/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodOXXOTests: STPNetworkStubbingTestCase { + private(set) var oxxoJSON: [AnyHashable: Any]? + + func _retrieveOXXOJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let oxxoJSON { + completion(oxxoJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingMEXPublishableKey) + client.retrievePaymentIntent(withClientSecret: "pi_1GvAdyHNG4o8pO5l0dr078gf_secret_h0tJE5mSX9BPEkmpKSh93jBXi", expand: ["payment_method"]) { paymentIntent, _ in + self.oxxoJSON = paymentIntent?.paymentMethod?.oxxo?.allResponseFields + completion(self.oxxoJSON) + } + } + } + + func testCorrectParsing() { + let expectation = self.expectation(description: "Retrieve payment intent") + _retrieveOXXOJSON({ json in + let oxxo = STPPaymentMethodOXXO.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(oxxo, "Failed to decode JSON") + expectation.fulfill() + }) + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodOptionsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodOptionsTest.swift new file mode 100644 index 00000000..8c0ac4f3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodOptionsTest.swift @@ -0,0 +1,133 @@ +// +// STPPaymentMethodOptionsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 4/8/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodOptionsTest: STPNetworkStubbingTestCase { + + func testUSBankAccountOptions_PaymentIntent() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationMethods = [ + "skip", + "automatic", + "instant", + "microdeposits", + "instant_or_skip", + ] + for verificationMethod in verificationMethods { + var clientSecret: String? + let createPIExpectation = expectation(description: "Create PaymentIntent") + STPTestingAPIClient.shared.createPaymentIntent( + withParams: [ + "payment_method_types": ["us_bank_account"], + "payment_method_options": [ + "us_bank_account": ["verification_method": verificationMethod] + ], + "currency": "usd", + "amount": 1000, + ], + account: nil + ) { intentClientSecret, error in + XCTAssertNil(error) + XCTAssertNotNil(intentClientSecret) + clientSecret = intentClientSecret + createPIExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + guard let clientSecret = clientSecret else { + XCTFail("Failed to create PaymentIntent") + continue + } + + let retrievePIExpectation = expectation(description: "Retrieve PaymentIntent") + client.retrievePaymentIntent(withClientSecret: clientSecret) { paymentIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(paymentIntent) + + XCTAssertNotNil( + paymentIntent?.paymentMethodOptions?.usBankAccount?.verificationMethod + ) + XCTAssertEqual( + paymentIntent?.paymentMethodOptions?.usBankAccount?.verificationMethod, + STPPaymentMethodOptions.USBankAccount.VerificationMethod( + rawValue: verificationMethod + ) + ) + retrievePIExpectation.fulfill() + + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + } + + func testUSBankAccountOptions_SetupIntent() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationMethods = [ + "skip", + "automatic", + "instant", + "microdeposits", + "instant_or_skip", + ] + for verificationMethod in verificationMethods { + var clientSecret: String? + let createPIExpectation = expectation(description: "Create SetupIntent") + STPTestingAPIClient.shared.createSetupIntent( + withParams: [ + "payment_method_types": ["us_bank_account"], + "payment_method_options": [ + "us_bank_account": ["verification_method": verificationMethod] + ], + ], + account: nil + ) { intentClientSecret, error in + XCTAssertNil(error) + XCTAssertNotNil(intentClientSecret) + clientSecret = intentClientSecret + createPIExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + guard let clientSecret = clientSecret else { + XCTFail("Failed to create SetupIntent") + continue + } + + let retrievePIExpectation = expectation(description: "Retrieve PaymentIntent") + client.retrieveSetupIntent(withClientSecret: clientSecret) { setupIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(setupIntent) + + XCTAssertNotNil( + setupIntent?.paymentMethodOptions?.usBankAccount?.verificationMethod + ) + XCTAssertEqual( + setupIntent?.paymentMethodOptions?.usBankAccount?.verificationMethod, + STPPaymentMethodOptions.USBankAccount.VerificationMethod( + rawValue: verificationMethod + ) + ) + retrievePIExpectation.fulfill() + + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodParamsTest.swift new file mode 100644 index 00000000..934dbacc --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodParamsTest.swift @@ -0,0 +1,33 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodParamsTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/7/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodParamsTest: XCTestCase { + // MARK: STPFormEncodable Tests + + func testRootObjectName() { + XCTAssertNil(STPPaymentMethodParams.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let params = STPPaymentMethodParams() + + let mapping = STPPaymentMethodParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(params.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodPayPalParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodPayPalParamsTests.swift new file mode 100644 index 00000000..e52efa50 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodPayPalParamsTests.swift @@ -0,0 +1,50 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodPayPalParamsTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/7/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodPayPalParamsTests: STPNetworkStubbingTestCase { + func testCreatePayPalPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let payPalParams = STPPaymentMethodPayPalParams() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jane Doe" + + let params = STPPaymentMethodParams( + payPal: payPalParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method PayPal create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating PayPal PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create PayPal PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .payPal, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jane Doe") + + // PayPal Details + XCTAssertNotNil(paymentMethod?.payPal, "Missing PayPal") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodPayPalTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodPayPalTests.swift new file mode 100644 index 00000000..cd9319fa --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodPayPalTests.swift @@ -0,0 +1,37 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodPayPalTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/7/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodPayPalTests: STPNetworkStubbingTestCase { + var payPalJSON: [AnyHashable: Any]? + + func _retrievePayPalJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let payPalJSON { + completion(payPalJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent(withClientSecret: "pi_1HcI17FY0qyl6XeWcFAAbZCw_secret_oAZ9OCoeyIg8EPeBEdF96ZJOT", expand: ["payment_method"]) { paymentIntent, _ in + self.payPalJSON = paymentIntent?.lastPaymentError?.paymentMethod?.payPal?.allResponseFields + completion(self.payPalJSON) + } + } + } + + func testCorrectParsing() { + let expectation = self.expectation(description: "Retrieve payment intent") + _retrievePayPalJSON({ json in + let payPal = STPPaymentMethodPayPal.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(payPal, "Failed to decode JSON") + expectation.fulfill() + }) + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24ParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24ParamsTests.swift new file mode 100644 index 00000000..ce537a9d --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24ParamsTests.swift @@ -0,0 +1,51 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodPrzelewy24ParamsTests.m +// StripeiOS Tests +// +// Created by Vineet Shah on 4/23/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodPrzelewy24ParamsTests: STPNetworkStubbingTestCase { + func testCreatePrzelewy24PaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let przelewy24Params = STPPaymentMethodPrzelewy24Params() + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.email = "email@email.com" + + let params = STPPaymentMethodParams( + przelewy24: przelewy24Params, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method Przelewy24 create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating Przelewy24 PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create Przelewy24 PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .przelewy24, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.email, "email@email.com") + + // Przelewy24 Details + XCTAssertNotNil(paymentMethod?.przelewy24, "Missing Przelewy24") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24Tests.swift b/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24Tests.swift new file mode 100644 index 00000000..887ebb06 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodPrzelewy24Tests.swift @@ -0,0 +1,44 @@ +// +// STPPaymentMethodPrzelewy24Tests.swift +// StripeiOS Tests +// +// Created by Vineet Shah on 4/23/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodPrzelewy24Tests: STPNetworkStubbingTestCase { + private(set) var przelewy24JSON: [AnyHashable: Any]? + + func _retrievePrzelewy24JSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let przelewy24JSON = przelewy24JSON { + completion(przelewy24JSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1GciHFFY0qyl6XeWp9RdhmFF_secret_rFeERcidL1O5o1lwQUcIjLEZz", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + przelewy24JSON = paymentIntent?.paymentMethod?.przelewy24?.allResponseFields + completion(przelewy24JSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let expectation = self.expectation(description: "Retrieve payment intent") + _retrievePrzelewy24JSON({ json in + let przelewy24 = STPPaymentMethodPrzelewy24.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(przelewy24, "Failed to decode JSON") + expectation.fulfill() + }) + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayParamsTests.swift new file mode 100644 index 00000000..2712f72c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayParamsTests.swift @@ -0,0 +1,43 @@ +// +// STPPaymentMethodRevolutPayParamsTests.swift +// StripeiOSTests +// + +import Foundation +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodRevolutPayParamsTests: STPNetworkStubbingTestCase { + + func testCreateRevolutPayPaymentMethod() throws { + let revolutPayParams = STPPaymentMethodRevolutPayParams() + + let params = STPPaymentMethodParams( + revolutPay: revolutPayParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Revolut Pay create") + + let client = STPAPIClient(publishableKey: STPTestingGBPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .revolutPay, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.revolutPay, "The `revolut_pay` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayTests.swift new file mode 100644 index 00000000..68e47a23 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodRevolutPayTests.swift @@ -0,0 +1,39 @@ +// +// STPPaymentMethodRevolutPayTests.swift +// StripeiOSTests +// + +@testable import Stripe +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +class STPPaymentMethodRevolutPayTests: STPNetworkStubbingTestCase { + + static let revolutPayPaymentIntentClientSecret = "pi_3NqgBBGoesj9fw9Q1TkY7iBp_secret_Ha7VfLCwaAuhEOshZiNnIDjh6" + + func _retrieveRevolutPayJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingGBPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.revolutPayPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["revolut_pay"]) + let revolutPayJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.revolutPay?.allResponseFields) + completion(revolutPayJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveRevolutPayJSON({ json in + let revolutPay = STPPaymentMethodRevolutPay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(revolutPay, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSEPADebitTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodSEPADebitTest.swift new file mode 100644 index 00000000..9454c33a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSEPADebitTest.swift @@ -0,0 +1,39 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodSEPADebitTest.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/7/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodSEPADebitTest: XCTestCase { + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodSEPADebit.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPPaymentMethodSEPADebit.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any] + let sepaDebit = STPPaymentMethodSEPADebit.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(sepaDebit?.bankCode, "37040044") + XCTAssertEqual(sepaDebit?.branchCode, "a_branch") + XCTAssertEqual(sepaDebit?.country, "DE") + XCTAssertEqual(sepaDebit?.fingerprint, "NxdSyRegc9PsMkWy") + XCTAssertEqual(sepaDebit?.last4, "3001") + XCTAssertEqual(sepaDebit?.mandate, "NXDSYREGC9PSMKWY") + + XCTAssertEqual(sepaDebit!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSatispayTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSatispayTests.swift new file mode 100644 index 00000000..4dccb425 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSatispayTests.swift @@ -0,0 +1,40 @@ +// +// STPPaymentMethodSatispayTests.swift +// StripeiOSTests +// +// Created by Eric Geniesse on 7/1/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import XCTest +@testable@_spi(STP) import StripePaymentsTestUtils + +class STPPaymentMethodSatispayTests: STPNetworkStubbingTestCase { + + static let satispayPaymentIntentClientSecret = "pi_3PXrkJIFbdis1OxT0XLmWug3_secret_oZfKhPPuB4KNqR4H3f34ZgDvY" + + func _retrieveSatispayJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingITPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.satispayPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["satispay"]) + let satispayJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.satispay?.allResponseFields) + completion(satispayJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveSatispayJSON({ json in + let satispay = STPPaymentMethodSatispay.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(satispay, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSofortParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSofortParamsTests.swift new file mode 100644 index 00000000..f96ce155 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSofortParamsTests.swift @@ -0,0 +1,52 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodSofortParamsTests.m +// StripeiOS Tests +// +// Created by David Estes on 8/7/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodSofortParamsTests: STPNetworkStubbingTestCase { + func testCreateSofortPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let sofortParams = STPPaymentMethodSofortParams() + sofortParams.country = "DE" + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + sofort: sofortParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method Sofort create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating Sofort PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create Sofort PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .sofort, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // Sofort Details + XCTAssertNotNil(paymentMethod?.sofort, "Missing Sofort") + XCTAssertEqual(paymentMethod?.sofort!.country, "DE") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSofortTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSofortTests.swift new file mode 100644 index 00000000..cafa0534 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSofortTests.swift @@ -0,0 +1,44 @@ +// +// STPPaymentMethodSofortTests.swift +// StripeiOS Tests +// +// Created by David Estes on 8/7/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// +import StripeCoreTestUtils +import StripePaymentsTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodSofortTests: STPNetworkStubbingTestCase { + private(set) var sofortJSON: [AnyHashable: Any]? + + func _retrieveSofortJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let sofortJSON = sofortJSON { + completion(sofortJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1HDdfSFY0qyl6XeWjk7ogYVV_secret_5ikjoct7F271A4Bp6t7HkHwUo", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + sofortJSON = paymentIntent?.paymentMethod?.sofort?.allResponseFields + completion(sofortJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch Sofort JSON") + _retrieveSofortJSON({ json in + let sofort = STPPaymentMethodSofort.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(sofort, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSunbitTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSunbitTests.swift new file mode 100644 index 00000000..87c32c9b --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSunbitTests.swift @@ -0,0 +1,39 @@ +// +// STPPaymentMethodSunbitTests.swift +// StripeiOSTests +// +// Created by Eric Geniesse on 6/27/24. +// + +@testable import Stripe +import StripeCoreTestUtils +import XCTest + +class STPPaymentMethodSunbitTests: XCTestCase { + + static let sunbitPaymentIntentClientSecret = "pi_3PXmrrFY0qyl6XeW1KiGqDLP_secret_zAZk6ZD3cLPwb2lB6wTcLAhLU" + + func _retrieveSunbitJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.sunbitPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + XCTAssertNotNil(paymentIntent?.paymentMethod?.allResponseFields["sunbit"]) + let sunbitJson = try? XCTUnwrap(paymentIntent?.paymentMethod?.sunbit?.allResponseFields) + completion(sunbitJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveSunbitJSON({ json in + let sunbit = STPPaymentMethodSunbit.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(sunbit, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSwishParamsTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSwishParamsTests.swift new file mode 100644 index 00000000..c67b09c2 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSwishParamsTests.swift @@ -0,0 +1,40 @@ +// +// STPPaymentMethodSwishParamsTests.swift +// StripeiOSTests +// +// Created by Eduardo Urias on 9/21/23. +// + +import Foundation +@testable @_spi(STP) import StripeCoreTestUtils +@testable @_spi(STP) import StripePayments +import StripePaymentsTestUtils + +class STPPaymentMethodSwishParamsTests: STPNetworkStubbingTestCase { + + func testCreateSwishPaymentMethod() throws { + let swishParams = STPPaymentMethodSwishParams() + + let params = STPPaymentMethodParams( + swish: swishParams, + billingDetails: nil, + metadata: nil + ) + + let exp = expectation(description: "Payment Method Swish create") + + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + client.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + exp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .swish, "Incorrect PaymentMethod type") + XCTAssertNotNil(paymentMethod?.swish, "The `swish` property must be populated") + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodSwishTests.swift b/Stripe/StripeiOSTests/STPPaymentMethodSwishTests.swift new file mode 100644 index 00000000..f7c2fbc4 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodSwishTests.swift @@ -0,0 +1,42 @@ +// +// STPPaymentMethodSwishTests.swift +// StripeiOSTests +// +// Created by Eduardo Urias on 9/21/23. +// + +import Foundation +import StripePaymentsTestUtils + +@testable @_spi(STP) import StripeCoreTestUtils +@testable @_spi(STP) import StripePayments + +class STPPaymentMethodSwishTests: STPNetworkStubbingTestCase { + + static let swishPaymentIntentClientSecret = + "pi_3Nsu6oKG6vc7r7YC1FJJPNjg_secret_wQtTkgmjgOMSqN7lje5RCtzrm" + + func _retrieveSwishJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingFRPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.swishPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + let swishJson = paymentIntent?.paymentMethod?.swish?.allResponseFields + completion(swishJson) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + _retrieveSwishJSON({ json in + let klarna = STPPaymentMethodSwish.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(klarna, "Failed to decode JSON") + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodTest.swift new file mode 100644 index 00000000..99bd2660 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodTest.swift @@ -0,0 +1,167 @@ +// +// STPPaymentMethodTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/6/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodTest: XCTestCase { + // MARK: - STPPaymentMethodType Tests + func testTypeFromString() { + XCTAssertEqual( + STPPaymentMethod.type(from: "au_becs_debit"), + STPPaymentMethodType.AUBECSDebit + ) + XCTAssertEqual( + STPPaymentMethod.type(from: "AU_BECS_DEBIT"), + STPPaymentMethodType.AUBECSDebit + ) + XCTAssertEqual(STPPaymentMethod.type(from: "BACS_DEBIT"), STPPaymentMethodType.bacsDebit) + XCTAssertEqual(STPPaymentMethod.type(from: "bacs_debit"), STPPaymentMethodType.bacsDebit) + XCTAssertEqual(STPPaymentMethod.type(from: "BACS_DEBIT"), STPPaymentMethodType.bacsDebit) + XCTAssertEqual(STPPaymentMethod.type(from: "card"), STPPaymentMethodType.card) + XCTAssertEqual(STPPaymentMethod.type(from: "CARD"), STPPaymentMethodType.card) + XCTAssertEqual(STPPaymentMethod.type(from: "ideal"), STPPaymentMethodType.iDEAL) + XCTAssertEqual(STPPaymentMethod.type(from: "IDEAL"), STPPaymentMethodType.iDEAL) + XCTAssertEqual(STPPaymentMethod.type(from: "fpx"), STPPaymentMethodType.FPX) + XCTAssertEqual(STPPaymentMethod.type(from: "FPX"), STPPaymentMethodType.FPX) + XCTAssertEqual(STPPaymentMethod.type(from: "sepa_debit"), STPPaymentMethodType.SEPADebit) + XCTAssertEqual(STPPaymentMethod.type(from: "SEPA_DEBIT"), STPPaymentMethodType.SEPADebit) + XCTAssertEqual(STPPaymentMethod.type(from: "multibanco"), STPPaymentMethodType.multibanco) + XCTAssertEqual( + STPPaymentMethod.type(from: "card_present"), + STPPaymentMethodType.cardPresent + ) + XCTAssertEqual( + STPPaymentMethod.type(from: "CARD_PRESENT"), + STPPaymentMethodType.cardPresent + ) + XCTAssertEqual(STPPaymentMethod.type(from: "unknown_string"), STPPaymentMethodType.unknown) + } + + func testTypesFromStrings() { + let rawTypes = [ + "card", + "ideal", + "card_present", + "fpx", + "sepa_debit", + "bacs_debit", + "au_becs_debit", + "multibanco", + ] + let expectedTypes: [STPPaymentMethodType] = [ + .card, + .iDEAL, + .cardPresent, + .FPX, + .SEPADebit, + .bacsDebit, + .AUBECSDebit, + .multibanco, + ] + XCTAssertEqual(STPPaymentMethod.paymentMethodTypes(from: rawTypes), expectedTypes) + } + + func testStringFromType() { + let values: [STPPaymentMethodType] = [ + .card, + .iDEAL, + .cardPresent, + .FPX, + .SEPADebit, + .bacsDebit, + .AUBECSDebit, + .OXXO, + .alipay, + .payPal, + .giropay, + .multibanco, + .unknown, + ] + for type in values { + let string = STPPaymentMethod.string(from: type) + + switch type { + case .card: + XCTAssertEqual(string, "card") + case .iDEAL: + XCTAssertEqual(string, "ideal") + case .cardPresent: + XCTAssertEqual(string, "card_present") + case .FPX: + XCTAssertEqual(string, "fpx") + case .SEPADebit: + XCTAssertEqual(string, "sepa_debit") + case .bacsDebit: + XCTAssertEqual(string, "bacs_debit") + case .AUBECSDebit: + XCTAssertEqual(string, "au_becs_debit") + case .giropay: + XCTAssertEqual(string, "giropay") + case .przelewy24: + XCTAssertEqual(string, "p24") + case .bancontact: + XCTAssertEqual(string, "bancontact") + case .EPS: + XCTAssertEqual(string, "eps") + case .OXXO: + XCTAssertEqual(string, "oxxo") + case .sofort: + XCTAssertEqual(string, "sofort") + case .alipay: + XCTAssertEqual(string, "alipay") + case .payPal: + XCTAssertEqual(string, "paypal") + case .grabPay: + XCTAssertEqual(string, "grabpay") + case .multibanco: + XCTAssertEqual(string, "multibanco") + case .unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - STPAPIResponseDecodable Tests + func testDecodedObjectFromAPIResponseRequiredFields() { + let fullJson = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard) + + XCTAssertNotNil( + STPPaymentMethod.decodedObject(fromAPIResponse: fullJson), + "can decode with full json" + ) + + let requiredFields = ["id"] + + for field in requiredFields { + var partialJson = fullJson + + XCTAssertNotNil(partialJson?[field]) + partialJson?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentIntent.decodedObject(fromAPIResponse: partialJson)) + } + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed(STPTestJSONPaymentMethodCard) + let paymentMethod = STPPaymentMethod.decodedObject(fromAPIResponse: response) + XCTAssertEqual(paymentMethod?.stripeId, "pm_123456789") + XCTAssertEqual(paymentMethod?.created, Date(timeIntervalSince1970: 123_456_789)) + XCTAssertEqual(paymentMethod!.liveMode, false) + XCTAssertEqual(paymentMethod?.type, .card) + XCTAssertNotNil(paymentMethod?.billingDetails) + XCTAssertNotNil(paymentMethod?.card) + XCTAssertNil(paymentMethod?.customerId) + XCTAssertEqual(paymentMethod!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodThreeDSecureUsageTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodThreeDSecureUsageTest.swift new file mode 100644 index 00000000..37627145 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodThreeDSecureUsageTest.swift @@ -0,0 +1,27 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodThreeDSecureUsageTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/5/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import XCTest + +class STPPaymentMethodThreeDSecureUsageTest: XCTestCase { + func testDecodedObjectFromAPIResponse() { + let response = [ + "supported": NSNumber(value: true) + ] + let requiredFields = ["supported"] + + for field in requiredFields { + var mutableResponse = response + mutableResponse.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodThreeDSecureUsage.decodedObject(fromAPIResponse: mutableResponse)) + } + XCTAssertNotNil(STPPaymentMethodThreeDSecureUsage.decodedObject(fromAPIResponse: response)) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodUPIParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodUPIParamsTest.swift new file mode 100644 index 00000000..e46cbc8c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodUPIParamsTest.swift @@ -0,0 +1,52 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodUPIParamsTest.m +// StripeiOS Tests +// +// Created by Anirudh Bhargava on 11/6/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils + +class STPPaymentMethodUPIParamsTests: STPNetworkStubbingTestCase { + func testCreateUPIPaymentMethod() { + let client = STPAPIClient(publishableKey: STPTestingINPublishableKey) + let upiParams = STPPaymentMethodUPIParams() + upiParams.vpa = "somevpa@hdfcbank" + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + + let params = STPPaymentMethodParams( + upi: upiParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value" + ]) + + let expectation = self.expectation(description: "Payment Method UPI create") + + client.createPaymentMethod( + with: params) { paymentMethod, error in + expectation.fulfill() + + XCTAssertNil(error, "Unexpected error creating UPI PaymentMethod") + XCTAssertNotNil(paymentMethod, "Failed to create UPI PaymentMethod") + XCTAssertNotNil(paymentMethod?.stripeId, "Missing stripeId") + XCTAssertNotNil(paymentMethod?.created, "Missing created") + XCTAssertFalse(paymentMethod!.liveMode, "Incorrect livemode") + XCTAssertEqual(paymentMethod?.type, .UPI, "Incorrect PaymentMethod type") + + // Billing Details + XCTAssertEqual(paymentMethod?.billingDetails!.name, "Jenny Rosen") + + // UPI Details + XCTAssertNotNil(paymentMethod?.upi, "Missing UPI") + XCTAssertEqual(paymentMethod?.upi!.vpa, "somevpa@hdfcbank") + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodUPITests.swift b/Stripe/StripeiOSTests/STPPaymentMethodUPITests.swift new file mode 100644 index 00000000..304e57dd --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodUPITests.swift @@ -0,0 +1,45 @@ +// +// STPPaymentMethodUPITests.swift +// StripeiOS Tests +// +// Created by Anirudh Bhargava on 11/10/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodUPITests: STPNetworkStubbingTestCase { + private(set) var upiJSON: [AnyHashable: Any]? + + func _retrieveUPIJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + if let upiJSON = upiJSON { + completion(upiJSON) + } else { + let client = STPAPIClient(publishableKey: STPTestingINPublishableKey) + client.retrievePaymentIntent( + withClientSecret: "pi_1HlYxxBte6TMTRd48W66zjTJ_secret_TgB7p7e7aTRbr22UT6N6KNrSm", + expand: ["payment_method"] + ) { [self] paymentIntent, _ in + upiJSON = paymentIntent?.paymentMethod?.upi?.allResponseFields + completion(upiJSON ?? [:]) + } + } + } + + func testCorrectParsing() { + let jsonExpectation = XCTestExpectation(description: "Fetch UPI JSON") + _retrieveUPIJSON({ json in + let upi = STPPaymentMethodUPI.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(upi, "Failed to decode JSON") + jsonExpectation.fulfill() + }) + wait(for: [jsonExpectation], timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsStubbedTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsStubbedTest.swift new file mode 100644 index 00000000..e58023d6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsStubbedTest.swift @@ -0,0 +1,302 @@ +// +// STPPaymentMethodUSBankAccountParamsStubbedTest.swift +// StripeiOS Tests +// +// Created by John Woo on 3/24/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeApplePay +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet + +class STPPaymentMethodUSBankAccountParamsStubbedTest: APIStubbedTestCase { + func testus_bank_account_withoutNetworks() { + let stubbedApiClient = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + } response: { _ in + let jsonText = """ + { + "id": "pm_1KgvOfFY0qyl6XeW7RfvDvxE", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "tester@example.com", + "name": "iOS CI Tester", + "phone": null + }, + "created": 1648146513, + "customer": null, + "livemode": false, + "type": "us_bank_account", + "us_bank_account": { + "account_holder_type": "individual", + "account_type": "checking", + "bank_name": "STRIPE TEST BANK", + "fingerprint": "ickfX9sbxIyAlbuh", + "last4": "6789", + "linked_account": null, + "routing_number": "110000000" + } + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + usBankAccountParams.accountHolderType = .individual + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + stubbedApiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .individual, + "`accountHolderType` should be individual" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .checking, + "`accountType` should be checking" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + XCTAssertNil(paymentMethod?.usBankAccount?.networks) + exp.fulfill() + + } + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + func testus_bank_account_networksInPayloadWithPreferred() { + + let stubbedApiClient = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + } response: { _ in + let jsonText = """ + { + "id": "pm_1KgvOfFY0qyl6XeW7RfvDvxE", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "tester@example.com", + "name": "iOS CI Tester", + "phone": null + }, + "created": 1648146513, + "customer": null, + "livemode": false, + "type": "us_bank_account", + "us_bank_account": { + "account_holder_type": "individual", + "account_type": "checking", + "bank_name": "STRIPE TEST BANK", + "fingerprint": "ickfX9sbxIyAlbuh", + "last4": "6789", + "linked_account": null, + "networks": { + "preferred": "ach", + "supported": [ + "ach" + ] + }, + "routing_number": "110000000" + } + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + usBankAccountParams.accountHolderType = .individual + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + stubbedApiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .individual, + "`accountHolderType` should be individual" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .checking, + "`accountType` should be checking" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + XCTAssertEqual(paymentMethod?.usBankAccount?.networks?.preferred, "ach") + XCTAssertEqual(paymentMethod?.usBankAccount?.networks?.supported.count, 1) + XCTAssertEqual(paymentMethod?.usBankAccount?.networks?.supported.first, "ach") + exp.fulfill() + + } + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + func testus_bank_account_networksInPayloadWithoutPreferred() { + + let stubbedApiClient = stubbedAPIClient() + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/payment_methods") ?? false + } response: { _ in + let jsonText = """ + { + "id": "pm_1KgvOfFY0qyl6XeW7RfvDvxE", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "tester@example.com", + "name": "iOS CI Tester", + "phone": null + }, + "created": 1648146513, + "customer": null, + "livemode": false, + "type": "us_bank_account", + "us_bank_account": { + "account_holder_type": "individual", + "account_type": "checking", + "bank_name": "STRIPE TEST BANK", + "fingerprint": "ickfX9sbxIyAlbuh", + "last4": "6789", + "linked_account": null, + "networks": { + "supported": [ + "ach" + ] + }, + "routing_number": "110000000" + } + } + """ + return HTTPStubsResponse( + data: jsonText.data(using: .utf8)!, + statusCode: 200, + headers: nil + ) + } + + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + usBankAccountParams.accountHolderType = .individual + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + stubbedApiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .individual, + "`accountHolderType` should be individual" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .checking, + "`accountType` should be checking" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + XCTAssertNil(paymentMethod?.usBankAccount?.networks?.preferred) + XCTAssertEqual(paymentMethod?.usBankAccount?.networks?.supported.count, 1) + XCTAssertEqual(paymentMethod?.usBankAccount?.networks?.supported.first, "ach") + exp.fulfill() + + } + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsTest.swift new file mode 100644 index 00000000..fa872ddc --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountParamsTest.swift @@ -0,0 +1,235 @@ +// +// STPPaymentMethodUSBankAccountParamsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/2/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodUSBankAccountParamsTest: STPNetworkStubbingTestCase { + + var apiClient: STPAPIClient! + override func setUp() { + super.setUp() + apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + } + + func testCreateUSBankAccountPaymentMethod_checking_individual() { + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + XCTAssertEqual(usBankAccountParams.accountTypeString, "checking") + usBankAccountParams.accountHolderType = .individual + XCTAssertEqual(usBankAccountParams.accountHolderTypeString, "individual") + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + apiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .individual, + "`accountHolderType` should be individual" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .checking, + "`accountType` should be checking" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + + exp.fulfill() + + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + func testCreateUSBankAccountPaymentMethod_savings_company() { + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .savings + XCTAssertEqual(usBankAccountParams.accountTypeString, "savings") + usBankAccountParams.accountHolderType = .company + XCTAssertEqual(usBankAccountParams.accountHolderTypeString, "company") + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + apiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .company, + "`accountHolderType` should be company" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .savings, + "`accountType` should be savings" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + + exp.fulfill() + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + func testCreateUSBankAccountPaymentMethod_checking_individual_set_with_string() { + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountTypeString = "checking" + XCTAssertEqual(usBankAccountParams.accountType, .checking) + usBankAccountParams.accountHolderTypeString = "individual" + XCTAssertEqual(usBankAccountParams.accountHolderType, .individual) + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + apiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .individual, + "`accountHolderType` should be individual" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .checking, + "`accountType` should be checking" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + + exp.fulfill() + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + func testCreateUSBankAccountPaymentMethod_savings_company_set_with_string() { + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountTypeString = "savings" + XCTAssertEqual(usBankAccountParams.accountType, .savings) + usBankAccountParams.accountHolderTypeString = "company" + XCTAssertEqual(usBankAccountParams.accountHolderType, .company) + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let params = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let exp = expectation(description: "Payment Method US Bank Account create") + + apiClient.createPaymentMethod(with: params) { + (paymentMethod: STPPaymentMethod?, error: Error?) in + + XCTAssertNil(error) + XCTAssertNotNil(paymentMethod, "Payment method should be populated") + XCTAssertEqual(paymentMethod?.type, .USBankAccount, "Incorrect PaymentMethod type") + XCTAssertNotNil( + paymentMethod?.usBankAccount, + "The `usBankAccount` property must be populated" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountHolderType, + .company, + "`accountHolderType` should be company" + ) + XCTAssertEqual( + paymentMethod?.usBankAccount?.accountType, + .savings, + "`accountType` should be savings" + ) + XCTAssertEqual(paymentMethod?.usBankAccount?.last4, "6789", "`last4` should be 6789") + + exp.fulfill() + } + + self.waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + + func testEncodingWithLinkAccountSessionID() { + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.linkAccountSessionID = "las_test" + let encoded = STPFormEncoder.dictionary(forObject: usBankAccountParams) + XCTAssertEqual( + (encoded["us_bank_account"] as? [AnyHashable: Any])?["link_account_session"] as? String, + "las_test" + ) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountTest.swift new file mode 100644 index 00000000..33460d6c --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodUSBankAccountTest.swift @@ -0,0 +1,53 @@ +// +// STPPaymentMethodUSBankAccountTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/2/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import StripeCoreTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentMethodUSBankAccountTest: STPNetworkStubbingTestCase { + + static let usBankAccountPaymentIntentClientSecret = + "pi_3KhHLqFY0qyl6XeW1X2ZMsOT_secret_k5bOLoKJEW8ZhQFpokL0OrpbU" + + func retrieveUSBankAccountJSON(_ completion: @escaping ([AnyHashable: Any]?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + client.retrievePaymentIntent( + withClientSecret: Self.usBankAccountPaymentIntentClientSecret, + expand: ["payment_method"] + ) { paymentIntent, _ in + let klarnaJson = paymentIntent?.paymentMethod?.usBankAccount?.allResponseFields + completion(klarnaJson ?? [:]) + } + } + + func testObjectDecoding() { + let retrieveJSON = XCTestExpectation(description: "Retrieve JSON") + + retrieveUSBankAccountJSON({ json in + let usBankAccount = STPPaymentMethodUSBankAccount.decodedObject(fromAPIResponse: json) + XCTAssertNotNil(usBankAccount, "Failed to decode JSON") + XCTAssertEqual(usBankAccount?.last4, "6789") + XCTAssertEqual(usBankAccount?.routingNumber, "110000000") + XCTAssertEqual(usBankAccount?.bankName, "STRIPE TEST BANK") + XCTAssertEqual(usBankAccount?.accountHolderType, .individual) + XCTAssertEqual(usBankAccount?.accountType, .checking) + XCTAssertNotNil(usBankAccount?.fingerprint) + retrieveJSON.fulfill() + }) + + wait(for: [retrieveJSON], timeout: STPTestingNetworkRequestTimeout) + } + +} diff --git a/Stripe/StripeiOSTests/STPPaymentMethodiDEALTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodiDEALTest.swift new file mode 100644 index 00000000..24f7cc65 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentMethodiDEALTest.swift @@ -0,0 +1,37 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPPaymentMethodiDEALTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 3/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +class STPPaymentMethodiDEALTest: XCTestCase { + func exampleJson() -> [AnyHashable: Any]? { + return [ + "bank": "Rabobank", + "bic": "RABONL2U", + ] + } + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = exampleJson() + response?.removeValue(forKey: field) + + XCTAssertNil(STPPaymentMethodiDEAL.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPPaymentMethodiDEAL.decodedObject(fromAPIResponse: exampleJson())) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = exampleJson() + let ideal = STPPaymentMethodiDEAL.decodedObject(fromAPIResponse: response) + XCTAssertEqual(ideal?.bankName, "Rabobank") + XCTAssertEqual(ideal?.bankIdentifierCode, "RABONL2U") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift b/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift new file mode 100644 index 00000000..08530695 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift @@ -0,0 +1,102 @@ +// +// STPPaymentOptionsViewControllerLocalizationSnapshotTests.swift +// StripeiOS Tests +// +// Created by Brian Dorfman on 10/17/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class MockSTPPaymentOptionsViewControllerDelegate: NSObject, STPPaymentOptionsViewControllerDelegate +{ + func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didFailToLoadWithError error: Error + ) { + } + + func paymentOptionsViewControllerDidFinish( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + } + + func paymentOptionsViewControllerDidCancel( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + } + +} + +class STPPaymentOptionsViewControllerLocalizationSnapshotTests: STPSnapshotTestCase { + + func performSnapshotTest(forLanguage language: String?) { + let config = STPPaymentConfiguration() + config.companyName = "Test Company" + config.requiredBillingAddressFields = .full + let theme = STPTheme.defaultTheme + let paymentMethods = [STPFixtures.paymentMethod(), STPFixtures.paymentMethod()] + let customerContext = Testing_StaticCustomerContext.init( + customer: STPFixtures.customerWithCardTokenAndSourceSources(), + paymentMethods: paymentMethods + ) + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + STPLocalizationUtils.overrideLanguage(to: language) + let paymentOptionsVC = STPPaymentOptionsViewController( + configuration: config, + theme: theme, + customerContext: customerContext, + delegate: delegate + ) + let didLoadExpectation = expectation(description: "VC did load") + + paymentOptionsVC.loadingPromise?.onSuccess({ (_) in + didLoadExpectation.fulfill() + }) + wait(for: [didLoadExpectation].compactMap { $0 }, timeout: 2) + + let viewToTest = stp_preparedAndSizedViewForSnapshotTest(from: paymentOptionsVC)! + + STPSnapshotVerifyView(viewToTest, identifier: nil) + STPLocalizationUtils.overrideLanguage(to: nil) + } + + func testGerman() { + performSnapshotTest(forLanguage: "de") + } + + func testEnglish() { + performSnapshotTest(forLanguage: "en") + } + + func testSpanish() { + performSnapshotTest(forLanguage: "es") + } + + func testFrench() { + performSnapshotTest(forLanguage: "fr") + } + + func testItalian() { + performSnapshotTest(forLanguage: "it") + } + + func testJapanese() { + performSnapshotTest(forLanguage: "ja") + } + + func testDutch() { + performSnapshotTest(forLanguage: "nl") + } + + func testChinese() { + performSnapshotTest(forLanguage: "zh-Hans") + } +} diff --git a/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerTest.swift b/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerTest.swift new file mode 100644 index 00000000..0555fa97 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPaymentOptionsViewControllerTest.swift @@ -0,0 +1,351 @@ +// +// STPPaymentOptionsViewControllerTest.swift +// StripeiOS Tests +// +// Created by Brian Dorfman on 10/10/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import OCMock + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPaymentOptionsViewControllerTest: XCTestCase { + class MockSTPPaymentOptionsViewControllerDelegate: NSObject, + STPPaymentOptionsViewControllerDelegate + { + var didFail = false + func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didFailToLoadWithError error: Error + ) { + didFail = true + } + + var didFinish = false + func paymentOptionsViewControllerDidFinish( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + didFinish = true + } + + var didCancel = false + func paymentOptionsViewControllerDidCancel( + _ paymentOptionsViewController: STPPaymentOptionsViewController + ) { + didCancel = true + } + + var didSelect = false + func paymentOptionsViewController( + _ paymentOptionsViewController: STPPaymentOptionsViewController, + didSelect paymentOption: STPPaymentOption + ) { + didSelect = true + } + } + + func buildViewController( + with customer: STPCustomer, + paymentMethods: [STPPaymentMethod], + configuration config: STPPaymentConfiguration, + delegate: STPPaymentOptionsViewControllerDelegate + ) -> STPPaymentOptionsViewController { + let mockCustomerContext = Testing_StaticCustomerContext( + customer: customer, + paymentMethods: paymentMethods + ) + return buildViewController( + with: mockCustomerContext, + configuration: config, + delegate: delegate + ) + } + + func buildViewController( + with customerContext: STPCustomerContext, + configuration config: STPPaymentConfiguration, + delegate: STPPaymentOptionsViewControllerDelegate + ) -> STPPaymentOptionsViewController { + let theme = STPTheme.defaultTheme + let vc = STPPaymentOptionsViewController( + configuration: config, + theme: theme, + customerContext: customerContext, + delegate: delegate + ) + let didLoadExpectation = expectation(description: "VC did load") + vc.loadingPromise?.onSuccess({ (_) in + didLoadExpectation.fulfill() + }) + + wait(for: [didLoadExpectation], timeout: 2) + + return vc + } + + /// When the customer has no sources, and card is the sole available payment + /// method, STPAddCardViewController should be shown. + func testInitWithNoSourcesAndConfigWithUseSourcesOffAndCardAvailable() { + let customer = STPFixtures.customerWithNoSources() + let config = STPPaymentConfiguration() + config.applePayEnabled = false + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: [], + configuration: config, + delegate: delegate + ) + XCTAssertTrue((sut.internalViewController is STPAddCardViewController)) + } + + /// When the customer has a single card token source and the available payment methods + /// are card and apple pay, STPPaymentOptionsInternalVC should be shown. + func testInitWithSingleCardTokenSourceAndCardAvailable() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + XCTAssertTrue((sut.internalViewController is STPPaymentOptionsInternalViewController)) + } + + /// When the customer has a single card source source and the available payment methods + /// are card only, STPPaymentOptionsInternalVC should be shown. + func testInitWithSingleCardSourceSourceAndCardAvailable() { + let customer = STPFixtures.customerWithSingleCardSourceSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + config.applePayEnabled = false + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + XCTAssertTrue((sut.internalViewController is STPPaymentOptionsInternalViewController)) + } + + /// Tapping cancel in an internal AddCard view controller should result in a call to + /// didCancel: + func testAddCardCancelForwardsToDelegate() { + let customer = STPFixtures.customerWithNoSources() + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: [], + configuration: config, + delegate: delegate + ) + XCTAssertTrue((sut.internalViewController is STPAddCardViewController)) + let cancelButton = sut.internalViewController?.navigationItem.leftBarButtonItem + _ = cancelButton?.target?.perform(cancelButton?.action, with: cancelButton) + + XCTAssertTrue(delegate.didCancel) + } + + /// Tapping cancel in an internal PaymentOptionsInternal view controller should + /// result in a call to didCancel: + func testInternalCancelForwardsToDelegate() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + XCTAssertTrue((sut.internalViewController is STPPaymentOptionsInternalViewController)) + let cancelButton = sut.internalViewController?.navigationItem.leftBarButtonItem + _ = cancelButton?.target?.perform(cancelButton?.action, with: cancelButton) + + XCTAssertTrue(delegate.didCancel) + } + + /// When an AddCard view controller creates a card payment method, it should be attached to the + /// customer and the correct delegate methods should be called. + func testAddCardAttachesToCustomerAndFinishes() { + let config = STPPaymentConfiguration() + let customer = STPFixtures.customerWithNoSources() + let mockCustomerContext = Testing_StaticCustomerContext( + customer: customer, + paymentMethods: [] + ) + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: mockCustomerContext, + configuration: config, + delegate: delegate + ) + XCTAssertNotNil(sut.view) + XCTAssertTrue((sut.internalViewController is STPAddCardViewController)) + + let internalVC = sut.internalViewController as? STPAddCardViewController + let exp = expectation(description: "completion") + let expectedPaymentMethod = STPFixtures.paymentMethod() + internalVC?.delegate?.addCardViewController( + internalVC!, + didCreatePaymentMethod: expectedPaymentMethod + ) { error in + XCTAssertNil(error) + exp.fulfill() + } + + let _: ((Any?) -> Bool)? = { obj in + let paymentMethod = obj as? STPPaymentMethod + return paymentMethod?.stripeId == expectedPaymentMethod.stripeId + } + XCTAssertTrue(mockCustomerContext.didAttach) + XCTAssertTrue(delegate.didSelect) + XCTAssertTrue(delegate.didFinish) + waitForExpectations(timeout: 2, handler: nil) + } + + // Tests for race condition where the promise for fetching payment methods + // finishes in the context of intializing the sut, and `addCardViewControllerFooterView` + // is set directly after init, while internalViewController is `STPAddCardViewController` + func testSetAfterInit_addCardViewControllerFooterView_STPAddCardViewController() { + let customer = STPFixtures.customerWithNoSources() + let config = STPPaymentConfiguration() + config.applePayEnabled = false + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: [], + configuration: config, + delegate: delegate + ) + sut.addCardViewControllerFooterView = UIView() + guard let payMethodsInternal = sut.internalViewController as? STPAddCardViewController else { + XCTFail() + return + } + XCTAssertNotNil(payMethodsInternal.customFooterView) + } + + // Tests for race condition where the promise for fetching payment methods + // finishes in the context of intializing the sut, and the `paymentOptionsViewControllerFooterView` + // is set directly after init, while internalViewController is `STPPaymentOptionsInternalViewController` + func testSetAfterInit_paymentOptionsViewControllerFooterView_STPPaymentOptionsInternalViewController() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + sut.paymentOptionsViewControllerFooterView = UIView() + guard let payMethodsInternal = sut.internalViewController as? STPPaymentOptionsInternalViewController else { + XCTFail() + return + } +#if compiler(>=5.7) + XCTAssertNotNil(payMethodsInternal.customFooterView) +#endif + } + + // Tests for race condition where the promise for fetching payment methods + // finishes in the context of init the sut, and the `addCardViewControllerFooterView` + // is set directly after init, while internalViewController is `STPPaymentOptionsInternalViewController` + func testSetAfterInit_addCardViewControllerFooterView_STPPaymentOptionsInternalViewController() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + sut.addCardViewControllerFooterView = UIView() + guard let payMethodsInternal = sut.internalViewController as? STPPaymentOptionsInternalViewController else { + XCTFail() + return + } +#if compiler(>=5.7) + XCTAssertNotNil(payMethodsInternal.addCardViewControllerCustomFooterView) +#endif + } + + // Tests for race condition where the promise for fetching payment methods + // finishes in the context of init the sut, and the `prefilledInformation` + // is set directly after init, while internalViewController is `STPPaymentOptionsInternalViewController` + func testSetAfterInit_prefilledInformation_STPPaymentOptionsInternalViewController() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()] + let config = STPPaymentConfiguration() + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: paymentMethods.compactMap { $0 }, + configuration: config, + delegate: delegate + ) + let userInformation = STPUserInformation() + let address = STPAddress() + address.name = "John Doe" + address.line1 = "123 Main" + address.city = "Seattle" + address.state = "Washington" + address.postalCode = "98104" + address.phone = "2065551234" + userInformation.billingAddress = address + sut.prefilledInformation = userInformation + guard let payMethodsInternal = sut.internalViewController as? STPPaymentOptionsInternalViewController else { + XCTFail() + return + } +#if compiler(>=5.7) + XCTAssertNotNil(payMethodsInternal.prefilledInformation) +#endif + } + + // Tests for race condition where the promise for fetching payment methods + // finishes in the context of init the sut, and the `prefilledInformation` + // is set directly after init, while internalViewController is `STPAddCardViewController` + func testSetAfterInit_prefilledInformation_STPAddCardViewController() { + let customer = STPFixtures.customerWithNoSources() + let config = STPPaymentConfiguration() + config.applePayEnabled = false + let delegate = MockSTPPaymentOptionsViewControllerDelegate() + let sut = buildViewController( + with: customer, + paymentMethods: [], + configuration: config, + delegate: delegate + ) + let userInformation = STPUserInformation() + let address = STPAddress() + address.name = "John Doe" + address.line1 = "123 Main" + address.city = "Seattle" + address.state = "Washington" + address.postalCode = "98104" + address.phone = "2065551234" + userInformation.billingAddress = address + sut.prefilledInformation = userInformation + guard let payMethodsInternal = sut.internalViewController as? STPAddCardViewController else { + XCTFail() + return + } + XCTAssertNotNil(payMethodsInternal.prefilledInformation) + } +} diff --git a/Stripe/StripeiOSTests/STPPhoneNumberValidatorTest.swift b/Stripe/StripeiOSTests/STPPhoneNumberValidatorTest.swift new file mode 100644 index 00000000..a1baab25 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPhoneNumberValidatorTest.swift @@ -0,0 +1,137 @@ +// +// STPPhoneNumberValidatorTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 3/22/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +private let kUSCountryCode = "US" +private let kUKCountryCode = "UK" +class STPPhoneNumberValidatorTest: XCTestCase { + func testValidPhoneNumbers() { + XCTAssertTrue( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "555-555-5555", + forCountryCode: kUSCountryCode + ) + ) + XCTAssertTrue( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "5555555555", + forCountryCode: kUSCountryCode + ) + ) + XCTAssertTrue( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "(555) 555-5555", + forCountryCode: kUSCountryCode + ) + ) + } + + func testInvalidPhoneNumbers() { + XCTAssertFalse( + STPPhoneNumberValidator.stringIsValidPhoneNumber("", forCountryCode: kUSCountryCode) + ) + XCTAssertFalse( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "555-555-555", + forCountryCode: kUSCountryCode + ) + ) + XCTAssertFalse( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "555-555-A555", + forCountryCode: kUSCountryCode + ) + ) + XCTAssertTrue( + STPPhoneNumberValidator.stringIsValidPhoneNumber( + "55555555555", + forCountryCode: kUSCountryCode + ) + ) + } + + func testFormattedSanitizedPhoneNumberForString() { + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "55", + forCountryCode: kUSCountryCode + ), + "55" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "555", + forCountryCode: kUSCountryCode + ), + "(555) " + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "55555", + forCountryCode: kUSCountryCode + ), + "(555) 55" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "A-55555", + forCountryCode: kUSCountryCode + ), + "(555) 55" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "5555555", + forCountryCode: kUSCountryCode + ), + "(555) 555-5" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "5555555555", + forCountryCode: kUSCountryCode + ), + "(555) 555-5555" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "5555555555123", + forCountryCode: kUSCountryCode + ), + "(555) 555-5555" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedSanitizedPhoneNumber( + for: "5555555555123", + forCountryCode: kUKCountryCode + ), + "5555555555123" + ) + } + + func testFormattedRedactedPhoneNumberForString() { + XCTAssertEqual( + STPPhoneNumberValidator.formattedRedactedPhoneNumber( + for: "+1******1234", + forCountryCode: kUSCountryCode + ), + "+1 (•••) •••-1234" + ) + XCTAssertEqual( + STPPhoneNumberValidator.formattedRedactedPhoneNumber( + for: "+86******1234", + forCountryCode: kUKCountryCode + ), + "+86 ••••••1234" + ) + } +} diff --git a/Stripe/StripeiOSTests/STPPinManagementServiceFunctionalTest.swift b/Stripe/StripeiOSTests/STPPinManagementServiceFunctionalTest.swift new file mode 100644 index 00000000..8f011b14 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPinManagementServiceFunctionalTest.swift @@ -0,0 +1,270 @@ +// +// STPPinManagementServiceFunctionalTest.swift +// StripeiOS Tests +// +// Created by Arnaud Cavailhez on 4/29/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +import PassKit +import XCTest + +import OHHTTPStubs +import OHHTTPStubsSwift +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class TestEphemeralKeyProvider: NSObject, STPIssuingCardEphemeralKeyProvider { + func createIssuingCardKey( + withAPIVersion apiVersion: String, + completion: STPJSONResponseCompletionBlock + ) { + print("apiVersion \(apiVersion)") + let response = + [ + "id": "ephkey_token", + "object": "ephemeral_key", + "associated_objects": [ + [ + "type": "issuing.card", + "id": "ic_token", + ], + ], + "created": NSNumber(value: 1_556_656_558), + "expires": NSNumber(value: 1_556_660_158), + "livemode": NSNumber(value: true), + "secret": "ek_live_secret", + ] as [String: Any] + completion(response, nil) + } +} + +class STPPinManagementServiceFunctionalTest: APIStubbedTestCase { + + func testRetrievePin() { + let keyProvider = TestEphemeralKeyProvider() + let service = STPPinManagementService(keyProvider: keyProvider) + + let expectation = self.expectation(description: "Received PIN") + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/v1/issuing/cards/ic_token/pin") ?? false + } response: { _ in + let pinResponseJSON = """ + { + "pin" : "2345", + "object" : "issuing.card_pin", + "card" : { + "id" : "ic_token", + "last4" : "1234", + "livemode" : true, + "shipping" : null, + "metadata" : { + + }, + "brand" : "Visa", + "authorization_controls" : { + "max_approvals" : null, + "currency" : null, + "allowed_categories" : null, + "spending_limits" : null, + "blocked_categories" : null, + "max_amount" : null + }, + "type" : "virtual", + "cardholder" : { + "id" : "ich_token", + "livemode" : true, + "phone_number" : "+1415", + "metadata" : { + + }, + "authorization_controls" : { + "blocked_categories" : [ + + ], + "spending_limits" : [ + + ], + "allowed_categories" : [ + + ] + }, + "type" : "individual", + "object" : "issuing.cardholder", + "billing" : { + "address" : { + "state" : "CA", + "country" : "US", + "line2" : "123", + "city" : "San Francisco", + "line1" : "510 Townsend St", + "postal_code" : "94103" + }, + "name" : "Arnaud Cavailhez" + }, + "created" : 1536780742, + "is_default" : false, + "email" : "acavailhez@stripe.com", + "name" : "Arnaud Cavailhez", + "status" : "active" + }, + "object" : "issuing.card", + "exp_month" : 9, + "exp_year" : 2021, + "created" : 1536781947, + "currency" : "usd", + "name" : "Arnaud Cavailhez", + "status" : "active" + } + } + """ + return HTTPStubsResponse(data: pinResponseJSON.data(using: .utf8)!, statusCode: 200, headers: nil) + } + + service.retrievePin( + "ic_token", + verificationId: "iv_token", + oneTimeCode: "123456" + ) { cardPin, status, error in + if error == nil && status == .success && (cardPin?.pin == "2345") { + expectation.fulfill() + } + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testUpdatePin() { + let keyProvider = TestEphemeralKeyProvider() + let service = STPPinManagementService(keyProvider: keyProvider) + + let expectation = self.expectation(description: "Received PIN") + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/v1/issuing/cards/ic_token/pin") ?? false + } response: { _ in + let pinResponseJSON = """ + { + "pin" : "3456", + "object" : "issuing.card_pin", + "card" : { + "id" : "ic_token", + "last4" : "1234", + "livemode" : true, + "replacement_for" : null, + "metadata" : { + + }, + "brand" : "Visa", + "shipping" : null, + "authorization_controls" : { + "max_approvals" : null, + "currency" : null, + "allowed_categories" : null, + "spending_limits" : null, + "blocked_categories" : null, + "max_amount" : null + }, + "replacement_reason" : null, + "type" : "virtual", + "cardholder" : { + "id" : "ich_token", + "livemode" : true, + "phone_number" : "+1415", + "metadata" : { + + }, + "authorization_controls" : { + "blocked_categories" : [ + + ], + "spending_limits" : [ + + ], + "allowed_categories" : [ + + ] + }, + "type" : "individual", + "object" : "issuing.cardholder", + "billing" : { + "address" : { + "state" : "CA", + "country" : "US", + "line2" : "123", + "city" : "San Francisco", + "line1" : "510 Townsend St", + "postal_code" : "94103" + }, + "name" : "Arnaud Cavailhez" + }, + "created" : 1536780742, + "is_default" : false, + "email" : "acavailhez@stripe.com", + "name" : "Arnaud Cavailhez", + "status" : "active" + }, + "object" : "issuing.card", + "exp_month" : 9, + "exp_year" : 2021, + "created" : 1536781947, + "currency" : "usd", + "name" : "Arnaud Cavailhez", + "status" : "active" + } + } + """ + return HTTPStubsResponse(data: pinResponseJSON.data(using: .utf8)!, statusCode: 200, headers: nil) + } + + service.updatePin( + "ic_token", + newPin: "3456", + verificationId: "iv_token", + oneTimeCode: "123-456" + ) { cardPin, status, error in + if error == nil && status == .success && (cardPin?.pin == "3456") { + expectation.fulfill() + } + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testRetrievePinWithError() { + let keyProvider = TestEphemeralKeyProvider() + let service = STPPinManagementService(keyProvider: keyProvider) + + let expectation = self.expectation(description: "Received Error") + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/v1/issuing/cards/ic_token/pin") ?? false + } response: { _ in + let pinResponseJSON = """ + { + "error" : { + "message" : "Verification challenge does not exist or is already redeemed", + "type" : "invalid_request_error", + "code" : "already_redeemed" + } + } + """ + return HTTPStubsResponse(data: pinResponseJSON.data(using: .utf8)!, statusCode: 400, headers: nil) + } + + service.retrievePin( + "ic_token", + verificationId: "iv_token", + oneTimeCode: "123456" + ) { _, status, _ in + if status == .errorVerificationAlreadyRedeemed { + expectation.fulfill() + } + } + waitForExpectations(timeout: 5.0, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldFormatterTests.swift b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldFormatterTests.swift new file mode 100644 index 00000000..5a664d9e --- /dev/null +++ b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldFormatterTests.swift @@ -0,0 +1,58 @@ +// +// STPPostalCodeInputTextFieldFormatterTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/30/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPostalCodeInputTextFieldFormatterTests: XCTestCase { + + func testIsAllowedInput() { + let formatter = STPPostalCodeInputTextFieldFormatter() + formatter.countryCode = "US" + XCTAssertTrue(formatter.isAllowedInput("10002", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("21218", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("10002-1234", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("100021234", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("ABC10002", to: "", at: NSRange(location: 0, length: 0))) + + XCTAssertFalse(formatter.isAllowedInput("1", to: "100021234", at: NSRange(location: 10, length: 0))) + XCTAssertFalse(formatter.isAllowedInput("1", to: "10002", at: NSRange(location: 4, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("1", to: "1000", at: NSRange(location: 4, length: 0))) + + formatter.countryCode = "UK" + XCTAssertTrue(formatter.isAllowedInput("10002-1234", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("100021234", to: "", at: NSRange(location: 0, length: 0))) + XCTAssertTrue(formatter.isAllowedInput("ABC10002", to: "", at: NSRange(location: 0, length: 0))) + } + + func testFormattedString() { + let formatter = STPPostalCodeInputTextFieldFormatter() + formatter.countryCode = "US" + + XCTAssertEqual( + NSAttributedString(string: ""), + formatter.formattedText(from: "- ", with: [:]) + ) + XCTAssertEqual( + NSAttributedString(string: "10002"), + formatter.formattedText(from: "10002-1234", with: [:]) + ) + + formatter.countryCode = "UK" + XCTAssertEqual( + NSAttributedString(string: "A B C D E F G"), + formatter.formattedText(from: " a b c d e f g", with: [:]) + ) + } + +} diff --git a/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldSnapshotTests.swift b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldSnapshotTests.swift new file mode 100644 index 00000000..90801be6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldSnapshotTests.swift @@ -0,0 +1,73 @@ +// +// STPPostalCodeInputTextFieldSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/30/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPostalCodeInputTextFieldSnapshotTests: STPSnapshotTestCase { + + func testEmpty() { + let field = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + field.sizeToFit() + field.frame.size.width = 200 + + STPSnapshotVerifyView(field) + } + + func testIncomplete() { + let field = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + field.sizeToFit() + field.frame.size.width = 200 + field.countryCode = "US" + field.text = "1" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testValidUS() { + let field = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + field.sizeToFit() + field.frame.size.width = 200 + field.countryCode = "US" + field.text = "12345" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testValidUK() { + let field = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + field.sizeToFit() + field.frame.size.width = 200 + field.countryCode = "UK" + field.text = "abcdef" + field.textDidChange() + + STPSnapshotVerifyView(field) + } + + func testInvalid() { + let field = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + field.sizeToFit() + field.frame.size.width = 200 + field.countryCode = "US" + field.text = "12-3456789" + field.textDidChange() + // manually set because the formatter prevents setting invalid text + field.validator.validationState = .invalid(errorMessage: nil) + + STPSnapshotVerifyView(field) + } +} diff --git a/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldTests.swift b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldTests.swift new file mode 100644 index 00000000..d9ce772a --- /dev/null +++ b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldTests.swift @@ -0,0 +1,69 @@ +// +// STPPostalCodeInputTextFieldTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 9/3/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPostalCodeInputTextFieldTests: XCTestCase { + + func testClearingInvalidPostalCodeAfterCountryChange() { + let postalCodeField = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + postalCodeField.countryCode = "UK" + postalCodeField.text = "DL12" // valid UK post code, invalid US ZIP Code + + // Change country + postalCodeField.countryCode = "US" + + XCTAssertEqual( + postalCodeField.text, + "", + "Postal code field should clear its value if no longer valid after country change" + ) + } + + func testPreservingValidPostalCodeAfterCountryChange() { + let postalCodeField = STPPostalCodeInputTextField(postalCodeRequirement: .standard) + postalCodeField.countryCode = "US" + postalCodeField.text = "10010" // valid US and HR ZIP/postal code + + // Change country + postalCodeField.countryCode = "HR" + + XCTAssertEqual( + postalCodeField.text, + "10010", + "Postal code field should preserve its value if it is still valid after country change" + ) + } + + func testChangeToNonRequiredPostalCodeIsValid() { + let postalCodeField = STPPostalCodeInputTextField(postalCodeRequirement: .upe) + // given that the postal code field is empty... + + // when + postalCodeField.countryCode = "US" + if case .incomplete = postalCodeField.validationState { + // pass + } else { + XCTFail("Empty postal code should be incomplete for US") + } + + // when + postalCodeField.countryCode = "FR" + if case .valid = postalCodeField.validationState { + // pass + } else { + XCTFail("Empty postal code should be valid for non-required country") + } + } +} diff --git a/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldValidatorTests.swift b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldValidatorTests.swift new file mode 100644 index 00000000..edee1d92 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPostalCodeInputTextFieldValidatorTests.swift @@ -0,0 +1,79 @@ +// +// STPPostalCodeInputTextFieldValidatorTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/30/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPostalCodeInputTextFieldValidatorTests: XCTestCase { + + func testValidation() { + let validator = STPPostalCodeInputTextFieldValidator(postalCodeRequirement: .standard) + validator.countryCode = "US" + + validator.inputValue = nil + XCTAssertEqual( + STPValidatedInputState.incomplete(description: nil), + validator.validationState + ) + + validator.inputValue = "" + XCTAssertEqual( + STPValidatedInputState.incomplete(description: nil), + validator.validationState + ) + + validator.inputValue = "1234" + XCTAssertEqual( + STPValidatedInputState.incomplete(description: "Your ZIP is incomplete."), + validator.validationState + ) + + validator.inputValue = "12345" + XCTAssertEqual(STPValidatedInputState.valid(message: nil), validator.validationState) + + validator.inputValue = "12345678" + XCTAssertEqual( + STPValidatedInputState.incomplete(description: "Your ZIP is incomplete."), + validator.validationState + ) + + validator.inputValue = "123456789" + XCTAssertEqual(STPValidatedInputState.valid(message: nil), validator.validationState) + + validator.inputValue = "12345-6789" + XCTAssertEqual(STPValidatedInputState.valid(message: nil), validator.validationState) + + validator.inputValue = "12-3456789" + XCTAssertEqual( + STPValidatedInputState.invalid(errorMessage: "Your ZIP is invalid."), + validator.validationState + ) + + validator.inputValue = "12345-" + XCTAssertEqual( + STPValidatedInputState.incomplete(description: "Your ZIP is incomplete."), + validator.validationState + ) + + validator.inputValue = "hi" + XCTAssertEqual( + STPValidatedInputState.invalid(errorMessage: "Your ZIP is invalid."), + validator.validationState + ) + + validator.countryCode = "UK" + validator.inputValue = "hi" + XCTAssertEqual(STPValidatedInputState.valid(message: nil), validator.validationState) + } + +} diff --git a/Stripe/StripeiOSTests/STPPostalCodeValidatorTest.swift b/Stripe/StripeiOSTests/STPPostalCodeValidatorTest.swift new file mode 100644 index 00000000..04c47d73 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPostalCodeValidatorTest.swift @@ -0,0 +1,103 @@ +// +// STPPostalCodeValidatorTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 4/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPPostalCodeValidatorTest: XCTestCase { + func testValidUSPostalCodes() { + let codes = ["10002", "10002-1234", "100021234", "21218"] + for code in codes { + XCTAssertEqual( + STPPostalCodeValidator.validationState( + forPostalCode: code, + countryCode: "US" + ), + .valid + ) + } + } + + func testInvalidUSPostalCodes() { + let codes = ["100A03", "12345-12345", "1234512345", "$$$$$", "foo"] + for code in codes { + XCTAssertEqual( + STPPostalCodeValidator.validationState( + forPostalCode: code, + countryCode: "US" + ), + .invalid + ) + } + } + + func testIncompleteUSPostalCodes() { + let codes = ["", "123", "12345-", "12345-12"] + for code in codes { + XCTAssertEqual( + STPPostalCodeValidator.validationState( + forPostalCode: code, + countryCode: "US" + ), + .incomplete + ) + } + } + + func testValidGenericPostalCodes() { + let codes = ["ABC10002", "10002-ABCD", "ABCDE"] + for code in codes { + XCTAssertEqual( + STPPostalCodeValidator.validationState( + forPostalCode: code, + countryCode: "UK" + ), + .valid + ) + } + } + + func testIncompleteGenericPostalCodes() { + let codes = [""] + for code in codes { + XCTAssertEqual( + STPPostalCodeValidator.validationState( + forPostalCode: code, + countryCode: "UK" + ), + .incomplete + ) + } + } + + func testPostalCodeIsRequiredForUPE_nil() { + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: nil)) + } + + func testPostalCodeIsRequiredForUPE_empty() { + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: "")) + } + + func testPostalCodeIsRequiredForUPE_CA() { + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: "CA")) + } + + func testPostalCodeIsRequiredForUPE_GB() { + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: "GB")) + } + + func testPostalCodeIsRequiredForUPE_US() { + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: "CA")) + } + + func testPostalCodeIsRequiredForUPE_DK() { + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequiredForUPE(forCountryCode: "DK")) + } +} diff --git a/Stripe/StripeiOSTests/STPPushProvisioningDetailsFunctionalTest.swift b/Stripe/StripeiOSTests/STPPushProvisioningDetailsFunctionalTest.swift new file mode 100644 index 00000000..6ccf1578 --- /dev/null +++ b/Stripe/StripeiOSTests/STPPushProvisioningDetailsFunctionalTest.swift @@ -0,0 +1,73 @@ +// +// STPPushProvisioningDetailsFunctionalTest.swift +// StripeiOS Tests +// +// Created by Jack Flintermann on 11/30/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Stripe + +import OHHTTPStubs +import OHHTTPStubsSwift +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +import StripeCoreTestUtils +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPPushProvisioningDetailsFunctionalTest: APIStubbedTestCase { + + func testRetrievePushProvisioningDetails() { + // this API requires a secret key - replace the key below if you need to re-record the network traffic. + let client = STPAPIClient(publishableKey: "pk_test_REPLACEME") + let cardId = "ic_1C0Xig4JYtv6MPZK91WoXa9u" + let cert1 = + "MIID/TCCA6OgAwIBAgIIGM2CpiS9WyYwCgYIKoZIzj0EAwIwgYAxNDAyBgNVBAMMK0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zIENBIC0gRzIxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0xODA2MDEyMjE0MTVaFw0yMDA2MzAyMjE0MTVaMGwxMjAwBgNVBAMMKWVjYy1jcnlwdG8tc2VydmljZXMtZW5jaXBoZXJtZW50X1VDNi1QUk9EMRQwEgYDVQQLDAtpT1MgU3lzdGVtczETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASzCVyQGX3syyW2aI6nyfNQe+vjjzjU4rLO0ZiWiVZZSmEzYfACFI8tuDFiDLv9XWrHEeX0/yNtGVjwAzpanWb/o4ICGDCCAhQwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSEtoTMOoZichZZlOgao71I3zrfCzBHBggrBgEFBQcBAQQ7MDkwNwYIKwYBBQUHMAGGK2h0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDMtYXBwbGV3d2RyY2EyMDUwggEdBgNVHSAEggEUMIIBEDCCAQwGCSqGSIb3Y2QFATCB/jCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA2BggrBgEFBQcCARYqaHR0cDovL3d3dy5hcHBsZS5jb20vY2VydGlmaWNhdGVhdXRob3JpdHkvMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwuYXBwbGUuY29tL2FwcGxld3dkcmNhMi5jcmwwHQYDVR0OBBYEFI5aYtQKaJCRpvI1Dgh+Ra4x2iCrMA4GA1UdDwEB/wQEAwIDKDASBgkqhkiG92NkBicBAf8EAgUAMAoGCCqGSM49BAMCA0gAMEUCIAY/9gwN/KAAw3EtW3NyeX1UVM3fO+wVt0cbeHL8eM/mAiEAppLm5O/2Ox8uHkxI4U/kU5vDhJA21DRbzm2rsYN+EcQ=" + let cert2 = + "MIIC9zCCAnygAwIBAgIIb+/Y9emjp+4wCgYIKoZIzj0EAwIwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNTA2MjM0MzI0WhcNMjkwNTA2MjM0MzI0WjCBgDE0MDIGA1UEAwwrQXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ0EgLSBHMjEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3fC3BkvP3XMEE8RDiQOTgPte9nStQmFSWAImUxnIYyIHCVJhysTZV+9tJmiLdJGMxPmAaCj8CWjwENrp0C7JGqOB9zCB9DBGBggrBgEFBQcBAQQ6MDgwNgYIKwYBBQUHMAGGKmh0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDQtYXBwbGVyb290Y2FnMzAdBgNVHQ4EFgQUhLaEzDqGYnIWWZToGqO9SN863wswDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS7sN6hWDOImqSKmd6+veuv2sskqzA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY3JsLmFwcGxlLmNvbS9hcHBsZXJvb3RjYWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEAYKKoZIhvdjZAYCDwQCBQAwCgYIKoZIzj0EAwIDaQAwZgIxANmxxzHGI/ZPTdDZR8V9GGkRh3En02it4Jtlmr5s3z9GppAJvm6hOyywUYlBPIfSvwIxAPxkUolLPF2/axzCiZgvcq61m6oaCyNUd1ToFUOixRLal1BzfF7QbrJcYlDXUfE6Wg==" + let nonce = "ea85a73a" + let nonceSignature = + "QBfCqTvDhmRcwqxJF3fDqzhXezIpwrpHFcOMw7/DvGVBwpfCuicwwqHCmMKYMD06w754wrjChcObwqjDr8K9wqxxUydQaMOyfsKGZMK4AcKMwqNfwoHDlcKLHsO5w7JqQiHDln7Du8KUNMOnwqpGwq/CqcKswo1Lw7s=" + let certs: [Data] = [ + Data(base64Encoded: cert1, options: [])!, + Data(base64Encoded: cert2, options: [])!, + ] + let expectation = self.expectation(description: "Push provisioning details") + + stub { urlRequest in + return urlRequest.url?.absoluteString.contains("/v1/issuing/cards/ic_1C0Xig4JYtv6MPZK91WoXa9u/push_provisioning_details") ?? false + } response: { _ in + let pushDetailsResponseJSON = """ + { + "activation_data" : "TUJQQUMtMS1GSy00MDU1NTEuMS0tVERFQS1FNUVCNUJCQjhGMENDMjdCQjU5MUNCRTdCMTVDN0U2RjBENTQ5RDM1NkZERTRDQUZBNDBGOUNCMTA1MzI5NUQ4N0RBRTk0MTE0QTQyNENDQTY0NDAxRTFCQTExOTRBRDM=", + "object" : "issuing.push_provisioning_details", + "ephemeral_public_key" : "BHdLb5BNpoPrh9Btay8LwQ5oELoziQwJL7HagE3xB5mrbdgLa5iiwogu34Y32\\/xBEviaN31s\\/ONRXSetYT745ig=", + "card" : "ic_1C0Xig4JYtv6MPZK91WoXa9u", + "contents" : "UYeMxRqiYrjVzwqKcCGRVbgFXRspbDIKkWWly8e55caWkHYmO2DNnFtqD3y5S6bvGbe+bkNPCIkT1UzQQChCQOkb0P+cbVDBLaFGaDeJW0Mhca8\\/6GwbUx9lp4H3czqszG504PkiA89dNvnbtwUmlQOpk+B\\/IAMnkaXjD2xUBUtPX9xEr5EvckkDSHHFmpy5rbGfqnWsPbJNPwUiE+6mYbt643DqF9RpmgdFN84DImuMU1W0xshbkN7voq63L\\/6UgTW7liTzWVzKUT36TtaTw5TGKVf1Niqu5CHNu2NpDEnzrvcwUCgphxRVgezJyFfq1NjhZVlGA2nUKZuRvc\\/XjBwdE0fr4Enw5XfHbRQHorpv\\/S2rX4Cmn4VHJE1JDHWK3Wrn4HmMOCH+psVi+T5hPvy6+\\/v+0zRRmGGeFKQEx0soItHiQauN2\\/zO4QoC2DCQOAKGj1KSzqHhTgdxBcBu4TIOQRsIXu6zk1ItenHIdq4thD8vF8m3wgJ8Y3KcG3TwwgbjomxOjO4rX0AA9q2V6w0TXWQ1eWC8WfX11J30Zt\\/SbyYHoU8KrIdM2ANcOvIFHENnUNBcL7AO0+tjv9lVO7M9w7hKMiVJOnDGMeH2OQfnTBOJI8SEHGm2kDRNw\\/5+VGJ7pHA1wZ9y2IS6EvY8IeNhqZ7HdBXhn18X4AWThZqmNvGuZoEU\\/ZlPmfed\\/c0BgqSw5x6K9GuC5G9Nyee3O1E6wETf4Z3goNbFnRYNJ8m+A4DxUKbvhhRSNva4d\\/lRkoNuQj2ztPiLSAPVt1H7Dk3ryeyXCrqprHsjn5T35jXFOlm5whuyeGgeXWt3DUxsYXZGTiohZyU3bZELqW3EUSwjwQ==" + } + """ + return HTTPStubsResponse(data: pushDetailsResponseJSON.data(using: .utf8)!, statusCode: 200, headers: nil) + } + + let params = STPPushProvisioningDetailsParams( + cardId: "ic_1C0Xig4JYtv6MPZK91WoXa9u", + certificates: certs, + nonce: Data(base64Encoded: nonce, options: [])!, + nonceSignature: Data(base64Encoded: nonceSignature, options: [])! + ) + // To re-record this test, get an ephemeral key for the above Issuing card and pass that instead of [STPFixtures ephemeralKey] + let ephemeralKey = STPFixtures.ephemeralKey() + client.retrievePushProvisioningDetails(with: params, ephemeralKey: ephemeralKey) { + details, + error in + expectation.fulfill() + XCTAssertNil(error) + XCTAssert((details?.cardId == cardId)) + XCTAssertEqual(details, details) + } + waitForExpectations(timeout: 5.0, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPRadarSessionFunctionalTest.swift b/Stripe/StripeiOSTests/STPRadarSessionFunctionalTest.swift new file mode 100644 index 00000000..29bfe220 --- /dev/null +++ b/Stripe/StripeiOSTests/STPRadarSessionFunctionalTest.swift @@ -0,0 +1,57 @@ +// +// STPRadarSessionFunctionalTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/20/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +import StripePaymentsTestUtils +@testable@_spi(STP) import StripePaymentsUI + +class STPRadarSessionFunctionalTest: XCTestCase { + func testCreateWithoutInitialFraudDetection() { + // When fraudDetectionData is empty... + FraudDetectionData.shared.reset() + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let exp1 = expectation(description: "Create RadarSession") + client.createRadarSession { session, error in + // ...creates a Radar Session + XCTAssertNil(error) + guard let session = session else { + XCTFail() + return + } + XCTAssertTrue(session.id.count > 0) + exp1.fulfill() + } + + waitForExpectations(timeout: 10, handler: nil) + + // Now that fraudDetectionData is populated... + XCTAssertNotNil(FraudDetectionData.shared.sid) + XCTAssertNotNil(FraudDetectionData.shared.muid) + XCTAssertNotNil(FraudDetectionData.shared.guid) + + let exp2 = expectation(description: "Create RadarSession again") + client.createRadarSession { session, error in + // ...still creates a Radar Session + XCTAssertNil(error) + guard let session = session else { + XCTFail() + return + } + XCTAssertTrue(session.id.count > 0) + exp2.fulfill() + } + + waitForExpectations(timeout: 10, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPRedirectContextTest.swift b/Stripe/StripeiOSTests/STPRedirectContextTest.swift new file mode 100644 index 00000000..608bd52a --- /dev/null +++ b/Stripe/StripeiOSTests/STPRedirectContextTest.swift @@ -0,0 +1,625 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPRedirectContextTest.m +// Stripe +// +// Created by Ben Guo on 4/6/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import SafariServices +@_spi(STP) @testable import StripeCore +@testable import StripePayments + +class MockUIViewController: UIViewController { + var presentChecker: (UIViewController) -> Bool = { _ in return true } + var presentCalled: Bool = false + var dismiss: () -> Void = { } + override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + presentCalled = true + if !presentChecker(viewControllerToPresent) { + XCTFail("checker failed") + } + super.present(viewControllerToPresent, animated: flag, completion: completion) + } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + super.dismiss(animated: flag, completion: completion) + dismiss() + } +} + +class MockUIApplication: UIApplicationProtocol { + var openHandler: (URL, ((Bool) -> Void)?) -> Void = { _, completion in completion?(true) } + var openCalled: Bool = false + + func _open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { + openCalled = true + openHandler(url, completion) + } +} + +/* + NOTE: + + If you are adding a test make sure your context unsubscribes from notifications + before your test ends. Otherwise notifications fired from other tests can cause + a reaction in an earlier, completed test and cause strange failures. + + Possible ways to do this: + 1. Your sut should already be calling unsubscribe, verified by OCMVerify + - you're good + 2. Your sut doesn't call unsubscribe as part of the test but it's not explicitly + disallowed - call [sut unsubscribeFromNotifications] at the end of your test + 3. Your sut doesn't call unsubscribe and you explicitly OCMReject it firing + - call [self unsubscribeContext:context] at the end of your test (use + the original context object here and _NOT_ the sut or it will not work). + */ +class STPRedirectContextTest: XCTestCase { + weak var weak_sut: STPRedirectContext? + + /// Use this to unsubscribe a context from notifications without calling + /// `sut.unsubscrbeFromNotifications` if you have OCMReject'd that method and thus + /// can't call it. + /// Note: You MUST pass in the actual context object here and not the mock or the + /// unsubscibe will silently fail. + func unsubscribeContext(_ context: STPRedirectContext?) { + if let context { + NotificationCenter.default.removeObserver( + context, + name: UIApplication.didBecomeActiveNotification, + object: nil) + STPURLCallbackHandler.shared().unregisterListener(context) + } + } + + func testInitWithNonRedirectSourceReturnsNil() { + let source = STPFixtures.cardSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion was called") + } + XCTAssertNil(sut) + } + + func testInitWithConsumedSourceReturnsNil() { + var json = STPTestUtils.jsonNamed(STPTestJSONSourceCard) + json?["status"] = "consumed" + let source = STPSource.decodedObject(fromAPIResponse: json)! + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion was called") + } + XCTAssertNil(sut) + } + + func testInitWithSource() { + let source = STPFixtures.iDEALSource() + var completionCalled = false + let fakeError = NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil) + + let sut = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(source.stripeID, sourceID) + XCTAssertEqual(source.clientSecret, clientSecret) + XCTAssertEqual(error! as NSError, fakeError, "Should be the same NSError object passed to completion() below") + completionCalled = true + } + + // Make sure the initWithSource: method pulled out the right values from the Source + XCTAssertNil(sut?.nativeRedirectURL) + XCTAssertEqual(sut?.redirectURL, source.redirect?.url) + XCTAssertEqual(sut?.returnURL, source.redirect?.returnURL) + + // and make sure the completion calls the completion block above + sut?.completion(fakeError) + XCTAssertTrue(completionCalled) + } + + func testInitWithSourceWithNativeURL() { + let source = STPFixtures.alipaySourceWithNativeURL() + var completionCalled = false + let nativeURL = URL(string: source.details?["native_url"] as? String ?? "") + let fakeError = NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil) + + let sut = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(source.stripeID, sourceID) + XCTAssertEqual(source.clientSecret, clientSecret) + XCTAssertEqual(error! as NSError, fakeError, "Should be the same NSError object passed to completion() below") + completionCalled = true + } + + // Make sure the initWithSource: method pulled out the right values from the Source + XCTAssertEqual(sut?.nativeRedirectURL, nativeURL) + XCTAssertEqual(sut?.redirectURL, source.redirect?.url) + XCTAssertEqual(sut?.returnURL, source.redirect?.returnURL) + + // and make sure the completion calls the completion block above + sut?.completion(fakeError) + XCTAssertTrue(completionCalled) + } + + func testInitWithPaymentIntent() { + let paymentIntent = STPFixtures.paymentIntent() + var completionCalled = false + let fakeError = NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil) + + let sut = STPRedirectContext(paymentIntent: paymentIntent) { clientSecret, error in + XCTAssertEqual(paymentIntent.clientSecret, clientSecret) + XCTAssertEqual(error! as NSError, fakeError, "Should be the same NSError object passed to completion() below") + completionCalled = true + } + + // Make sure the initWithPaymentIntent: method pulled out the right values from the PaymentIntent + XCTAssertNil(sut?.nativeRedirectURL) + XCTAssertEqual( + sut?.redirectURL?.absoluteString, + "https://hooks.stripe.com/redirect/authenticate/src_1Cl1AeIl4IdHmuTb1L7x083A?client_secret=src_client_secret_DBNwUe9qHteqJ8qQBwNWiigk") + + // `nextSourceAction` & `authorizeWithURL` should just be aliases for `nextAction` & `redirectToURL`, already tested in `STPPaymentIntentTest` + XCTAssertNotNil(paymentIntent.nextAction?.redirectToURL?.returnURL) + XCTAssertEqual(sut?.returnURL, paymentIntent.nextAction?.redirectToURL?.returnURL) + + // and make sure the completion calls the completion block above + XCTAssertNotNil(sut) + sut?.completion(fakeError) + XCTAssertTrue(completionCalled) + } + + func testInitWithPaymentIntentFailures() { + // Note next_action has been renamed to next_source_action in the API, but both still get sent down in the 2015-10-12 API + let unusedCompletion: ((String?, Error?) -> Void) = { _, _ in + XCTFail("should not be constructed, definitely not completed") + } + + let create: (([AnyHashable: Any]) -> STPRedirectContext?) = { json in + let paymentIntent = STPPaymentIntent.decodedObject(fromAPIResponse: json)! + return STPRedirectContext( + paymentIntent: paymentIntent, + completion: unusedCompletion) + } + + var json = STPTestUtils.jsonNamed(STPTestJSONPaymentIntent)! + XCTAssertNotNil(create(json), "before mutation of json, creation should succeed") + + json["status"] = "processing" + XCTAssertNil(create(json), "not created with wrong status") + json["status"] = "requires_action" + + json[jsonDict: "next_action"]?["type"] = "not_redirect_to_url" + XCTAssertNil(create(json), "not created with wrong next_action.type") + json[jsonDict: "next_action"]?["type"] = "redirect_to_url" + + let correctURL = json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["url"] as? String + json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["url"] = "not a valid URL" + XCTAssertNil(create(json), "not created with an invalid URL in next_action.redirect_to_url.url") + json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["url"] = correctURL ?? "" + + let correctReturnURL = json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["return_url"] as? String + json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["return_url"] = "not a url" + XCTAssertNil(create(json), "not created with invalid returnUrl") + json[jsonDict: "next_action"]?[jsonDict: "redirect_to_url"]?["return_url"] = correctReturnURL ?? "" + + XCTAssertNotNil(create(json), "works again when everything is back to normal") + } + + /// After starting a SafariViewController redirect flow, + /// when a DidBecomeActive notification is posted, RedirectContext's completion + /// block and dismiss method should _NOT_ be called. + func testSafariViewControllerRedirectFlow_activeNotification() { + let mockVC = MockUIViewController() + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + return true + } + return false + } + + let source = STPFixtures.iDEALSource() + + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + unsubscribeContext(sut) + XCTAssertFalse(sut._unsubscribeFromNotificationsCalled) + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) + XCTAssertTrue(mockVC.presentCalled) + } + + /// After starting a SafariViewController redirect flow, + /// when the shared URLCallbackHandler is called with a valid URL, + /// RedirectContext's completion block and dismiss method should be called. + func testSafariViewControllerRedirectFlow_callbackHandlerCalledValidURL() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + let url = source.redirect!.returnURL + let comps = NSURLComponents(url: url, resolvingAgainstBaseURL: false)! + comps.stp_queryItemsDictionary = + [ + "source": source.stripeID, + "client_secret": source.clientSecret!, + ] + STPURLCallbackHandler.shared().handleURLCallback(comps.url!) + return true + } + return false + } + let exp = expectation(description: "completion") + let sut = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(sourceID, source.stripeID) + XCTAssertEqual(clientSecret, source.clientSecret) + XCTAssertNil(error) + exp.fulfill() + }! + XCTAssertEqual(source.redirect?.returnURL, sut.returnURL) + sut._handleRedirectCompletionWithErrorHook = { shouldDismissViewController in + if shouldDismissViewController { + sut.safariViewControllerDidCompleteDismissal(SFSafariViewController(url: URL(string: "https://www.stripe.com")!)) + } + } + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + XCTAssertTrue(sut._unsubscribeFromNotificationsCalled) + XCTAssertTrue(sut._dismissPresentedViewControllerCalled) + XCTAssertTrue(mockVC.presentCalled) + waitForExpectations(timeout: 2, handler: nil) + } + + /// After starting a SafariViewController redirect flow, + /// when the shared URLCallbackHandler is called with an invalid URL, + /// RedirectContext's completion block and dismiss method should not be called. + func testSafariViewControllerRedirectFlow_callbackHandlerCalledInvalidURL() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + let url = URL(string: "my-app://some_path")! + XCTAssertNotEqual(url, sut.returnURL) + STPURLCallbackHandler.shared().handleURLCallback(url) + return true + } + return false + } + + XCTAssertFalse(sut._unsubscribeFromNotificationsCalled) + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) + XCTAssertTrue(mockVC.presentCalled) + unsubscribeContext(sut) + } + + /// After starting a SafariViewController redirect flow, + /// when SafariViewController finishes, RedirectContext's completion block + /// should be called. + func testSafariViewControllerRedirectFlow_didFinish() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + + let exp = expectation(description: "completion") + let sut: STPRedirectContext = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(sourceID, source.stripeID) + XCTAssertEqual(clientSecret, source.clientSecret) + // because we are manually invoking the dismissal, we report this as a cancelation + let error = error! as NSError + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.cancellationError.rawValue) + exp.fulfill() + }! + + sut._handleRedirectCompletionWithErrorHook = { shouldDismissViewController in + if !shouldDismissViewController { + sut.safariViewControllerDidCompleteDismissal(SFSafariViewController(url: URL(string: "https://www.stripe.com")!)) + } + } + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + let sfvc = vc as? SFSafariViewController + if let sfvc { + sfvc.delegate?.safariViewControllerDidFinish?(sfvc) + } + return true + } + return false + } + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + + // dismiss should not be called – SafariVC dismisses itself when Done is tapped + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) + XCTAssertTrue(sut._unsubscribeFromNotificationsCalled) + waitForExpectations(timeout: 2, handler: nil) + } + + /// After starting a SafariViewController redirect flow, + /// when SafariViewController fails to load the initial page (on iOS 11+ & without redirects), + /// RedirectContext's completion block and dismiss method should be called. + func testSafariViewControllerRedirectFlow_failedInitialLoad_iOS11Plus() { + + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + let exp = expectation(description: "completion") + let sut = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(sourceID, source.stripeID) + XCTAssertEqual(clientSecret, source.clientSecret) + let expectedError = NSError.stp_genericConnectionError() + XCTAssertEqual(error! as NSError, expectedError) + exp.fulfill() + }! + + sut._handleRedirectCompletionWithErrorHook = { shouldDismissViewController in + if shouldDismissViewController { + sut.safariViewControllerDidCompleteDismissal(SFSafariViewController(url: URL(string: "https://www.stripe.com")!)) + } + } + + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + let sfvc = vc as? SFSafariViewController + if let sfvc { + sfvc.delegate?.safariViewController?(sfvc, didCompleteInitialLoad: false) + } + return true + } + return false + } + sut.startSafariViewControllerRedirectFlow(from: mockVC) + + XCTAssertTrue(sut._unsubscribeFromNotificationsCalled) + XCTAssertTrue(sut._dismissPresentedViewControllerCalled) + + waitForExpectations(timeout: 2, handler: nil) + } + + /// After starting a SafariViewController redirect flow, + /// when SafariViewController fails to load the initial page (on iOS 11+ after redirecting to non-Stripe page), + /// RedirectContext's completion block should not be called (SFVC keeps loading) + func testSafariViewControllerRedirectFlow_failedInitialLoadAfterRedirect_iOS11Plus() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + XCTAssertFalse(sut._unsubscribeFromNotificationsCalled) // move + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) // move + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + + mockVC.presentChecker = { vc in + if vc is SFSafariViewController { + let sfvc = vc as? SFSafariViewController + // before initial load is done, SFVC was redirected to a non-stripe.com domain + if let sfvc, let url = URL(string: "https://girogate.de") { + sfvc.delegate?.safariViewController?( + sfvc, + initialLoadDidRedirectTo: url) + } + // Tell the delegate that the initial load failed. + // on iOS 11, with the redirect, this is a no-op + if let sfvc { + sfvc.delegate?.safariViewController?(sfvc, didCompleteInitialLoad: false) + } + return true + } + return false + } + unsubscribeContext(sut) + } + + /// After starting a SafariViewController redirect flow, + /// when the RedirectContext is cancelled, its dismiss method should be called. + func testSafariViewControllerRedirectFlow_cancel() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + sut.cancel() + + XCTAssertTrue(mockVC.presentCalled) + XCTAssertTrue(sut._unsubscribeFromNotificationsCalled) + XCTAssertTrue(sut._dismissPresentedViewControllerCalled) + } + + /// After starting a SafariViewController redirect flow, + /// if no action is taken, nothing should be called. + func testSafariViewControllerRedirectFlow_noAction() { + let mockVC = MockUIViewController() + let source = STPFixtures.iDEALSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + sut.startSafariViewControllerRedirectFlow(from: mockVC) + XCTAssertTrue(mockVC.presentCalled) + XCTAssertFalse(sut._unsubscribeFromNotificationsCalled) + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) + + unsubscribeContext(sut) + } + + /// After starting a Safari app redirect flow, + /// when a DidBecomeActive notification is posted, RedirectContext's completion + /// block and dismiss method should be called. + func testSafariAppRedirectFlow_activeNotification() { + let source = STPFixtures.iDEALSource() + let exp = expectation(description: "completion") + let sut = STPRedirectContext(source: source) { sourceID, clientSecret, error in + XCTAssertEqual(sourceID, source.stripeID) + XCTAssertEqual(clientSecret, source.clientSecret) + XCTAssertNil(error) + + exp.fulfill() + }! + + sut.startSafariAppRedirectFlow() + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(sut._unsubscribeFromNotificationsCalled) + } + + /// After starting a Safari app redirect flow, + /// if no notification is posted, nothing should be called. + func testSafariAppRedirectFlow_noNotification() { + let source = STPFixtures.iDEALSource() + let sut = STPRedirectContext(source: source) { _, _, _ in + XCTFail("completion called") + }! + + sut.startSafariAppRedirectFlow() + XCTAssertFalse(sut._unsubscribeFromNotificationsCalled) + XCTAssertFalse(sut._dismissPresentedViewControllerCalled) + + unsubscribeContext(sut) + } + + /// If a source type that supports native redirect is used and it contains a native + /// url, an app to app redirect should attempt to be initiated. + func testNativeRedirectSupportingSourceFlow_validNativeURL() { + let source = STPFixtures.alipaySourceWithNativeURL() + let sourceURL = URL(string: source.details?["native_url"] as! String)! + + let sut = STPRedirectContext( + source: source) { _, _, _ in + XCTFail("completion called") + }! + + XCTAssertNotNil(sut.nativeRedirectURL) + XCTAssertEqual(sut.nativeRedirectURL, sourceURL) + + let applicationMock = MockUIApplication() + sut.application = applicationMock + applicationMock.openHandler = { url, completion in + XCTAssertTrue(url == sourceURL) + completion?(true) + } + + let mockVC = MockUIViewController() + sut.startRedirectFlow(from: mockVC) + XCTAssertFalse(sut._startSafariAppRedirectFlowCalled) + XCTAssertTrue(applicationMock.openCalled) + sut.unsubscribeFromNotifications() + } + + /// If a source type that supports native redirect is used and it does not + /// contain a native url, standard web view redirect should be attempted + func testNativeRedirectSupportingSourceFlow_invalidNativeURL() { + let source = STPFixtures.alipaySource() + let sut = STPRedirectContext( + source: source) { _, _, _ in + XCTFail("completion called") + }! + XCTAssertNil(sut.nativeRedirectURL) + + let applicationMock = MockUIApplication() + sut.application = applicationMock + + let mockVC = MockUIViewController() + mockVC.presentChecker = { $0 is SFSafariViewController } + sut.startRedirectFlow(from: mockVC) + + let expectation = self.expectation(description: "Waiting 100ms for SafariServices") + + // Hack: Wait ~100ms to call sut back before unsubscribing from notifications. Otherwise the Safari thread doesn't get the unsubscribe request in time and calls the deallocated sut, crashing the app. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertFalse(applicationMock.openCalled) + XCTAssertTrue(mockVC.presentCalled) + sut.unsubscribeFromNotifications() + } + + // MARK: - WeChat Pay + + /// If a WeChat source type is used, we should attempt an app redirect. + func testWeChatPaySource_appRedirectSucceeds() { + let source = STPFixtures.weChatPaySource() + let sourceURL = URL(string: source.weChatPayDetails!.weChatAppURL!)! + + let sut = STPRedirectContext( + source: source) { _, _, _ in + XCTFail("completion called") + }! + + XCTAssertNotNil(sut.nativeRedirectURL) + XCTAssertEqual(sut.nativeRedirectURL, sourceURL) + XCTAssertNil(sut.redirectURL) + XCTAssertNotNil(sut.returnURL) + + let applicationMock = MockUIApplication() + applicationMock.openHandler = { url, completion in + XCTAssertEqual(url, sourceURL) + completion?(true) + } + sut.application = applicationMock + let mockVC = MockUIViewController() + sut.startRedirectFlow(from: mockVC) + let expectation = self.expectation(description: "Waiting 100ms for SafariServices") + + // Hack: Wait ~100ms to call sut back before unsubscribing from notifications. Otherwise the Safari thread doesn't get the unsubscribe request in time and calls the deallocated sut, crashing the app. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { + expectation.fulfill() + }) + + waitForExpectations(timeout: 1, handler: nil) + XCTAssertFalse(sut._startSafariAppRedirectFlowCalled) + XCTAssertFalse(sut.isSafariVCPresented()) + XCTAssertTrue(applicationMock.openCalled) + sut.unsubscribeFromNotifications() + } + + /// If a WeChat source type is used, we should attempt an app redirect. + /// If app redirect fails, expect an error. + func testWeChatPaySource_appRedirectFails() { + let source = STPFixtures.weChatPaySource() + let sourceURL = URL(string: source.weChatPayDetails!.weChatAppURL!)! + + let expectation = self.expectation(description: "Completion block called") + let sut = STPRedirectContext(source: source) { _, _, error in + guard let error = error as? NSError else { + XCTFail() + return + } + XCTAssertEqual(error.domain, STPRedirectContext.STPRedirectContextErrorDomain) + XCTAssertEqual(error.code, STPRedirectContextError.appRedirectError.rawValue) + expectation.fulfill() + }! + + XCTAssertNotNil(sut.nativeRedirectURL) + XCTAssertEqual(sut.nativeRedirectURL, sourceURL) + XCTAssertNil(sut.redirectURL) + XCTAssertNotNil(sut.returnURL) + + let applicationMock = MockUIApplication() + applicationMock.openHandler = { url, completion in + XCTAssertEqual(url, sourceURL) + completion?(false) + } + sut.application = applicationMock + let mockVC = MockUIViewController() + sut.startRedirectFlow(from: mockVC) + let safariWaitExpectation = self.expectation(description: "Waiting 100ms for SafariServices") + + // Hack: Wait ~100ms to call sut back before unsubscribing from notifications. Otherwise the Safari thread doesn't get the unsubscribe request in time and calls the deallocated sut, crashing the app. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { + safariWaitExpectation.fulfill() + }) + waitForExpectations(timeout: 10, handler: nil) + XCTAssertFalse(sut._startSafariAppRedirectFlowCalled) + XCTAssertFalse(sut.isSafariVCPresented()) + XCTAssertTrue(applicationMock.openCalled) + sut.unsubscribeFromNotifications() + } +} diff --git a/Stripe/StripeiOSTests/STPSetupIntentConfirmParamsTest.swift b/Stripe/StripeiOSTests/STPSetupIntentConfirmParamsTest.swift new file mode 100644 index 00000000..a0ce7fe7 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSetupIntentConfirmParamsTest.swift @@ -0,0 +1,156 @@ +// +// STPSetupIntentConfirmParamsTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 7/15/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPSetupIntentConfirmParamsTest: XCTestCase { + func testInit() { + for params in [ + STPSetupIntentConfirmParams(clientSecret: "secret"), + STPSetupIntentConfirmParams(), + STPSetupIntentConfirmParams(), + ] { + XCTAssertNotNil(params) + XCTAssertNotNil(params.clientSecret) + XCTAssertNotNil(params.additionalAPIParameters) + XCTAssertEqual(params.additionalAPIParameters.count, 0) + XCTAssertNil(params.paymentMethodID) + XCTAssertNil(params.returnURL) + XCTAssertNil(params.useStripeSDK) + XCTAssertNil(params.mandateData) + } + } + + func testDescription() { + let params = STPSetupIntentConfirmParams() + XCTAssertNotNil(params.description) + } + + func testDefaultMandateData() { + let params = STPSetupIntentConfirmParams() + + // no configuration should have no mandateData + XCTAssertNil(params.mandateData) + + params.paymentMethodParams = STPPaymentMethodParams() + + params.paymentMethodParams?.rawTypeString = "card" + // card type should have no default mandateData + XCTAssertNil(params.mandateData) + + for type in ["sepa_debit", "au_becs_debit", "bacs_debit", "bancontact", "ideal", "eps", "sofort", "link", "us_bank_account", "cashapp", "paypal", "revolut_pay", "klarna"] { + params.mandateData = nil + params.paymentMethodParams?.rawTypeString = type + // Mandate-required type should have mandateData + XCTAssertNotNil(params.mandateData) + XCTAssertEqual( + params.mandateData?.customerAcceptance.onlineParams?.inferFromClient, + NSNumber(value: true) + ) + + let customerAcceptance = STPMandateCustomerAcceptanceParams( + type: .offline, + onlineParams: nil + ) + params.mandateData = STPMandateDataParams(customerAcceptance: customerAcceptance!) + // Default behavior should not override custom setting + XCTAssertNotNil(params.mandateData) + XCTAssertNil(params.mandateData?.customerAcceptance.onlineParams) + } + } + + // MARK: STPFormEncodable Tests + func testRootObjectName() { + XCTAssertNil(STPSetupIntentConfirmParams.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let params = STPSetupIntentConfirmParams() + + let mapping = STPSetupIntentConfirmParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(params.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } + + func testCopy() { + let params = STPSetupIntentConfirmParams(clientSecret: "test_client_secret") + params.paymentMethodParams = STPPaymentMethodParams() + params.paymentMethodID = "test_payment_method_id" + params.returnURL = "fake://testing_only" + params.useStripeSDK = NSNumber(value: true) + params.mandateData = STPMandateDataParams( + customerAcceptance: STPMandateCustomerAcceptanceParams( + type: .offline, + onlineParams: nil + )! + ) + params.additionalAPIParameters = [ + "other_param": "other_value" + ] + + let paramsCopy = params.copy() as! STPSetupIntentConfirmParams + XCTAssertEqual(params.clientSecret, paramsCopy.clientSecret) + XCTAssertEqual(params.paymentMethodID, paramsCopy.paymentMethodID) + + // assert equal, not equal objects, because this is a shallow copy + XCTAssertEqual(params.paymentMethodParams, paramsCopy.paymentMethodParams) + XCTAssertEqual(params.mandateData, paramsCopy.mandateData) + + XCTAssertEqual(params.returnURL, paramsCopy.returnURL) + XCTAssertEqual(params.useStripeSDK, paramsCopy.useStripeSDK) + XCTAssertEqual( + params.additionalAPIParameters as NSDictionary, + paramsCopy.additionalAPIParameters as NSDictionary + ) + + } + + func testClientSecretValidation() { + XCTAssertFalse( + STPSetupIntentConfirmParams.isClientSecretValid("seti_12345"), + "'seti_12345' is not a valid client secret." + ) + XCTAssertFalse( + STPSetupIntentConfirmParams.isClientSecretValid("seti_12345_secret_"), + "'seti_12345_secret_' is not a valid client secret." + ) + XCTAssertFalse( + STPSetupIntentConfirmParams.isClientSecretValid( + "seti_a1b2c3_secret_x7y8z9seti_a1b2c3_secret_x7y8z9" + ), + "'seti_a1b2c3_secret_x7y8z9seti_a1b2c3_secret_x7y8z9' is not a valid client secret." + ) + XCTAssertFalse( + STPSetupIntentConfirmParams.isClientSecretValid("pi_a1b2c3_secret_x7y8z9"), + "'pi_a1b2c3_secret_x7y8z9' is not a valid client secret." + ) + + XCTAssertTrue( + STPSetupIntentConfirmParams.isClientSecretValid("seti_a1b2c3_secret_x7y8z9"), + "'seti_a1b2c3_secret_x7y8z9' is a valid client secret." + ) + XCTAssertTrue( + STPSetupIntentConfirmParams.isClientSecretValid( + "seti_1Eq5kyGMT9dGPIDGxiSp4cce_secret_FKlHb3yTI0YZWe4iqghS8ZXqwwMoMmy" + ), + "'seti_1Eq5kyGMT9dGPIDGxiSp4cce_secret_FKlHb3yTI0YZWe4iqghS8ZXqwwMoMmy' is a valid client secret." + ) + } +} diff --git a/Stripe/StripeiOSTests/STPSetupIntentFunctionalTest.swift b/Stripe/StripeiOSTests/STPSetupIntentFunctionalTest.swift new file mode 100644 index 00000000..9f40f320 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSetupIntentFunctionalTest.swift @@ -0,0 +1,262 @@ +// +// STPSetupIntentFunctionalTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/2/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +@_spi(STP) import StripeCore +import StripeCoreTestUtils +import StripePaymentsTestUtils +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPSetupIntentFunctionalTestSwift: STPNetworkStubbingTestCase { + + // MARK: - US Bank Account + func createAndConfirmSetupIntentWithUSBankAccount(completion: @escaping (String?) -> Void) { + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + var clientSecret: String? + let createSIExpectation = expectation(description: "Create SetupIntent") + STPTestingAPIClient.shared.createSetupIntent( + withParams: ["payment_method_types": ["us_bank_account"]], + account: nil + ) { intentClientSecret, error in + XCTAssertNil(error) + XCTAssertNotNil(intentClientSecret) + clientSecret = intentClientSecret + createSIExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + guard let clientSecret = clientSecret else { + XCTFail("Failed to create SetupIntent") + return + } + + let usBankAccountParams = STPPaymentMethodUSBankAccountParams() + usBankAccountParams.accountType = .checking + usBankAccountParams.accountHolderType = .individual + usBankAccountParams.accountNumber = "000123456789" + usBankAccountParams.routingNumber = "110000000" + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "iOS CI Tester" + billingDetails.email = "tester@example.com" + + let paymentMethodParams = STPPaymentMethodParams( + usBankAccount: usBankAccountParams, + billingDetails: billingDetails, + metadata: nil + ) + + let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: clientSecret) + setupIntentParams.paymentMethodParams = paymentMethodParams + + let confirmSIExpectation = expectation(description: "Confirm SetupIntent") + client.confirmSetupIntent(with: setupIntentParams, expand: ["payment_method"]) { + setupIntent, + error in + XCTAssertNil(error) + guard let setupIntent else { XCTFail(); return } + XCTAssertNotNil(setupIntent.paymentMethod) + XCTAssertNotNil(setupIntent.paymentMethod?.usBankAccount) + XCTAssertEqual(setupIntent.paymentMethod?.usBankAccount?.last4, "6789") + XCTAssertEqual(setupIntent.status, .requiresAction) + XCTAssertEqual(setupIntent.nextAction?.type, .verifyWithMicrodeposits) + confirmSIExpectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + completion(clientSecret) + } + + func testConfirmSetupIntentWithUSBankAccount_verifyWithAmounts() { + createAndConfirmSetupIntentWithUSBankAccount { [self] clientSecret in + guard let clientSecret = clientSecret else { + XCTFail("Failed to create SetupIntent") + return + } + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationExpectation = expectation(description: "Verify with microdeposits") + client.verifySetupIntentWithMicrodeposits( + clientSecret: clientSecret, + firstAmount: 32, + secondAmount: 45 + ) { setupIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(setupIntent) + XCTAssertEqual(setupIntent?.status, .succeeded) + verificationExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + } + + func testConfirmSetupIntentWithUSBankAccount_verifyWithDescriptorCode() { + createAndConfirmSetupIntentWithUSBankAccount { [self] clientSecret in + guard let clientSecret = clientSecret else { + XCTFail("Failed to create SetupIntent") + return + } + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + + let verificationExpectation = expectation(description: "Verify with microdeposits") + client.verifySetupIntentWithMicrodeposits( + clientSecret: clientSecret, + descriptorCode: "SM11AA" + ) { setupIntent, error in + XCTAssertNil(error) + XCTAssertNotNil(setupIntent) + XCTAssertEqual(setupIntent?.status, .succeeded) + verificationExpectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout) + } + } +} + +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +extension STPSetupIntentFunctionalTestSwift { + func testCreateSetupIntentWithTestingServer() { + let expectation = self.expectation(description: "SetupIntent create.") + STPTestingAPIClient.shared.createSetupIntent( + withParams: nil) { clientSecret, error in + XCTAssertNotNil(clientSecret) + XCTAssertNil(error) + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRetrieveSetupIntentSucceeds() { + // Tests retrieving a previously created SetupIntent succeeds + let setupIntentClientSecret = "seti_1GGCuIFY0qyl6XeWVfbQK6b3_secret_GnoX2tzX2JpvxsrcykRSVna2lrYLKew" + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Setup Intent retrieve") + + client.retrieveSetupIntent( + withClientSecret: setupIntentClientSecret) { setupIntent, error in + XCTAssertNil(error) + guard let setupIntent else { XCTFail(); return } + XCTAssertNotNil(setupIntent) + XCTAssertEqual(setupIntent.stripeID, "seti_1GGCuIFY0qyl6XeWVfbQK6b3") + XCTAssertEqual(setupIntent.clientSecret, setupIntentClientSecret) + XCTAssertEqual(setupIntent.created, Date(timeIntervalSince1970: 1582673622)) + XCTAssertNil(setupIntent.customerID) + XCTAssertNil(setupIntent.stripeDescription) + XCTAssertFalse(setupIntent.livemode) + XCTAssertNil(setupIntent.nextAction) + XCTAssertNil(setupIntent.paymentMethodID) + XCTAssertEqual(setupIntent.paymentMethodTypes, [NSNumber(value: STPPaymentMethodType.card.rawValue)]) + XCTAssertEqual(setupIntent.status, STPSetupIntentStatus.requiresPaymentMethod) + XCTAssertEqual(setupIntent.usage, STPSetupIntentUsage.offSession) + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testConfirmSetupIntentSucceeds() { + + var clientSecret: String? + let createExpectation = self.expectation(description: "Create SetupIntent.") + STPTestingAPIClient.shared.createSetupIntent(withParams: nil) { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "SetupIntent confirm") + let params = STPSetupIntentConfirmParams(clientSecret: clientSecret!) + params.returnURL = "example-app-scheme://authorized" + // Confirm using a card requiring 3DS1 authentication (ie requires next steps) + params.paymentMethodID = "pm_card_authenticationRequired" + client.confirmSetupIntent( + with: params) { setupIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + guard let setupIntent else { XCTFail(); return } + + XCTAssertNotNil(setupIntent) + XCTAssertEqual(setupIntent.stripeID, STPSetupIntent.id(fromClientSecret: params.clientSecret)) + XCTAssertEqual(setupIntent.clientSecret, clientSecret) + XCTAssertFalse(setupIntent.livemode) + + XCTAssertEqual(setupIntent.status, STPSetupIntentStatus.requiresAction) + XCTAssertNotNil(setupIntent.nextAction) + XCTAssertEqual(setupIntent.nextAction?.type, STPIntentActionType.redirectToURL) + XCTAssertEqual(setupIntent.nextAction?.redirectToURL?.returnURL, URL(string: "example-app-scheme://authorized")) + XCTAssertNotNil(setupIntent.paymentMethodID) + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // MARK: - AU BECS Debit + + func testConfirmAUBECSDebitSetupIntent() { + + var clientSecret: String? + let createExpectation = self.expectation(description: "Create PaymentIntent.") + STPTestingAPIClient.shared.createSetupIntent( + withParams: [ + "payment_method_types": ["au_becs_debit"], + ], + account: "au") { createdClientSecret, creationError in + XCTAssertNotNil(createdClientSecret) + XCTAssertNil(creationError) + createExpectation.fulfill() + clientSecret = createdClientSecret + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + XCTAssertNotNil(clientSecret) + + let becsParams = STPPaymentMethodAUBECSDebitParams() + becsParams.bsbNumber = "000000" // Stripe test bank + becsParams.accountNumber = "000123456" // test account + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.name = "Jenny Rosen" + billingDetails.email = "jrosen@example.com" + + let params = STPPaymentMethodParams( + aubecsDebit: becsParams, + billingDetails: billingDetails, + metadata: [ + "test_key": "test_value", + ]) + + let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: clientSecret!) + setupIntentParams.paymentMethodParams = params + + let client = STPAPIClient(publishableKey: STPTestingAUPublishableKey) + let expectation = self.expectation(description: "Setup Intent confirm") + + client.confirmSetupIntent( + with: setupIntentParams) { setupIntent, error in + XCTAssertNil(error, "With valid key + secret, should be able to confirm the intent") + guard let setupIntent else { XCTFail(); return } + + XCTAssertNotNil(setupIntent) + XCTAssertEqual(setupIntent.stripeID, STPSetupIntent.id(fromClientSecret: setupIntentParams.clientSecret)) + XCTAssertNotNil(setupIntent.paymentMethodID) + XCTAssertEqual(setupIntent.status, STPSetupIntentStatus.succeeded) + + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/Stripe/StripeiOSTests/STPSetupIntentLastSetupErrorTest.swift b/Stripe/StripeiOSTests/STPSetupIntentLastSetupErrorTest.swift new file mode 100644 index 00000000..6bd56371 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSetupIntentLastSetupErrorTest.swift @@ -0,0 +1,32 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSetupIntentLastSetupErrorTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 8/9/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments + +class STPSetupIntentLastSetupErrorTest: XCTestCase { + func testTypeFromString() { + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "api_connection_error"), STPSetupIntentLastSetupErrorType.apiConnection) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "API_CONNECTION_ERROR"), STPSetupIntentLastSetupErrorType.apiConnection) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "api_error"), STPSetupIntentLastSetupErrorType.API) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "API_ERROR"), STPSetupIntentLastSetupErrorType.API) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "authentication_error"), STPSetupIntentLastSetupErrorType.authentication) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "AUTHENTICATION_ERROR"), STPSetupIntentLastSetupErrorType.authentication) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "card_error"), STPSetupIntentLastSetupErrorType.card) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "CARD_ERROR"), STPSetupIntentLastSetupErrorType.card) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "idempotency_error"), STPSetupIntentLastSetupErrorType.idempotency) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "IDEMPOTENCY_ERROR"), STPSetupIntentLastSetupErrorType.idempotency) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "invalid_request_error"), STPSetupIntentLastSetupErrorType.invalidRequest) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "INVALID_REQUEST_ERROR"), STPSetupIntentLastSetupErrorType.invalidRequest) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "rate_limit_error"), STPSetupIntentLastSetupErrorType.rateLimit) + XCTAssertEqual(STPSetupIntentLastSetupError.type(from: "RATE_LIMIT_ERROR"), STPSetupIntentLastSetupErrorType.rateLimit) + } + // MARK: - STPAPIResponseDecodable Tests + + // STPSetupIntentLastError is a sub-object of STPSetupIntent, see STPSetupIntentTest +} diff --git a/Stripe/StripeiOSTests/STPSetupIntentTest.swift b/Stripe/StripeiOSTests/STPSetupIntentTest.swift new file mode 100644 index 00000000..0d7cb3df --- /dev/null +++ b/Stripe/StripeiOSTests/STPSetupIntentTest.swift @@ -0,0 +1,104 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSetupIntentTest.m +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/27/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// +@_spi(STP) @testable import StripePayments + +class STPSetupIntentTest: XCTestCase { + // MARK: - Description Tests + + func testDescription() { + let setupIntent = STPFixtures.setupIntent() + + XCTAssertNotNil(setupIntent) + let desc = setupIntent.description + XCTAssertTrue(desc.contains(NSStringFromClass(type(of: setupIntent).self))) + XCTAssertGreaterThan(desc.count, 500, "Custom description should be long") + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let fullJson = STPTestUtils.jsonNamed(STPTestJSONSetupIntent) + + XCTAssertNotNil(STPSetupIntent.decodedObject(fromAPIResponse: fullJson), "can decode with full json") + + let requiredFields = [ + "id", + "client_secret", + "livemode", + "status", + ] + + for field in requiredFields { + var partialJson = fullJson! as [AnyHashable: Any] + + XCTAssertNotNil(partialJson[field]) + partialJson.removeValue(forKey: field) + + XCTAssertNil(STPSetupIntent.decodedObject(fromAPIResponse: partialJson)) + } + } + + func testDecodedObjectFromAPIResponseMapping() { + let setupIntentJson = STPTestUtils.jsonNamed("SetupIntent")! + guard let setupIntent = STPSetupIntent.decodedObject(fromAPIResponse: setupIntentJson) else { XCTFail(); return } + + XCTAssertEqual(setupIntent.stripeID, "seti_123456789") + XCTAssertEqual(setupIntent.clientSecret, "seti_123456789_secret_123456789") + XCTAssertEqual(setupIntent.created, Date(timeIntervalSince1970: 123456789)) + XCTAssertEqual(setupIntent.customerID, "cus_123456") + XCTAssertEqual(setupIntent.paymentMethodID, "pm_123456") + XCTAssertEqual(setupIntent.stripeDescription, "My Sample SetupIntent") + XCTAssertFalse(setupIntent.livemode) + // nextAction + XCTAssertNotNil(setupIntent.nextAction) + XCTAssertEqual(setupIntent.nextAction?.type, STPIntentActionType.redirectToURL) + XCTAssertNotNil(setupIntent.nextAction?.redirectToURL) + XCTAssertNotNil(setupIntent.nextAction?.redirectToURL?.url) + let returnURL = setupIntent.nextAction?.redirectToURL?.returnURL + XCTAssertNotNil(returnURL) + XCTAssertEqual(returnURL, URL(string: "payments-example://stripe-redirect")) + let url = setupIntent.nextAction?.redirectToURL?.url + XCTAssertNotNil(url) + + XCTAssertEqual(url, URL(string: "https://hooks.stripe.com/redirect/authenticate/src_1Cl1AeIl4IdHmuTb1L7x083A?client_secret=src_client_secret_DBNwUe9qHteqJ8qQBwNWiigk")) + XCTAssertEqual(setupIntent.paymentMethodID, "pm_123456") + XCTAssertEqual(setupIntent.status, STPSetupIntentStatus.requiresAction) + XCTAssertEqual(setupIntent.usage, STPSetupIntentUsage.offSession) + + XCTAssertEqual(setupIntent.paymentMethodTypes, [NSNumber(value: STPPaymentMethodType.card.rawValue)]) + + // lastSetupError + + XCTAssertNotNil(setupIntent.lastSetupError) + XCTAssertEqual(setupIntent.lastSetupError?.code, "setup_intent_authentication_failure") + XCTAssertEqual(setupIntent.lastSetupError?.docURL, "https://stripe.com/docs/error-codes#setup-intent-authentication-failure") + XCTAssertEqual(setupIntent.lastSetupError?.message, "The latest attempt to set up the payment method has failed because authentication failed.") + XCTAssertNotNil(setupIntent.lastSetupError?.paymentMethod) + XCTAssertEqual(setupIntent.lastSetupError?.type, STPSetupIntentLastSetupErrorType.invalidRequest) + } + + // MARK: STPSetupIntentStatus extension tests + + func testStringFromStatus() { + let expected: [STPSetupIntentStatus: String] = [ + .requiresPaymentMethod: "requires_payment_method", + .requiresConfirmation: "requires_confirmation", + .requiresAction: "requires_action", + .processing: "processing", + .succeeded: "succeeded", + .canceled: "canceled", + .unknown: "unknown", + ] + + for (status, expectedString) in expected { + let resultString = STPSetupIntentStatus.string(from: status) + XCTAssertEqual(resultString, expectedString, "Expected \(status) to map to string '\(expectedString)', but got '\(resultString)'") + } + } +} diff --git a/Stripe/StripeiOSTests/STPShippingAddressViewControllerLocalizationSnapshotTests.swift b/Stripe/StripeiOSTests/STPShippingAddressViewControllerLocalizationSnapshotTests.swift new file mode 100644 index 00000000..1760b7a0 --- /dev/null +++ b/Stripe/StripeiOSTests/STPShippingAddressViewControllerLocalizationSnapshotTests.swift @@ -0,0 +1,113 @@ +// +// STPShippingAddressViewControllerLocalizationSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ben Guo on 11/3/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPShippingAddressViewControllerLocalizationSnapshotTests: STPSnapshotTestCase { + + func performSnapshotTest( + forLanguage language: String?, + shippingType: STPShippingType, + contact: Bool + ) { + var identifier = (shippingType == .shipping) ? "shipping" : "delivery" + let config = STPPaymentConfiguration() + config.companyName = "Test Company" + config.requiredShippingAddressFields = Set([ + .postalAddress, + .emailAddress, + .phoneNumber, + .name, + ]) + if contact { + config.requiredShippingAddressFields = Set([.emailAddress]) + identifier = "contact" + } + config.shippingType = shippingType + + STPLocalizationUtils.overrideLanguage(to: language) + let info = STPUserInformation() + info.billingAddress = STPAddress() + info.billingAddress!.email = "@" // trigger "use billing address" button + + let shippingVC = STPShippingAddressViewController( + configuration: config, + theme: STPTheme.defaultTheme, + currency: nil, + shippingAddress: nil, + selectedShippingMethod: nil, + prefilledInformation: info + ) + + /// This method rejects nil or empty country codes to stop strange looking behavior + /// when scrolling to the top "unset" position in the picker, so put in + /// an invalid country code instead to test seeing the "Country" placeholder + shippingVC.addressViewModel.addressFieldTableViewCountryCode = "INVALID" + + let viewToTest = stp_preparedAndSizedViewForSnapshotTest(from: shippingVC)! + + STPSnapshotVerifyView(viewToTest, identifier: identifier) + + STPLocalizationUtils.overrideLanguage(to: nil) + } + + func testGerman() { + performSnapshotTest(forLanguage: "de", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "de", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "de", shippingType: .delivery, contact: false) + } + + func testEnglish() { + performSnapshotTest(forLanguage: "en", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "en", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "en", shippingType: .delivery, contact: false) + } + + func testSpanish() { + performSnapshotTest(forLanguage: "es", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "es", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "es", shippingType: .delivery, contact: false) + } + + func testFrench() { + performSnapshotTest(forLanguage: "fr", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "fr", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "fr", shippingType: .delivery, contact: false) + } + + func testItalian() { + performSnapshotTest(forLanguage: "it", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "it", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "it", shippingType: .delivery, contact: false) + } + + func testJapanese() { + performSnapshotTest(forLanguage: "ja", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "ja", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "ja", shippingType: .delivery, contact: false) + } + + func testDutch() { + performSnapshotTest(forLanguage: "nl", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "nl", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "nl", shippingType: .delivery, contact: false) + } + + func testChinese() { + performSnapshotTest(forLanguage: "zh-Hans", shippingType: .shipping, contact: false) + performSnapshotTest(forLanguage: "zh-Hans", shippingType: .shipping, contact: true) + performSnapshotTest(forLanguage: "zh-Hans", shippingType: .delivery, contact: false) + } +} diff --git a/Stripe/StripeiOSTests/STPShippingAddressViewControllerTest.swift b/Stripe/StripeiOSTests/STPShippingAddressViewControllerTest.swift new file mode 100644 index 00000000..cc473049 --- /dev/null +++ b/Stripe/StripeiOSTests/STPShippingAddressViewControllerTest.swift @@ -0,0 +1,114 @@ +// +// STPShippingAddressViewControllerTest.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 8/7/18. +// Copyright © 2018 Stripe, Inc. All rights reserved. +// + +import Stripe + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPShippingAddressViewControllerTest: XCTestCase { + func testPrefilledBillingAddress_removeAddress() { + let config = STPPaymentConfiguration() + config.requiredShippingAddressFields = Set([.postalAddress]) + + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "Harare" + address.postalCode = "10002" + // Zimbabwe does not require zip codes, while the default locale for tests (US) does + address.country = "ZW" + // Sanity checks + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "ZW")) + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "US")) + + let sut = STPShippingAddressViewController( + configuration: config, + theme: STPTheme.defaultTheme, + currency: nil, + shippingAddress: address, + selectedShippingMethod: nil, + prefilledInformation: nil + ) + + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + } + + func testPrefilledBillingAddress_addAddressWithLimitedCountries() { + // Zimbabwe does not require zip codes, while the default locale for tests (US) does + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_ZW")) { + // Sanity checks + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "ZW")) + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "US")) + let config = STPPaymentConfiguration() + config.requiredShippingAddressFields = Set([.postalAddress]) + config.availableCountries = Set(["CA", "BT"]) + + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let sut = STPShippingAddressViewController( + configuration: config, + theme: STPTheme.defaultTheme, + currency: nil, + shippingAddress: address, + selectedShippingMethod: nil, + prefilledInformation: nil + ) + + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + } + } + + func testPrefilledBillingAddress_addAddress() { + // Zimbabwe does not require zip codes, while the default locale for tests (US) does + NSLocale.stp_withLocale(as: NSLocale(localeIdentifier: "en_ZW")) { + // Sanity checks + XCTAssertFalse(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "ZW")) + XCTAssertTrue(STPPostalCodeValidator.postalCodeIsRequired(forCountryCode: "US")) + let config = STPPaymentConfiguration() + config.requiredShippingAddressFields = Set([.postalAddress]) + + let address = STPAddress() + address.name = "John Smith Doe" + address.phone = "8885551212" + address.email = "foo@example.com" + address.line1 = "55 John St" + address.city = "New York" + address.state = "NY" + address.postalCode = "10002" + address.country = "US" + + let sut = STPShippingAddressViewController( + configuration: config, + theme: STPTheme.defaultTheme, + currency: nil, + shippingAddress: address, + selectedShippingMethod: nil, + prefilledInformation: nil + ) + + XCTAssertNoThrow(sut.loadView()) + XCTAssertNoThrow(sut.viewDidLoad()) + } + } +} diff --git a/Stripe/StripeiOSTests/STPShippingMethodsViewControllerLocalizationSnapshotTests.swift b/Stripe/StripeiOSTests/STPShippingMethodsViewControllerLocalizationSnapshotTests.swift new file mode 100644 index 00000000..f9b256b3 --- /dev/null +++ b/Stripe/StripeiOSTests/STPShippingMethodsViewControllerLocalizationSnapshotTests.swift @@ -0,0 +1,76 @@ +// +// STPShippingMethodsViewControllerLocalizationSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ben Guo on 11/3/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPShippingMethodsViewControllerLocalizationSnapshotTests: STPSnapshotTestCase { + + func performSnapshotTest(forLanguage language: String?) { + STPLocalizationUtils.overrideLanguage(to: language) + + let method1 = PKShippingMethod() + method1.label = "UPS Ground" + method1.detail = "Arrives in 3-5 days" + method1.amount = NSDecimalNumber(string: "0.00") + method1.identifier = "ups_ground" + let method2 = PKShippingMethod() + method2.label = "FedEx" + method2.detail = "Arrives tomorrow" + method2.amount = NSDecimalNumber(string: "5.99") + method2.identifier = "fedex" + + let shippingVC = STPShippingMethodsViewController( + shippingMethods: [method1, method2], + selectedShippingMethod: method1, + currency: "usd", + theme: STPTheme.defaultTheme + ) + let viewToTest = stp_preparedAndSizedViewForSnapshotTest(from: shippingVC)! + STPSnapshotVerifyView(viewToTest, identifier: nil) + STPLocalizationUtils.overrideLanguage(to: nil) + } + + func testGerman() { + performSnapshotTest(forLanguage: "de") + } + + func testEnglish() { + performSnapshotTest(forLanguage: "en") + } + + func testSpanish() { + performSnapshotTest(forLanguage: "es") + } + + func testFrench() { + performSnapshotTest(forLanguage: "fr") + } + + func testItalian() { + performSnapshotTest(forLanguage: "it") + } + + func testJapanese() { + performSnapshotTest(forLanguage: "ja") + } + + func testDutch() { + performSnapshotTest(forLanguage: "nl") + } + + func testChinese() { + performSnapshotTest(forLanguage: "zh-Hans") + } +} diff --git a/Stripe/StripeiOSTests/STPSourceCardDetailsTest.swift b/Stripe/StripeiOSTests/STPSourceCardDetailsTest.swift new file mode 100644 index 00000000..5bddcdc6 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceCardDetailsTest.swift @@ -0,0 +1,116 @@ +// +// STPSourceCardDetailsTest.swift +// StripeiOS Tests +// +// Created by Joey Dong on 6/21/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPSourceCardDetailsTest: XCTestCase { + // MARK: - STPSourceCard3DSecureStatus Tests + func testThreeDSecureStatusFromString() { + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "required"), .required) + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "REQUIRED"), .required) + + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "optional"), .optional) + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "OPTIONAL"), .optional) + + XCTAssertEqual( + STPSourceCardDetails.threeDSecureStatus(from: "not_supported"), + .notSupported + ) + XCTAssertEqual( + STPSourceCardDetails.threeDSecureStatus(from: "NOT_SUPPORTED"), + .notSupported + ) + + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "recommended"), .recommended) + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "RECOMMENDED"), .recommended) + + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "unknown"), .unknown) + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "UNKNOWN"), .unknown) + + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "garbage"), .unknown) + XCTAssertEqual(STPSourceCardDetails.threeDSecureStatus(from: "GARBAGE"), .unknown) + } + + func testStringFromThreeDSecureStatus() { + let values: [STPSourceCard3DSecureStatus] = [ + .required, + .optional, + .notSupported, + .recommended, + .unknown, + ] + + for threeDSecureStatus in values { + let string = STPSourceCardDetails.string(fromThreeDSecureStatus: threeDSecureStatus) + + switch threeDSecureStatus { + case .required: + XCTAssertEqual(string, "required") + case .optional: + XCTAssertEqual(string, "optional") + case .notSupported: + XCTAssertEqual(string, "not_supported") + case .recommended: + XCTAssertEqual(string, "recommended") + case .unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - Description Tests + func testDescription() { + let cardDetails = STPSourceCardDetails.decodedObject( + fromAPIResponse: STPTestUtils.jsonNamed("CardSource")!["card"] as? [AnyHashable: Any] + ) + XCTAssert(cardDetails?.description != nil) + } + + // MARK: - STPAPIResponseDecodable Tests + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = STPTestUtils.jsonNamed("CardSource")?["card"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceCardDetails.decodedObject(fromAPIResponse: response)) + } + + XCTAssert( + (STPSourceCardDetails.decodedObject( + fromAPIResponse: STPTestUtils.jsonNamed("CardSource")!["card"] + as? [AnyHashable: Any] + ) + != nil) + ) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("CardSource")?["card"] as? [AnyHashable: Any] + let cardDetails = STPSourceCardDetails.decodedObject(fromAPIResponse: response)! + + XCTAssertEqual(cardDetails.brand, .visa) + XCTAssertEqual(cardDetails.country, "US") + XCTAssertEqual(cardDetails.expMonth, UInt(12)) + XCTAssertEqual(cardDetails.expYear, UInt(2034)) + XCTAssertEqual(cardDetails.funding, .debit) + XCTAssertEqual(cardDetails.last4, "5556") + XCTAssertEqual(cardDetails.threeDSecure, .notSupported) + + XCTAssertEqual(cardDetails.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceFunctionalTest.swift b/Stripe/StripeiOSTests/STPSourceFunctionalTest.swift new file mode 100644 index 00000000..b95c1d57 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceFunctionalTest.swift @@ -0,0 +1,603 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceFunctionalTest.m +// Stripe +// +// Created by Ben Guo on 1/23/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@_spi(STP) @testable import StripeCore +@testable import StripeCoreTestUtils +@_spi(STP) @testable import StripePayments +import StripePaymentsTestUtils +import XCTest + +class STPSourceFunctionalTest: STPNetworkStubbingTestCase { + func testCreateSource_bancontact() { + let params = STPSourceParams.bancontactParams( + withAmount: 1099, + name: "Jenny Rosen", + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: "ORDER AT123") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.bancontact) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_card() { + let card = STPCardParams() + card.number = "4242 4242 4242 4242" + card.expMonth = 6 + card.expYear = 2050 + card.currency = "usd" + card.name = "Jenny Rosen" + card.address.line1 = "123 Fake Street" + card.address.line2 = "Apartment 4" + card.address.city = "New York" + card.address.state = "NY" + card.address.country = "USA" + card.address.postalCode = "10002" + let params = STPSourceParams.cardParams(withCard: card) + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.card) + XCTAssertEqual(source?.cardDetails?.last4, "4242") + XCTAssertEqual(source?.cardDetails?.expMonth, card.expMonth) + XCTAssertEqual(source?.cardDetails?.expYear, card.expYear) + XCTAssertEqual(source?.owner?.name, card.name) + let address = source?.owner?.address + XCTAssertEqual(address?.line1, card.address.line1) + XCTAssertEqual(address?.line2, card.address.line2) + XCTAssertEqual(address?.city, card.address.city) + XCTAssertEqual(address?.state, card.address.state) + XCTAssertEqual(address?.country, card.address.country) + XCTAssertEqual(address?.postalCode, card.address.postalCode) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_ideal() { + let params = STPSourceParams.idealParams( + withAmount: 1099, + name: "Jenny Rosen", + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: "ORDER AT123", + bank: "ing") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.iDEAL) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertEqual(source?.details?["bank"] as? String, "ing") + XCTAssertEqual(source?.details?["statement_descriptor"] as? String, "ORDER AT123") + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + // #pragma clang diagnostic pop + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_ideal_missingOptionalFields() { + let params = STPSourceParams.idealParams( + withAmount: 1099, + name: nil, + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: nil, + bank: nil) + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.iDEAL) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertNil(source?.owner?.name) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.details?["bank"]) + XCTAssertNil(source?.details?["statement_descriptor"]) + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + // #pragma clang diagnostic pop + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_ideal_emptyOptionalFields() { + let params = STPSourceParams.idealParams( + withAmount: 1099, + name: "", + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: "", + bank: "") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.iDEAL) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertNil(source?.owner?.name) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.details?["bank"]) + XCTAssertNil(source?.details?["statement_descriptor"]) + // #pragma clang diagnostic push + // #pragma clang diagnostic ignored "-Wdeprecated" + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + // #pragma clang diagnostic pop + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_sepaDebit() { + let params = STPSourceParams.sepaDebitParams( + withName: "Jenny Rosen", + iban: "DE89370400440532013000", + addressLine1: "Nollendorfstraße 27", + city: "Berlin", + postalCode: "10777", + country: "DE") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.SEPADebit) + XCTAssertNil(source?.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.owner?.address?.city, "Berlin") + XCTAssertEqual(source?.owner?.address?.line1, "Nollendorfstraße 27") + XCTAssertEqual(source?.owner?.address?.country, "DE") + XCTAssertEqual(source?.sepaDebitDetails?.country, "DE") + XCTAssertEqual(source?.sepaDebitDetails?.last4, "3000") + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_sepaDebit_NoAddress() { + let params = STPSourceParams.sepaDebitParams( + withName: "Jenny Rosen", + iban: "DE89370400440532013000", + addressLine1: nil, + city: nil, + postalCode: nil, + country: nil) + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.SEPADebit) + XCTAssertNil(source?.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertNil(source?.owner?.address?.city) + XCTAssertNil(source?.owner?.address?.line1) + XCTAssertNil(source?.owner?.address?.country) + XCTAssertEqual(source?.sepaDebitDetails?.country, "DE") // German IBAN so sepa tells us country here even though we didnt pass it up as owner info + XCTAssertEqual(source?.sepaDebitDetails?.last4, "3000") + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_sofort() { + let params = STPSourceParams.sofortParams( + withAmount: 1099, + returnURL: "https://shop.example.com/crtABC", + country: "DE", + statementDescriptor: "ORDER AT11990") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.sofort) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + XCTAssertEqual(source?.details?["country"] as? String, "DE") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func skip_testCreateSourceVisaCheckout() { + // The SDK does not have a means of generating Visa Checkout params for testing. Supply your own + // callId, and the correct publishable key, and you can run this test case + // manually after removing the `skip_` prefix. It'll log the source's stripeID, and that + // can be verified in dashboard. + let params = STPSourceParams.visaCheckoutParams(withCallId: "") + let client = STPAPIClient(publishableKey: "pk_") + client.apiURL = URL(string: "https://api.stripe.com/v1") + + let sourceExp = expectation(description: "VCO source created") + client.createSource(with: params) { source, error in + sourceExp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.card) + XCTAssertEqual(source?.flow, STPSourceFlow.none) + XCTAssertEqual(source?.status, STPSourceStatus.chargeable) + XCTAssertEqual(source?.usage, STPSourceUsage.reusable) + XCTAssertTrue(source!.stripeID.hasPrefix("src_")) + if let stripeID = source?.stripeID { + print("Created a VCO source \(stripeID)") + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func skip_testCreateSourceMasterpass() { + // The SDK does not have a means of generating Masterpass params for testing. Supply your own + // cartId & transactionId, and the correct publishable key, and you can run this test case + // manually after removing the `skip_` prefix. It'll log the source's stripeID, and that + // can be verified in dashboard. + let params = STPSourceParams.masterpassParams(withCartId: "", transactionId: "") + let client = STPAPIClient(publishableKey: "pk_") + client.apiURL = URL(string: "https://api.stripe.com/v1") + + let sourceExp = expectation(description: "Masterpass source created") + client.createSource(with: params) { source, error in + sourceExp.fulfill() + + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.card) + XCTAssertEqual(source?.flow, STPSourceFlow.none) + XCTAssertEqual(source?.status, STPSourceStatus.chargeable) + XCTAssertEqual(source?.usage, STPSourceUsage.singleUse) + XCTAssertTrue(source!.stripeID.hasPrefix("src_")) + if let stripeID = source?.stripeID { + print("Created a Masterpass source \(stripeID)") + } + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_alipay() { + let params = STPSourceParams.alipayParams( + withAmount: 1099, + currency: "usd", + returnURL: "https://shop.example.com/crtABC") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Alipay Source creation") + + params.metadata = [ + "foo": "bar", + ] + client.createSource(with: params) { source, error2 in + XCTAssertNil(error2) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.alipay) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_p24() { + let params = STPSourceParams.p24Params( + withAmount: 1099, + currency: "eur", + email: "user@example.com", + name: "Jenny Rosen", + returnURL: "https://shop.example.com/crtABC") + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "P24 Source creation") + + params.metadata = [ + "foo": "bar", + ] + client.createSource(with: params) { source, error2 in + XCTAssertNil(error2) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.P24) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.email, params.owner?["email"] as? String) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + expectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testRetrieveSource_sofort() { + let client = STPAPIClient(publishableKey: "pk_test_vOo1umqsYxSrP5UXfOeL3ecm") + let params = STPSourceParams() + params.type = STPSourceType.sofort + params.amount = NSNumber(value: 1099) + params.currency = "eur" + params.redirect = [ + "return_url": "https://shop.example.com/crtA6B28E1", + ] + params.metadata = [ + "foo": "bar", + ] + params.additionalAPIParameters = [ + "sofort": [ + "country": "DE", + ], + ] + let createExp = expectation(description: "Source creation") + let retrieveExp = expectation(description: "Source retrieval") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + guard let source else { XCTFail(); return } + createExp.fulfill() + client.retrieveSource( + withId: source.stripeID, + clientSecret: source.clientSecret!) { source2, error2 in + XCTAssertNil(error2) + XCTAssertNotNil(source2) + XCTAssertEqual(source, source2) + retrieveExp.fulfill() + } + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_eps() { + let params = STPSourceParams.epsParams( + withAmount: 1099, + name: "Jenny Rosen", + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: "ORDER AT123") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.EPS) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + XCTAssertEqual(source?.allResponseFields["statement_descriptor"] as! String, "ORDER AT123") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_eps_no_statement_descriptor() { + let params = STPSourceParams.epsParams( + withAmount: 1099, + name: "Jenny Rosen", + returnURL: "https://shop.example.com/crtABC", + statementDescriptor: nil) + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.EPS) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.currency, params.currency) + XCTAssertEqual(source?.owner?.name, params.owner?["name"] as? String) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + XCTAssertNil(source?.allResponseFields["statement_descriptor"]) + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_multibanco() { + let params = STPSourceParams.multibancoParams( + withAmount: 1099, + returnURL: "https://shop.example.com/crtABC", + email: "user@example.com") + params.metadata = [ + "foo": "bar", + ] + + let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.multibanco) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/crtABC?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata")), "Metadata is not returned.") + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_klarna() { + let lineItems = [ + STPKlarnaLineItem(itemType: STPKlarnaLineItemType.SKU, itemDescription: "Test Item", quantity: NSNumber(value: 2), totalAmount: NSNumber(value: 500)), + STPKlarnaLineItem(itemType: STPKlarnaLineItemType.tax, itemDescription: "Tax", quantity: NSNumber(value: 1), totalAmount: NSNumber(value: 100)), + ] + let address = STPAddress() + address.line1 = "29 Arlington Avenue" + address.email = "test@example.com" + address.city = "London" + address.postalCode = "N1 7BE" + address.country = "GB" + address.phone = "02012267709" + let dob = STPDateOfBirth() + dob.day = 11 + dob.month = 3 + dob.year = 1952 + let params = STPSourceParams.klarnaParams(withReturnURL: "https://shop.example.com/return", currency: "GBP", purchaseCountry: "GB", items: lineItems, customPaymentMethods: [STPKlarnaPaymentMethods.none], billingAddress: address, billingFirstName: "Arthur", billingLastName: "Dent", billingDOB: dob) + + let client = STPAPIClient(publishableKey: STPTestingGBPublishableKey) + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.klarna) + XCTAssertEqual(source?.amount, NSNumber(value: 600)) + XCTAssertEqual(source?.owner?.address?.line1, address.line1) + XCTAssertEqual(source?.klarnaDetails?.purchaseCountry, "GB") + XCTAssertEqual(source?.redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(source?.redirect?.returnURL, URL(string: "https://shop.example.com/return?redirect_merchant_name=xctest")) + XCTAssertNotNil(source?.redirect?.url) + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + // 7/5/2023: + // Previously, we were allowed to use live keys and test w/ wechat on sources would generated "ios_native_url" + // however, this is no longer possible. Therefore, to get ample test coverage, we will have two tests: + // - testCreateSource_wechatPay_testMode - run in test mode, to ensure we can still call sources + // - testCreateSource_wechatPay_mocked - run a mocked version which is what we would expect in live mode + func testCreateSource_wechatPay_testMode() { + let params = STPSourceParams.wechatPay( + withAmount: 1010, + currency: "usd", + appId: "wxa0df51ec63e578ce", + statementDescriptor: nil) + let client = STPAPIClient(publishableKey: "pk_test_h0JFD5q63mLThM5JVSbrREmR") + let expectation = self.expectation(description: "Source creation") + client.createSource(with: params) { source, error in + XCTAssertNil(error) + XCTAssertNotNil(source) + XCTAssertEqual(source?.type, STPSourceType.weChatPay) + XCTAssertEqual(source?.status, STPSourceStatus.pending) + XCTAssertEqual(source?.amount, params.amount) + XCTAssertNil(source?.redirect) + + let wechat = source?.weChatPayDetails + XCTAssertNotNil(wechat) + // Will not be generated in test mode + // XCTAssertNotNil(wechat.weChatAppURL); + + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateSource_wechatPay_mocked() { + let params = STPSourceParams.wechatPay( + withAmount: 1010, + currency: "usd", + appId: "wxa0df51ec63e578ce", + statementDescriptor: nil) + + let source = STPFixtures.weChatPaySource() + XCTAssertEqual(source.type, STPSourceType.weChatPay) + XCTAssertEqual(source.status, STPSourceStatus.pending) + XCTAssertEqual(source.amount, params.amount) + XCTAssertNil(source.redirect) + + let wechat = source.weChatPayDetails + XCTAssertNotNil(wechat) + XCTAssertNotNil(wechat?.weChatAppURL) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceOwnerTest.swift b/Stripe/StripeiOSTests/STPSourceOwnerTest.swift new file mode 100644 index 00000000..b2d4f930 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceOwnerTest.swift @@ -0,0 +1,57 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceOwnerTest.m +// Stripe +// +// Created by Joey Dong on 6/23/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import XCTest + +class STPSourceOwnerTest: XCTestCase { + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = STPTestUtils.jsonNamed(STPTestJSONSource3DS)["owner"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceOwner.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSourceOwner.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed(STPTestJSONSource3DS)["owner"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + guard + let response = STPTestUtils.jsonNamed(STPTestJSONSource3DS)["owner"] as? NSDictionary, + let owner = STPSourceOwner.decodedObject(fromAPIResponse: response as? [AnyHashable: Any]) else { + XCTFail() + return + } + + XCTAssertEqual(owner.address?.city, "Pittsburgh") + XCTAssertEqual(owner.address?.country, "US") + XCTAssertEqual(owner.address?.line1, "123 Fake St") + XCTAssertEqual(owner.address?.line2, "Apt 1") + XCTAssertEqual(owner.address?.postalCode, "19219") + XCTAssertEqual(owner.address?.state, "PA") + XCTAssertEqual(owner.email, "jenny.rosen@example.com") + XCTAssertEqual(owner.name, "Jenny Rosen") + XCTAssertEqual(owner.phone, "555-867-5309") + XCTAssertEqual(owner.verifiedAddress?.city, "Pittsburgh") + XCTAssertEqual(owner.verifiedAddress?.country, "US") + XCTAssertEqual(owner.verifiedAddress?.line1, "123 Fake St") + XCTAssertEqual(owner.verifiedAddress?.line2, "Apt 1") + XCTAssertEqual(owner.verifiedAddress?.postalCode, "19219") + XCTAssertEqual(owner.verifiedAddress?.state, "PA") + XCTAssertEqual(owner.verifiedEmail, "jenny.rosen@example.com") + XCTAssertEqual(owner.verifiedName, "Jenny Rosen") + XCTAssertEqual(owner.verifiedPhone, "555-867-5309") + + XCTAssertEqual(owner.allResponseFields as NSDictionary, response) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceParamsTest.swift b/Stripe/StripeiOSTests/STPSourceParamsTest.swift new file mode 100644 index 00000000..0534c5b8 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceParamsTest.swift @@ -0,0 +1,317 @@ +// +// STPSourceParamsTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 1/25/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPSourceParamsTest: XCTestCase { + // MARK: - + func testInit() { + let sourceParams = STPSourceParams() + XCTAssertEqual(sourceParams.rawTypeString, "") + XCTAssertEqual(sourceParams.flow, .unknown) + XCTAssertEqual(sourceParams.usage, .unknown) + XCTAssertEqual(sourceParams.additionalAPIParameters as NSDictionary, [:] as NSDictionary) + } + + func testType() { + let sourceParams = STPSourceParams() + XCTAssertEqual(sourceParams.type, .unknown) + + sourceParams.rawTypeString = "bancontact" + XCTAssertEqual(sourceParams.type, .bancontact) + + sourceParams.rawTypeString = "card" + XCTAssertEqual(sourceParams.type, .card) + + sourceParams.rawTypeString = "giropay" + XCTAssertEqual(sourceParams.type, .giropay) + + sourceParams.rawTypeString = "ideal" + XCTAssertEqual(sourceParams.type, .iDEAL) + + sourceParams.rawTypeString = "sepa_debit" + XCTAssertEqual(sourceParams.type, .SEPADebit) + + sourceParams.rawTypeString = "sofort" + XCTAssertEqual(sourceParams.type, .sofort) + + sourceParams.rawTypeString = "three_d_secure" + XCTAssertEqual(sourceParams.type, .threeDSecure) + + sourceParams.rawTypeString = "alipay" + XCTAssertEqual(sourceParams.type, .alipay) + + sourceParams.rawTypeString = "p24" + XCTAssertEqual(sourceParams.type, .P24) + + sourceParams.rawTypeString = "eps" + XCTAssertEqual(sourceParams.type, .EPS) + + sourceParams.rawTypeString = "multibanco" + XCTAssertEqual(sourceParams.type, .multibanco) + + sourceParams.rawTypeString = "klarna" + XCTAssertEqual(sourceParams.type, .klarna) + + sourceParams.rawTypeString = "unknown" + XCTAssertEqual(sourceParams.type, .unknown) + + sourceParams.rawTypeString = "garbage" + XCTAssertEqual(sourceParams.type, .unknown) + } + + func testSetType() { + let sourceParams = STPSourceParams() + XCTAssertEqual(sourceParams.type, .unknown) + + sourceParams.type = .bancontact + XCTAssertEqual(sourceParams.rawTypeString, "bancontact") + + sourceParams.type = .card + XCTAssertEqual(sourceParams.rawTypeString, "card") + + sourceParams.type = .giropay + XCTAssertEqual(sourceParams.rawTypeString, "giropay") + + sourceParams.type = .iDEAL + XCTAssertEqual(sourceParams.rawTypeString, "ideal") + + sourceParams.type = .SEPADebit + XCTAssertEqual(sourceParams.rawTypeString, "sepa_debit") + + sourceParams.type = .sofort + XCTAssertEqual(sourceParams.rawTypeString, "sofort") + + sourceParams.type = .threeDSecure + XCTAssertEqual(sourceParams.rawTypeString, "three_d_secure") + + sourceParams.type = .alipay + XCTAssertEqual(sourceParams.rawTypeString, "alipay") + + sourceParams.type = .P24 + XCTAssertEqual(sourceParams.rawTypeString, "p24") + + sourceParams.type = .EPS + XCTAssertEqual(sourceParams.rawTypeString, "eps") + + sourceParams.type = .multibanco + XCTAssertEqual(sourceParams.rawTypeString, "multibanco") + + sourceParams.type = .klarna + XCTAssertEqual(sourceParams.rawTypeString, "klarna") + + sourceParams.type = .unknown + XCTAssertNil(sourceParams.rawTypeString) + } + + func testSetTypePreserveUnknownRawTypeString() { + let sourceParams = STPSourceParams() + sourceParams.rawTypeString = "money" + sourceParams.type = .unknown + XCTAssertEqual(sourceParams.rawTypeString, "money") + } + + func testRawTypeString() { + let sourceParams = STPSourceParams() + + // Check defaults to unknown + XCTAssertEqual(sourceParams.type, .unknown) + + // Check changing type sets rawTypeString + sourceParams.type = .card + XCTAssertEqual(sourceParams.rawTypeString, STPSource.string(from: .card)) + + // Check changing to unknown raw string sets type to unknown + sourceParams.rawTypeString = "new_source_type" + XCTAssertEqual(sourceParams.type, .unknown) + + // Check once unknown that setting type to unknown doesnt clobber string + sourceParams.type = .unknown + XCTAssertEqual(sourceParams.rawTypeString, "new_source_type") + + // Check setting string to known type sets type correctly + sourceParams.rawTypeString = STPSource.string(from: .iDEAL) + XCTAssertEqual(sourceParams.type, .iDEAL) + } + + func testFlowString() { + let sourceParams = STPSourceParams() + XCTAssertNil(sourceParams.flowString()) + + sourceParams.flow = .redirect + XCTAssertEqual(sourceParams.flowString(), "redirect") + + sourceParams.flow = .receiver + XCTAssertEqual(sourceParams.flowString(), "receiver") + + sourceParams.flow = .codeVerification + XCTAssertEqual(sourceParams.flowString(), "code_verification") + + sourceParams.flow = .none + XCTAssertEqual(sourceParams.flowString(), "none") + } + + // MARK: - Constructors Tests + func testCardParamsWithCard() { + let card = STPCardParams() + card.number = "4242 4242 4242 4242" + card.cvc = "123" + card.expMonth = 6 + card.expYear = 2024 + card.currency = "usd" + card.name = "Jenny Rosen" + card.address.line1 = "123 Fake Street" + card.address.line2 = "Apartment 4" + card.address.city = "New York" + card.address.state = "NY" + card.address.country = "USA" + card.address.postalCode = "10002" + + let source = STPSourceParams.cardParams(withCard: card) + let sourceCard = source.additionalAPIParameters["card"] as! [String: AnyHashable] + XCTAssertEqual(sourceCard["number"], card.number) + XCTAssertEqual(sourceCard["cvc"], card.cvc) + XCTAssertEqual(sourceCard["exp_month"], NSNumber(value: card.expMonth)) + XCTAssertEqual(sourceCard["exp_year"], NSNumber(value: card.expYear)) + XCTAssertEqual(source.owner!["name"] as? String, card.name) + let sourceAddress = source.owner!["address"] as! [AnyHashable: Any] + XCTAssertEqual(sourceAddress["line1"] as? String, card.address.line1) + XCTAssertEqual(sourceAddress["line2"] as? String, card.address.line2) + XCTAssertEqual(sourceAddress["city"] as? String, card.address.city) + XCTAssertEqual(sourceAddress["state"] as? String, card.address.state) + XCTAssertEqual(sourceAddress["postal_code"] as? String, card.address.postalCode) + XCTAssertEqual(sourceAddress["country"] as? String, card.address.country) + } + + func testParamsWithVisaCheckout() { + let params = STPSourceParams.visaCheckoutParams(withCallId: "12345678") + + XCTAssertEqual(params.type, .card) + let sourceCard = params.additionalAPIParameters["card"] as? [AnyHashable: Any] + XCTAssertNotNil(sourceCard) + let sourceVisaCheckout = sourceCard!["visa_checkout"] as? [AnyHashable: Any] + XCTAssertNotNil(sourceVisaCheckout) + XCTAssertEqual(sourceVisaCheckout!["callid"] as! String, "12345678") + } + + func testParamsWithMasterPass() { + let params = STPSourceParams.masterpassParams( + withCartId: "12345678", + transactionId: "87654321" + ) + + XCTAssertEqual(params.type, .card) + let sourceCard = params.additionalAPIParameters["card"] as? [AnyHashable: Any] + XCTAssertNotNil(sourceCard) + let sourceMasterpass = sourceCard!["masterpass"] as? [AnyHashable: Any] + XCTAssertNotNil(sourceMasterpass) + XCTAssertEqual(sourceMasterpass!["cart_id"] as! String, "12345678") + XCTAssertEqual(sourceMasterpass!["transaction_id"] as! String, "87654321") + } + + func testKlarnaParams() { + let params = STPSourceParams.klarnaParams( + withReturnURL: "return_url", + currency: "USD", + purchaseCountry: "US", + items: [], + customPaymentMethods: [.none], + billingAddress: nil, + billingFirstName: nil, + billingLastName: nil, + billingDOB: nil + ) + + XCTAssertNotNil(params) + } + + // MARK: - Redirect Dictionary Tests + func redirectMerchantNameQueryItemValue(fromURLString urlString: String?) -> String? { + let components = NSURLComponents(string: urlString ?? "") + for item in components!.queryItems! { + if item.name == "redirect_merchant_name" { + return item.value + } + } + return nil + } + + func testRedirectMerchantNameURL() { + var sourceParams = STPSourceParams.sofortParams( + withAmount: 1000, + returnURL: "test://foo?value=baz", + country: "DE", + statementDescriptor: nil + ) + + var params = STPFormEncoder.dictionary(forObject: sourceParams) + // Should be nil because we have no app name in tests + XCTAssertNil( + redirectMerchantNameQueryItemValue( + fromURLString: (params["redirect"] as! [String: AnyHashable])["return_url"] + as? String + ) + ) + + sourceParams.redirectMerchantName = "bar" + params = STPFormEncoder.dictionary(forObject: sourceParams) + XCTAssertEqual( + redirectMerchantNameQueryItemValue( + fromURLString: (params["redirect"] as! [String: AnyHashable])["return_url"] + as? String + ), + "bar" + ) + + sourceParams = STPSourceParams.sofortParams( + withAmount: 1000, + returnURL: "test://foo?redirect_merchant_name=Manual%20Custom%20Name", + country: "DE", + statementDescriptor: nil + ) + sourceParams.redirectMerchantName = "bar" + params = STPFormEncoder.dictionary(forObject: sourceParams) + // Don't override names set by the user directly in the url + XCTAssertEqual( + redirectMerchantNameQueryItemValue( + fromURLString: (params["redirect"] as! [String: AnyHashable])["return_url"] + as? String + ), + "Manual Custom Name" + ) + + } + + // MARK: - STPFormEncodable Tests + func testRootObjectName() { + XCTAssertNil(STPSourceParams.rootObjectName()) + } + + func testPropertyNamesToFormFieldNamesMapping() { + let sourceParams = STPSourceParams() + + let mapping = STPSourceParams.propertyNamesToFormFieldNamesMapping() + + for propertyName in mapping.keys { + XCTAssertFalse(propertyName.contains(":")) + XCTAssert(sourceParams.responds(to: NSSelectorFromString(propertyName))) + } + + for formFieldName in mapping.values { + XCTAssert(formFieldName.count > 0) + } + + XCTAssertEqual(mapping.values.count, Set(mapping.values).count) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceReceiverTest.swift b/Stripe/StripeiOSTests/STPSourceReceiverTest.swift new file mode 100644 index 00000000..16971fb0 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceReceiverTest.swift @@ -0,0 +1,48 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceReceiverTest.m +// Stripe +// +// Created by Joey Dong on 6/26/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import XCTest + +class STPSourceReceiverTest: XCTestCase { + // MARK: - Description Tests + + func testDescription() { + let receiver = STPSourceReceiver.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed(STPTestJSONSource3DS)["receiver"] as? [AnyHashable: Any]) + XCTAssertNotNil(receiver?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "address", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed(STPTestJSONSource3DS)["receiver"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceReceiver.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSourceReceiver.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed(STPTestJSONSource3DS)["receiver"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed(STPTestJSONSource3DS)["receiver"] as? [AnyHashable: Any] + let receiver = STPSourceReceiver.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(receiver?.address, "test_1MBhWS3uv4ynCfQXF3xQjJkzFPukr4K56N") + XCTAssertEqual(receiver?.amountCharged, NSNumber(value: 300)) + XCTAssertEqual(receiver?.amountReceived, NSNumber(value: 200)) + XCTAssertEqual(receiver?.amountReturned, NSNumber(value: 100)) + + XCTAssertEqual(receiver!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceRedirectTest.swift b/Stripe/StripeiOSTests/STPSourceRedirectTest.swift new file mode 100644 index 00000000..7e754144 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceRedirectTest.swift @@ -0,0 +1,100 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceRedirectTest.m +// Stripe +// +// Created by Joey Dong on 6/21/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +import XCTest + +class STPSourceRedirectTest: XCTestCase { + // MARK: - STPSourceRedirectStatus Tests + + func testStatusFromString() { + XCTAssertEqual(STPSourceRedirect.status(from: "pending"), STPSourceRedirectStatus.pending) + XCTAssertEqual(STPSourceRedirect.status(from: "PENDING"), STPSourceRedirectStatus.pending) + + XCTAssertEqual(STPSourceRedirect.status(from: "succeeded"), STPSourceRedirectStatus.succeeded) + XCTAssertEqual(STPSourceRedirect.status(from: "SUCCEEDED"), STPSourceRedirectStatus.succeeded) + + XCTAssertEqual(STPSourceRedirect.status(from: "failed"), STPSourceRedirectStatus.failed) + XCTAssertEqual(STPSourceRedirect.status(from: "FAILED"), STPSourceRedirectStatus.failed) + + XCTAssertEqual(STPSourceRedirect.status(from: "unknown"), STPSourceRedirectStatus.unknown) + XCTAssertEqual(STPSourceRedirect.status(from: "UNKNOWN"), STPSourceRedirectStatus.unknown) + + XCTAssertEqual(STPSourceRedirect.status(from: "not_required"), STPSourceRedirectStatus.notRequired) + XCTAssertEqual(STPSourceRedirect.status(from: "NOT_REQUIRED"), STPSourceRedirectStatus.notRequired) + + XCTAssertEqual(STPSourceRedirect.status(from: "garbage"), STPSourceRedirectStatus.unknown) + XCTAssertEqual(STPSourceRedirect.status(from: "GARBAGE"), STPSourceRedirectStatus.unknown) + } + + func testStringFromStatus() { + let values = [ + STPSourceRedirectStatus.pending, + STPSourceRedirectStatus.succeeded, + STPSourceRedirectStatus.failed, + STPSourceRedirectStatus.unknown, + ] + + for status in values { + let string = STPSourceRedirect.string(from: status) + + switch status { + case STPSourceRedirectStatus.pending: + XCTAssertEqual(string, "pending") + case STPSourceRedirectStatus.succeeded: + XCTAssertEqual(string, "succeeded") + case STPSourceRedirectStatus.failed: + XCTAssertEqual(string, "failed") + case STPSourceRedirectStatus.notRequired: + XCTAssertEqual(string, "not_required") + case STPSourceRedirectStatus.unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - Description Tests + + func testDescription() { + let redirect = STPSourceRedirect.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("3DSSource")["redirect"] as? [AnyHashable: Any]) + XCTAssertNotNil(redirect?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "return_url", + "status", + "url", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("3DSSource")["redirect"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceRedirect.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSourceRedirect.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("3DSSource")["redirect"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("3DSSource")["redirect"] as? [AnyHashable: Any] + let redirect = STPSourceRedirect.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(redirect?.returnURL, URL(string: "exampleappschema://stripe_callback")) + XCTAssertEqual(redirect?.status, STPSourceRedirectStatus.pending) + XCTAssertEqual(redirect?.url, URL(string: "https://hooks.stripe.com/redirect/authenticate/src_19YlvWAHEMiOZZp1QQlOD79v?client_secret=src_client_secret_kBwCSm6Xz5MQETiJ43hUH8qv")) + + XCTAssertEqual(redirect!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceSEPADebitDetailsTest.swift b/Stripe/StripeiOSTests/STPSourceSEPADebitDetailsTest.swift new file mode 100644 index 00000000..9fdda789 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceSEPADebitDetailsTest.swift @@ -0,0 +1,49 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceSEPADebitDetails.m +// Stripe +// +// Created by Joey Dong on 6/26/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +import XCTest + +class STPSourceSEPADebitDetailsTest: XCTestCase { + // MARK: - Description Tests + + func testDescription() { + let sepaDebitDetails = STPSourceSEPADebitDetails.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any]) + XCTAssertNotNil(sepaDebitDetails?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields: [String]? = [] + + for field in requiredFields ?? [] { + var response = STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceSEPADebitDetails.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSourceSEPADebitDetails.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("SEPADebitSource")["sepa_debit"] as? [AnyHashable: Any] + let sepaDebitDetails = STPSourceSEPADebitDetails.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(sepaDebitDetails?.bankCode, "37040044") + XCTAssertEqual(sepaDebitDetails?.country, "DE") + XCTAssertEqual(sepaDebitDetails?.fingerprint, "NxdSyRegc9PsMkWy") + XCTAssertEqual(sepaDebitDetails?.last4, "3001") + XCTAssertEqual(sepaDebitDetails?.mandateReference, "NXDSYREGC9PSMKWY") + XCTAssertEqual(sepaDebitDetails?.mandateURL, URL(string: "https://hooks.stripe.com/adapter/sepa_debit/file/src_18HgGjHNCLa1Vra6Y9TIP6tU/src_client_secret_XcBmS94nTg5o0xc9MSliSlDW")) + + XCTAssertEqual(sepaDebitDetails!.allResponseFields as NSDictionary, response! as NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPSourceTest.swift b/Stripe/StripeiOSTests/STPSourceTest.swift new file mode 100644 index 00000000..b441e583 --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceTest.swift @@ -0,0 +1,495 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceTest.m +// Stripe +// +// Created by Ben Guo on 1/24/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +@testable import StripePaymentsUI +import XCTest + +class STPSourceTest: XCTestCase { + // MARK: - STPSourceType Tests + + func testTypeFromString() { + XCTAssertEqual(STPSource.type(from: "bancontact"), STPSourceType.bancontact) + XCTAssertEqual(STPSource.type(from: "BANCONTACT"), STPSourceType.bancontact) + + XCTAssertEqual(STPSource.type(from: "card"), STPSourceType.card) + XCTAssertEqual(STPSource.type(from: "CARD"), STPSourceType.card) + + XCTAssertEqual(STPSource.type(from: "giropay"), STPSourceType.giropay) + XCTAssertEqual(STPSource.type(from: "GIROPAY"), STPSourceType.giropay) + + XCTAssertEqual(STPSource.type(from: "ideal"), STPSourceType.iDEAL) + XCTAssertEqual(STPSource.type(from: "IDEAL"), STPSourceType.iDEAL) + + XCTAssertEqual(STPSource.type(from: "sepa_debit"), STPSourceType.SEPADebit) + XCTAssertEqual(STPSource.type(from: "SEPA_DEBIT"), STPSourceType.SEPADebit) + + XCTAssertEqual(STPSource.type(from: "sofort"), STPSourceType.sofort) + XCTAssertEqual(STPSource.type(from: "Sofort"), STPSourceType.sofort) + + XCTAssertEqual(STPSource.type(from: "three_d_secure"), STPSourceType.threeDSecure) + XCTAssertEqual(STPSource.type(from: "THREE_D_SECURE"), STPSourceType.threeDSecure) + + XCTAssertEqual(STPSource.type(from: "alipay"), STPSourceType.alipay) + XCTAssertEqual(STPSource.type(from: "ALIPAY"), STPSourceType.alipay) + + XCTAssertEqual(STPSource.type(from: "p24"), STPSourceType.P24) + XCTAssertEqual(STPSource.type(from: "P24"), STPSourceType.P24) + + XCTAssertEqual(STPSource.type(from: "eps"), STPSourceType.EPS) + XCTAssertEqual(STPSource.type(from: "EPS"), STPSourceType.EPS) + + XCTAssertEqual(STPSource.type(from: "multibanco"), STPSourceType.multibanco) + XCTAssertEqual(STPSource.type(from: "MULTIBANCO"), STPSourceType.multibanco) + + XCTAssertEqual(STPSource.type(from: "unknown"), STPSourceType.unknown) + XCTAssertEqual(STPSource.type(from: "UNKNOWN"), STPSourceType.unknown) + + XCTAssertEqual(STPSource.type(from: "garbage"), STPSourceType.unknown) + XCTAssertEqual(STPSource.type(from: "GARBAGE"), STPSourceType.unknown) + } + + func testStringFromType() { + let values = [ + STPSourceType.bancontact, + STPSourceType.card, + STPSourceType.giropay, + STPSourceType.iDEAL, + STPSourceType.SEPADebit, + STPSourceType.sofort, + STPSourceType.threeDSecure, + STPSourceType.alipay, + STPSourceType.P24, + STPSourceType.EPS, + STPSourceType.multibanco, + STPSourceType.unknown, + ] + + for type in values { + let string = STPSource.string(from: type) + + switch type { + case STPSourceType.bancontact: + XCTAssertEqual(string, "bancontact") + case STPSourceType.card: + XCTAssertEqual(string, "card") + case STPSourceType.giropay: + XCTAssertEqual(string, "giropay") + case STPSourceType.iDEAL: + XCTAssertEqual(string, "ideal") + case STPSourceType.SEPADebit: + XCTAssertEqual(string, "sepa_debit") + case STPSourceType.sofort: + XCTAssertEqual(string, "sofort") + case STPSourceType.threeDSecure: + XCTAssertEqual(string, "three_d_secure") + case STPSourceType.alipay: + XCTAssertEqual(string, "alipay") + case STPSourceType.P24: + XCTAssertEqual(string, "p24") + case STPSourceType.EPS: + XCTAssertEqual(string, "eps") + case STPSourceType.multibanco: + XCTAssertEqual(string, "multibanco") + case STPSourceType.weChatPay: + XCTAssertEqual(string, "wechat") + case STPSourceType.klarna: + XCTAssertEqual(string, "klarna") + case STPSourceType.unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - STPSourceFlow Tests + + func testFlowFromString() { + XCTAssertEqual(STPSource.flow(from: "redirect"), .redirect) + XCTAssertEqual(STPSource.flow(from: "REDIRECT"), .redirect) + + XCTAssertEqual(STPSource.flow(from: "receiver"), .receiver) + XCTAssertEqual(STPSource.flow(from: "RECEIVER"), .receiver) + + XCTAssertEqual(STPSource.flow(from: "code_verification"), .codeVerification) + XCTAssertEqual(STPSource.flow(from: "CODE_VERIFICATION"), .codeVerification) + + XCTAssertEqual(STPSource.flow(from: "none"), .none) + XCTAssertEqual(STPSource.flow(from: "NONE"), .none) + + XCTAssertEqual(STPSource.flow(from: "garbage"), .unknown) + XCTAssertEqual(STPSource.flow(from: "GARBAGE"), .unknown) + } + + func testStringFromFlow() { + let values: [STPSourceFlow] = [ + .redirect, + .receiver, + .codeVerification, + .none, + .unknown, + ] + + for flow in values { + let string = STPSource.string(from: flow) + + switch flow { + case .redirect: + XCTAssertEqual(string, "redirect") + case .receiver: + XCTAssertEqual(string, "receiver") + case .codeVerification: + XCTAssertEqual(string, "code_verification") + case .none: + XCTAssertEqual(string, "none") + case .unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - STPSourceStatus Tests + + func testStatusFromString() { + XCTAssertEqual(STPSource.status(from: "pending"), STPSourceStatus.pending) + XCTAssertEqual(STPSource.status(from: "PENDING"), STPSourceStatus.pending) + + XCTAssertEqual(STPSource.status(from: "chargeable"), STPSourceStatus.chargeable) + XCTAssertEqual(STPSource.status(from: "CHARGEABLE"), STPSourceStatus.chargeable) + + XCTAssertEqual(STPSource.status(from: "consumed"), STPSourceStatus.consumed) + XCTAssertEqual(STPSource.status(from: "CONSUMED"), STPSourceStatus.consumed) + + XCTAssertEqual(STPSource.status(from: "canceled"), STPSourceStatus.canceled) + XCTAssertEqual(STPSource.status(from: "CANCELED"), STPSourceStatus.canceled) + + XCTAssertEqual(STPSource.status(from: "failed"), STPSourceStatus.failed) + XCTAssertEqual(STPSource.status(from: "FAILED"), STPSourceStatus.failed) + + XCTAssertEqual(STPSource.status(from: "garbage"), STPSourceStatus.unknown) + XCTAssertEqual(STPSource.status(from: "GARBAGE"), STPSourceStatus.unknown) + } + + func testStringFromStatus() { + let values = [ + STPSourceStatus.pending, + STPSourceStatus.chargeable, + STPSourceStatus.consumed, + STPSourceStatus.canceled, + STPSourceStatus.failed, + STPSourceStatus.unknown, + ] + + for status in values { + let string = STPSource.string(from: status) + + switch status { + case STPSourceStatus.pending: + XCTAssertEqual(string, "pending") + case STPSourceStatus.chargeable: + XCTAssertEqual(string, "chargeable") + case STPSourceStatus.consumed: + XCTAssertEqual(string, "consumed") + case STPSourceStatus.canceled: + XCTAssertEqual(string, "canceled") + case STPSourceStatus.failed: + XCTAssertEqual(string, "failed") + case STPSourceStatus.unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - STPSourceUsage Tests + + func testUsageFromString() { + XCTAssertEqual(STPSource.usage(from: "reusable"), STPSourceUsage.reusable) + XCTAssertEqual(STPSource.usage(from: "REUSABLE"), STPSourceUsage.reusable) + + XCTAssertEqual(STPSource.usage(from: "single_use"), STPSourceUsage.singleUse) + XCTAssertEqual(STPSource.usage(from: "SINGLE_USE"), STPSourceUsage.singleUse) + + XCTAssertEqual(STPSource.usage(from: "garbage"), STPSourceUsage.unknown) + XCTAssertEqual(STPSource.usage(from: "GARBAGE"), STPSourceUsage.unknown) + } + + func testStringFromUsage() { + let values = [ + STPSourceUsage.reusable, + STPSourceUsage.singleUse, + STPSourceUsage.unknown, + ] + + for usage in values { + let string = STPSource.string(from: usage) + + switch usage { + case STPSourceUsage.reusable: + XCTAssertEqual(string, "reusable") + case STPSourceUsage.singleUse: + XCTAssertEqual(string, "single_use") + case STPSourceUsage.unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - Equality Tests + + func testSourceEquals() { + let source1 = STPSource.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("AlipaySource")) + let source2 = STPSource.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("AlipaySource")) + + XCTAssertEqual(source1, source1) + XCTAssertEqual(source1, source2) + + XCTAssertEqual(source1?.hash, source1?.hash) + XCTAssertEqual(source1?.hash, source2?.hash) + } + + // MARK: - Description Tests + + func testDescription() { + let source = STPSource.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("AlipaySource")) + XCTAssertNotNil(source?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "id", + "livemode", + "status", + "type", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("AlipaySource") + response?.removeValue(forKey: field) + + XCTAssertNil(STPSource.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSource.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("AlipaySource"))) + } + + func testDecodingSource_3ds() { + let response = STPTestUtils.jsonNamed(STPTestJSONSource3DS) + let source = STPSource.decodedObject(fromAPIResponse: response) + XCTAssertEqual(source?.stripeID, "src_456") + XCTAssertEqual(source?.amount, NSNumber(value: 1099)) + XCTAssertEqual(source?.clientSecret, "src_client_secret_456") + XCTAssertEqual(source?.created?.timeIntervalSince1970 ?? 0, 1483663790.0, accuracy: 1.0) + XCTAssertEqual(source?.currency, "eur") + XCTAssertEqual(source?.flow, .redirect) + XCTAssertEqual(source?.livemode, false) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata"))) + XCTAssertNotNil(source?.owner) // STPSourceOwnerTest + XCTAssertNotNil(source?.receiver) // STPSourceReceiverTest + XCTAssertNotNil(source?.redirect) // STPSourceRedirectTest + XCTAssertEqual(source?.status, STPSourceStatus.pending) + XCTAssertEqual(source?.type, STPSourceType.threeDSecure) + XCTAssertEqual(source?.usage, STPSourceUsage.singleUse) + XCTAssertNil(source?.verification) + var threedsecure = response?["three_d_secure"] as? [AnyHashable: Any] + threedsecure?.removeValue(forKey: "customer") // should be nil +// XCTAssertEqual(source?.details, threedsecure) + XCTAssertNotNil(source?.details) + XCTAssertNil(source?.cardDetails) // STPSourceCardDetailsTest + XCTAssertNil(source?.sepaDebitDetails) // STPSourceSEPADebitDetailsTest + } + + func testDecodingSource_alipay() { + let response = STPTestUtils.jsonNamed(STPTestJSONSourceAlipay) + let source = STPSource.decodedObject(fromAPIResponse: response) + XCTAssertEqual(source?.stripeID, "src_123") + XCTAssertEqual(source?.amount, NSNumber(value: 1099)) + XCTAssertEqual(source?.clientSecret, "src_client_secret_123") + XCTAssertEqual(source?.created?.timeIntervalSince1970 ?? 0, 1445277809.0, accuracy: 1.0) + XCTAssertEqual(source?.currency, "usd") + XCTAssertEqual(source?.flow, .redirect) + XCTAssertEqual(source?.livemode, true) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata"))) + XCTAssertNotNil(source?.owner) // STPSourceOwnerTest + XCTAssertNil(source?.receiver) // STPSourceReceiverTest + XCTAssertNotNil(source?.redirect) // STPSourceRedirectTest + XCTAssertEqual(source?.status, STPSourceStatus.pending) + XCTAssertEqual(source?.type, STPSourceType.alipay) + XCTAssertEqual(source?.usage, STPSourceUsage.singleUse) + XCTAssertNil(source?.verification) + var alipayResponse = response!["alipay"] as? [AnyHashable: Any] + alipayResponse?.removeValue(forKey: "native_url") // should be nil + alipayResponse?.removeValue(forKey: "statement_descriptor") // should be nil + XCTAssertEqual(source!.details! as NSDictionary, alipayResponse! as NSDictionary) + XCTAssertNil(source?.cardDetails) // STPSourceCardDetailsTest + XCTAssertNil(source?.sepaDebitDetails) // STPSourceSEPADebitDetailsTest + } + + func testDecodingSource_card() { + let response = STPTestUtils.jsonNamed(STPTestJSONSourceCard) + let source = STPSource.decodedObject(fromAPIResponse: response) + XCTAssertEqual(source?.stripeID, "src_123") + XCTAssertNil(source?.amount) + XCTAssertEqual(source?.clientSecret, "src_client_secret_123") + XCTAssertEqual(source?.created?.timeIntervalSince1970 ?? 0, 1483575790.0, accuracy: 1.0) + XCTAssertNil(source?.currency) + XCTAssertEqual(source?.flow, STPSourceFlow.none) + XCTAssertEqual(source?.livemode, false) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata"))) + XCTAssertNotNil(source?.owner) // STPSourceOwnerTest + XCTAssertNil(source?.receiver) // STPSourceReceiverTest + XCTAssertNil(source?.redirect) // STPSourceRedirectTest + XCTAssertEqual(source?.status, STPSourceStatus.chargeable) + XCTAssertEqual(source?.type, STPSourceType.card) + XCTAssertEqual(source?.usage, STPSourceUsage.reusable) + XCTAssertNil(source?.verification) + XCTAssertEqual(source!.details! as NSDictionary, response!["card"] as! NSDictionary) + XCTAssertNotNil(source?.cardDetails) // STPSourceCardDetailsTest + XCTAssertNil(source?.sepaDebitDetails) // STPSourceSEPADebitDetailsTest + } + + func testDecodingSource_ideal() { + let response = STPTestUtils.jsonNamed(STPTestJSONSourceiDEAL) + let source = STPSource.decodedObject(fromAPIResponse: response) + XCTAssertEqual(source?.stripeID, "src_123") + XCTAssertEqual(source?.amount, NSNumber(value: 1099)) + XCTAssertEqual(source?.clientSecret, "src_client_secret_123") + XCTAssertEqual(source?.created?.timeIntervalSince1970 ?? 0, 1445277809.0, accuracy: 1.0) + XCTAssertEqual(source?.currency, "eur") + XCTAssertEqual(source?.flow, .redirect) + XCTAssertEqual(source?.livemode, true) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata"))) + XCTAssertNotNil(source?.owner) // STPSourceOwnerTest + XCTAssertNil(source?.receiver) // STPSourceReceiverTest + XCTAssertNotNil(source?.redirect) // STPSourceRedirectTest + XCTAssertEqual(source?.status, STPSourceStatus.pending) + XCTAssertEqual(source?.type, STPSourceType.iDEAL) + XCTAssertEqual(source?.usage, STPSourceUsage.singleUse) + XCTAssertNil(source?.verification) + XCTAssertEqual(source!.details! as NSDictionary, response!["ideal"] as! NSDictionary) + XCTAssertNil(source?.cardDetails) // STPSourceCardDetailsTest + XCTAssertNil(source?.sepaDebitDetails) // STPSourceSEPADebitDetailsTest + } + + func testDecodingSource_sepa_debit() { + let response = STPTestUtils.jsonNamed(STPTestJSONSourceSEPADebit) + let source = STPSource.decodedObject(fromAPIResponse: response) + XCTAssertEqual(source?.stripeID, "src_18HgGjHNCLa1Vra6Y9TIP6tU") + XCTAssertNil(source?.amount) + XCTAssertEqual(source?.clientSecret, "src_client_secret_XcBmS94nTg5o0xc9MSliSlDW") + XCTAssertEqual(source?.created?.timeIntervalSince1970 ?? 0, 1464803577.0, accuracy: 1.0) + XCTAssertEqual(source?.currency, "eur") + XCTAssertEqual(source?.flow, STPSourceFlow.none) + XCTAssertEqual(source?.livemode, false) + XCTAssertNil(source?.perform(NSSelectorFromString("metadata"))) + XCTAssertEqual(source?.owner?.name, "Jenny Rosen") + XCTAssertNotNil(source?.owner) // STPSourceOwnerTest + XCTAssertNil(source?.receiver) // STPSourceReceiverTest + XCTAssertNil(source?.redirect) // STPSourceRedirectTest + XCTAssertEqual(source?.status, STPSourceStatus.chargeable) + XCTAssertEqual(source?.type, STPSourceType.SEPADebit) + XCTAssertEqual(source?.usage, STPSourceUsage.reusable) + XCTAssertEqual(source?.verification?.attemptsRemaining, NSNumber(value: 5)) + XCTAssertEqual(source?.verification?.status, STPSourceVerificationStatus.pending) + XCTAssertEqual(source!.details! as NSDictionary, response!["sepa_debit"] as! NSDictionary) + XCTAssertNil(source?.cardDetails) // STPSourceCardDetailsTest + XCTAssertNotNil(source?.sepaDebitDetails) // STPSourceSEPADebitDetailsTest + } + + // MARK: - STPPaymentOption Tests + + func possibleAPIResponses() -> [[AnyHashable: Any]] { + return [ + STPTestUtils.jsonNamed(STPTestJSONSourceCard), + STPTestUtils.jsonNamed(STPTestJSONSource3DS), + STPTestUtils.jsonNamed(STPTestJSONSourceAlipay), + STPTestUtils.jsonNamed(STPTestJSONSourceBancontact), + STPTestUtils.jsonNamed(STPTestJSONSourceEPS), + STPTestUtils.jsonNamed(STPTestJSONSourceGiropay), + STPTestUtils.jsonNamed(STPTestJSONSourceiDEAL), + STPTestUtils.jsonNamed(STPTestJSONSourceMultibanco), + STPTestUtils.jsonNamed(STPTestJSONSourceP24), + STPTestUtils.jsonNamed(STPTestJSONSourceSEPADebit), + STPTestUtils.jsonNamed(STPTestJSONSourceSofort), + ] + } + + func testPaymentOptionImage() { + for response in possibleAPIResponses() { + let source = STPSource.decodedObject(fromAPIResponse: response) + + switch source!.type { + case STPSourceType.card: + STPAssertEqualImages(source?.image, STPImageLibrary.cardBrandImage(for: source?.cardDetails?.brand ?? .unknown)) + default: + STPAssertEqualImages(source?.image, STPImageLibrary.cardBrandImage(for: STPCardBrand.unknown)) + } + } + } + + func testPaymentOptionTemplateImage() { + for response in possibleAPIResponses() { + let source = STPSource.decodedObject(fromAPIResponse: response) + + switch source!.type { + case STPSourceType.card: + STPAssertEqualImages(source?.templateImage, STPImageLibrary.templatedBrandImage(for: source?.cardDetails?.brand ?? .unknown)) + default: + STPAssertEqualImages(source?.templateImage, STPImageLibrary.templatedBrandImage(for: STPCardBrand.unknown)) + } + } + } + + func testPaymentOptionLabel() { + for response in possibleAPIResponses() { + let source = STPSource.decodedObject(fromAPIResponse: response) + + switch source!.type { + case STPSourceType.bancontact: + XCTAssertEqual(source?.label, "Bancontact") + case STPSourceType.card: + XCTAssertEqual(source?.label, "Visa 5556") + case STPSourceType.giropay: + XCTAssertEqual(source?.label, "giropay") + case STPSourceType.iDEAL: + XCTAssertEqual(source?.label, "iDEAL") + case STPSourceType.SEPADebit: + XCTAssertEqual(source?.label, "SEPA Debit") + case STPSourceType.sofort: + XCTAssertEqual(source?.label, "Sofort") + case STPSourceType.threeDSecure: + XCTAssertEqual(source?.label, "3D Secure") + case STPSourceType.alipay: + XCTAssertEqual(source?.label, "Alipay") + case STPSourceType.P24: + XCTAssertEqual(source?.label, "Przelewy24") + case STPSourceType.EPS: + XCTAssertEqual(source?.label, "EPS") + case STPSourceType.multibanco: + XCTAssertEqual(source?.label, "Multibanco") + case STPSourceType.weChatPay: + XCTAssertEqual(source?.label, "WeChat Pay") + case STPSourceType.klarna: + XCTAssertEqual(source?.label, "Klarna") + case STPSourceType.unknown: + XCTAssertEqual(source?.label, STPCard.string(from: .unknown)) + default: + break + } + } + } +} diff --git a/Stripe/StripeiOSTests/STPSourceVerificationTest.swift b/Stripe/StripeiOSTests/STPSourceVerificationTest.swift new file mode 100644 index 00000000..cce05ead --- /dev/null +++ b/Stripe/StripeiOSTests/STPSourceVerificationTest.swift @@ -0,0 +1,92 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPSourceVerificationTest.m +// Stripe +// +// Created by Joey Dong on 6/21/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +@testable import StripePayments +import XCTest + +class STPSourceVerificationTest: XCTestCase { + // MARK: - STPSourceVerificationStatus Tests + + func testStatusFromString() { + XCTAssertEqual(STPSourceVerification.status(from: "pending"), STPSourceVerificationStatus.pending) + XCTAssertEqual(STPSourceVerification.status(from: "pending"), STPSourceVerificationStatus.pending) + + XCTAssertEqual(STPSourceVerification.status(from: "succeeded"), STPSourceVerificationStatus.succeeded) + XCTAssertEqual(STPSourceVerification.status(from: "SUCCEEDED"), STPSourceVerificationStatus.succeeded) + + XCTAssertEqual(STPSourceVerification.status(from: "failed"), STPSourceVerificationStatus.failed) + XCTAssertEqual(STPSourceVerification.status(from: "FAILED"), STPSourceVerificationStatus.failed) + + XCTAssertEqual(STPSourceVerification.status(from: "unknown"), STPSourceVerificationStatus.unknown) + XCTAssertEqual(STPSourceVerification.status(from: "UNKNOWN"), STPSourceVerificationStatus.unknown) + + XCTAssertEqual(STPSourceVerification.status(from: "garbage"), STPSourceVerificationStatus.unknown) + XCTAssertEqual(STPSourceVerification.status(from: "GARBAGE"), STPSourceVerificationStatus.unknown) + } + + func testStringFromStatus() { + let values = [ + STPSourceVerificationStatus.pending, + STPSourceVerificationStatus.succeeded, + STPSourceVerificationStatus.failed, + STPSourceVerificationStatus.unknown, + ] + + for status in values { + let string = STPSourceVerification.string(from: status) + + switch status { + case STPSourceVerificationStatus.pending: + XCTAssertEqual(string, "pending") + case STPSourceVerificationStatus.succeeded: + XCTAssertEqual(string, "succeeded") + case STPSourceVerificationStatus.failed: + XCTAssertEqual(string, "failed") + case STPSourceVerificationStatus.unknown: + XCTAssertNil(string) + default: + break + } + } + } + + // MARK: - Description Tests + + func testDescription() { + let verification = STPSourceVerification.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("SEPADebitSource")["verification"] as? [AnyHashable: Any]) + XCTAssertNotNil(verification?.description) + } + + // MARK: - STPAPIResponseDecodable Tests + + func testDecodedObjectFromAPIResponseRequiredFields() { + let requiredFields = [ + "status", + ] + + for field in requiredFields { + var response = STPTestUtils.jsonNamed("SEPADebitSource")["verification"] as? [AnyHashable: Any] + response?.removeValue(forKey: field) + + XCTAssertNil(STPSourceVerification.decodedObject(fromAPIResponse: response)) + } + + XCTAssertNotNil(STPSourceVerification.decodedObject(fromAPIResponse: STPTestUtils.jsonNamed("SEPADebitSource")["verification"] as? [AnyHashable: Any])) + } + + func testDecodedObjectFromAPIResponseMapping() { + let response = STPTestUtils.jsonNamed("SEPADebitSource")["verification"] as? [AnyHashable: Any] + let verification = STPSourceVerification.decodedObject(fromAPIResponse: response) + + XCTAssertEqual(verification?.attemptsRemaining, NSNumber(value: 5)) + XCTAssertEqual(verification?.status, STPSourceVerificationStatus.pending) + + XCTAssertEqual(verification?.allResponseFields as? NSDictionary, response as? NSDictionary) + } +} diff --git a/Stripe/StripeiOSTests/STPStackViewWithSeparatorSnapshotTests.swift b/Stripe/StripeiOSTests/STPStackViewWithSeparatorSnapshotTests.swift new file mode 100644 index 00000000..b26e48ce --- /dev/null +++ b/Stripe/StripeiOSTests/STPStackViewWithSeparatorSnapshotTests.swift @@ -0,0 +1,214 @@ +// +// STPStackViewWithSeparatorSnapshotTests.swift +// StripeiOS Tests +// +// Created by Cameron Sabol on 10/23/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +@_spi(STP) import StripeUICore + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPStackViewWithSeparatorSnapshotTests: STPSnapshotTestCase { + + func embedInRenderableView(_ stackView: StackViewWithSeparator) -> UIView { + let containingView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 400)) + containingView.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: containingView.leadingAnchor), + containingView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), + stackView.topAnchor.constraint(equalTo: containingView.topAnchor), + containingView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + ]) + containingView.frame.size = containingView.systemLayoutSizeFitting( + UIView.layoutFittingCompressedSize + ) + return containingView + } + + func testHorizontal() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testVertical() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testSingleArrangedSubviewHorizontal() { + let label1 = UILabel() + label1.text = "Label 1" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testSingleArrangedSubviewVertical() { + let label1 = UILabel() + label1.text = "Label 1" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testCustomColorHorizontal() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .red + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testCustomColorVertical() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .red + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testDisabledColor() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + stackView.drawBorder = true + stackView.isUserInteractionEnabled = false + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testCustomBackgroundColor() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + stackView.drawBorder = true + stackView.customBackgroundColor = .green + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testCustomDisabledColor() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + stackView.customBackgroundDisabledColor = .green + stackView.drawBorder = true + stackView.isUserInteractionEnabled = false + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testPartialSeparatorHorizontal() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + stackView.separatorStyle = .partial + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + + func testPartialSeparatorVertical() { + let label1 = UILabel() + label1.text = "Label 1" + let label2 = UILabel() + label2.text = "Label 2" + let label3 = UILabel() + label3.text = "Label 3" + let stackView = StackViewWithSeparator(arrangedSubviews: [label1, label2, label3]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 1 + stackView.separatorColor = .lightGray + stackView.separatorStyle = .partial + + STPSnapshotVerifyView(embedInRenderableView(stackView)) + } + +} diff --git a/Stripe/StripeiOSTests/STPStringUtilsTest.swift b/Stripe/StripeiOSTests/STPStringUtilsTest.swift new file mode 100644 index 00000000..310ede4a --- /dev/null +++ b/Stripe/StripeiOSTests/STPStringUtilsTest.swift @@ -0,0 +1,142 @@ +// +// STPStringUtilsTest.swift +// StripeiOS Tests +// +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation + +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPStringUtilsTest: XCTestCase { + func testParseRangeSingleTagSuccess1() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRange( + from: "Test string", + withTag: "b" + ) { string, range in + XCTAssertTrue(NSEqualRanges(range, NSRange(location: 5, length: 6))) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testParseRangeSingleTagSuccess2() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRange( + from: "Test string", + withTag: "b" + ) { string, range in + XCTAssertTrue(NSEqualRanges(range, NSRange(location: 8, length: 10))) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testParseRangeSingleTagFailure1() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRange( + from: "Test string", + withTag: "a" + ) { string, range in + XCTAssertEqual(range.location, NSNotFound) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testParseRangeSingleTagFailure2() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRange( + from: "Test string", + withTag: "b" + ) { string, range in + XCTAssertEqual(range.location, NSNotFound) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testParseRangeMultiTag1() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRanges( + from: "Test string", + withTags: Set(["a", "b", "c"]) + ) { string, tagMap in + XCTAssertTrue(NSEqualRanges(tagMap["a"]!.rangeValue, NSRange(location: 0, length: 4))) + XCTAssertTrue(NSEqualRanges(tagMap["b"]!.rangeValue, NSRange(location: 5, length: 6))) + XCTAssertEqual(tagMap["c"]!.rangeValue.location, NSNotFound) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testParseRangeMultiTag2() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRanges(from: "Test string", withTags: Set(["a", "b", "c"])) { + string, + tagMap in + XCTAssertEqual(tagMap["a"]!.rangeValue.location, NSNotFound) + XCTAssertEqual(tagMap["b"]!.rangeValue.location, NSNotFound) + XCTAssertEqual(tagMap["c"]!.rangeValue.location, NSNotFound) + XCTAssertEqual(string, "Test string") + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + func testParseRangeWithOverlappingRanges() { + let exp = self.expectation(description: "Parsed") + STPStringUtils.parseRanges( + from: "Test string", + withTags: Set(["a", "b"]) + ) { string, tagMap in + XCTAssertEqual(string, "Test string") + XCTAssertTrue(tagMap.isEmpty) + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + func testHasOverlappingRanges_singleItem() { + let ranges: [NSValue: String] = [ + NSValue(range: NSRange(location: 0, length: 2)): "a", + ] + XCTAssertFalse(STPStringUtils.hasOverlappingRanges(ranges: ranges)) + } + func testHasOverlappingRanges_nonOverlapping() { + let ranges: [NSValue: String] = [ + NSValue(range: NSRange(location: 0, length: 2)): "a", + NSValue(range: NSRange(location: 2, length: 1)): "b", + ] + XCTAssertFalse(STPStringUtils.hasOverlappingRanges(ranges: ranges)) + } + func testHasOverlappingRanges_overlapping() { + let ranges: [NSValue: String] = [ + NSValue(range: NSRange(location: 0, length: 2)): "a", + NSValue(range: NSRange(location: 1, length: 1)): "b", + ] + XCTAssert(STPStringUtils.hasOverlappingRanges(ranges: ranges)) + } + + func testExpirationDateStrings() { + XCTAssertEqual(STPStringUtils.expirationDateString(from: "12/1995"), "12/95") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "12 / 1995"), "12 / 95") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "12 /1995"), "12 /95") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "1295"), "1295") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "12/95"), "12/95") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "08/2001"), "08/01") + XCTAssertEqual(STPStringUtils.expirationDateString(from: " 08/a 2001"), " 08/a 2001") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "20/2022"), "20/22") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "20/202222"), "20/22") + XCTAssertEqual(STPStringUtils.expirationDateString(from: ""), "") + XCTAssertEqual(STPStringUtils.expirationDateString(from: " "), " ") + XCTAssertEqual(STPStringUtils.expirationDateString(from: "12/"), "12/") + } +} diff --git a/Stripe/StripeiOSTests/STPSwiftFixtures.swift b/Stripe/StripeiOSTests/STPSwiftFixtures.swift new file mode 100644 index 00000000..f522531e --- /dev/null +++ b/Stripe/StripeiOSTests/STPSwiftFixtures.swift @@ -0,0 +1,107 @@ +// +// STPSwiftFixtures.swift +// StripeiOS Tests +// +// Created by David Estes on 10/2/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import Foundation + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +@_exported @testable import StripePaymentsObjcTestUtils + +extension STPFixtures { + /// A customer-scoped ephemeral key that expires in 100 seconds. + class func ephemeralKey() -> STPEphemeralKey { + var response = STPTestUtils.jsonNamed("EphemeralKey") + let interval: TimeInterval = 100 + response!["expires"] = NSNumber(value: Date(timeIntervalSinceNow: interval).timeIntervalSince1970) + return .decodedObject(fromAPIResponse: response)! + } + + /// A customer-scoped ephemeral key that expires in 10 seconds. + class func expiringEphemeralKey() -> STPEphemeralKey { + var response = STPTestUtils.jsonNamed("EphemeralKey") + let interval: TimeInterval = 10 + response!["expires"] = NSNumber(value: Date(timeIntervalSinceNow: interval).timeIntervalSince1970) + return .decodedObject(fromAPIResponse: response)! + } +} + +class MockEphemeralKeyProvider: NSObject, STPCustomerEphemeralKeyProvider { + func createCustomerKey( + withAPIVersion apiVersion: String, + completion: @escaping STPJSONResponseCompletionBlock + ) { + completion(STPFixtures.ephemeralKey().allResponseFields, nil) + } +} + +@objcMembers +@objc class Testing_StaticCustomerContext_Objc: Testing_StaticCustomerContext { + +} + +@objcMembers +class Testing_StaticCustomerContext: STPCustomerContext { + var customer: STPCustomer + var paymentMethods: [STPPaymentMethod] + convenience init() { + let customer = STPFixtures.customerWithSingleCardTokenSource() + let paymentMethods = [STPFixtures.paymentMethod()].compactMap { $0 } + self.init( + customer: customer, + paymentMethods: paymentMethods + ) + } + init( + customer: STPCustomer, + paymentMethods: [STPPaymentMethod] + ) { + self.customer = customer + self.paymentMethods = paymentMethods + super.init( + keyManager: STPEphemeralKeyManager( + keyProvider: MockEphemeralKeyProvider(), + apiVersion: "1", + performsEagerFetching: false + ), + apiClient: STPAPIClient.shared + ) + } + + override func retrieveCustomer(_ completion: STPCustomerCompletionBlock?) { + if let completion = completion { + completion(customer, nil) + } + } + + override func listPaymentMethodsForCustomer(completion: STPPaymentMethodsCompletionBlock?) { + if let completion = completion { + completion(paymentMethods, nil) + } + } + + var didAttach = false + override func attachPaymentMethod( + toCustomer paymentMethod: STPPaymentMethod, + completion: STPErrorBlock? + ) { + didAttach = true + if let completion = completion { + completion(nil) + } + } + + override func retrieveLastSelectedPaymentMethodIDForCustomer( + completion: @escaping (String?, Error?) -> Void + ) { + completion(nil, nil) + } +} diff --git a/Stripe/StripeiOSTests/STPTextFieldDelegateProxyTests.swift b/Stripe/StripeiOSTests/STPTextFieldDelegateProxyTests.swift new file mode 100644 index 00000000..31c53a7c --- /dev/null +++ b/Stripe/StripeiOSTests/STPTextFieldDelegateProxyTests.swift @@ -0,0 +1,37 @@ +// +// STPTextFieldDelegateProxyTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 11/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPTextFieldDelegateProxyTests: XCTestCase { + + func testProxyShouldDeleteLeadingWhitespace() { + let textField = STPFormTextField() + textField.autoFormattingBehavior = .cardNumbers + textField.text = " " // space + + let sut = STPTextFieldDelegateProxy() + + let result = sut.textField( + textField, + shouldChangeCharactersIn: NSRange(location: 0, length: 1), + replacementString: "" + ) + + // Proxy should handle the deletion and return `false`. + XCTAssertFalse(result) + XCTAssertEqual(textField.text, "") + } + +} diff --git a/Stripe/StripeiOSTests/STPThreeDSButtonCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSButtonCustomizationTest.swift new file mode 100644 index 00000000..85a2e8fa --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSButtonCustomizationTest.swift @@ -0,0 +1,40 @@ +// +// STPThreeDSButtonCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/17/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSButtonCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSButtonCustomization.defaultSettings(for: .next) + customization.backgroundColor = UIColor.red + customization.cornerRadius = -1 + customization.titleStyle = .lowercase + customization.font = UIFont.italicSystemFont(ofSize: 1) + customization.textColor = UIColor.blue + + let stdsCustomization = customization.buttonCustomization + XCTAssertEqual(UIColor.red, stdsCustomization.backgroundColor) + XCTAssertEqual(stdsCustomization.backgroundColor, customization.backgroundColor) + + XCTAssertEqual(-1, stdsCustomization.cornerRadius, accuracy: 0.1) + XCTAssertEqual(stdsCustomization.cornerRadius, customization.cornerRadius, accuracy: 0.1) + + XCTAssertEqual(.lowercase, stdsCustomization.titleStyle) + XCTAssertEqual(stdsCustomization.titleStyle.rawValue, customization.titleStyle.rawValue) + + XCTAssertEqual(UIFont.italicSystemFont(ofSize: 1), stdsCustomization.font) + XCTAssertEqual(stdsCustomization.font, customization.font) + + XCTAssertEqual(UIColor.blue, stdsCustomization.textColor) + XCTAssertEqual(stdsCustomization.textColor, customization.textColor) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSFooterCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSFooterCustomizationTest.swift new file mode 100644 index 00000000..81447030 --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSFooterCustomizationTest.swift @@ -0,0 +1,45 @@ +// +// STPThreeDSFooterCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/17/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSFooterCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSFooterCustomization.defaultSettings() + customization.backgroundColor = UIColor.red + customization.chevronColor = UIColor.blue + customization.headingTextColor = UIColor.green + customization.headingFont = UIFont.systemFont(ofSize: 1) + customization.font = UIFont.systemFont(ofSize: 2) + customization.textColor = UIColor.magenta + + let stdsCustomization = customization.footerCustomization + + XCTAssertEqual(UIColor.red, stdsCustomization.backgroundColor) + XCTAssertEqual(stdsCustomization.backgroundColor, customization.backgroundColor) + + XCTAssertEqual(UIColor.blue, stdsCustomization.chevronColor) + XCTAssertEqual(stdsCustomization.chevronColor, customization.chevronColor) + + XCTAssertEqual(UIColor.green, stdsCustomization.headingTextColor) + XCTAssertEqual(stdsCustomization.headingTextColor, customization.headingTextColor) + + XCTAssertEqual(UIFont.systemFont(ofSize: 1), stdsCustomization.headingFont) + XCTAssertEqual(stdsCustomization.headingFont, customization.headingFont) + + XCTAssertEqual(UIFont.systemFont(ofSize: 2), stdsCustomization.font) + XCTAssertEqual(stdsCustomization.font, customization.font) + + XCTAssertEqual(UIColor.magenta, stdsCustomization.textColor) + XCTAssertEqual(stdsCustomization.textColor, customization.textColor) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSLabelCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSLabelCustomizationTest.swift new file mode 100644 index 00000000..bd546dfc --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSLabelCustomizationTest.swift @@ -0,0 +1,37 @@ +// +// STPThreeDSLabelCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/17/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSLabelCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSLabelCustomization.defaultSettings() + customization.headingFont = UIFont.systemFont(ofSize: 1) + customization.headingTextColor = UIColor.red + customization.font = UIFont.systemFont(ofSize: 2) + customization.textColor = UIColor.blue + + let stdsCustomization = customization.labelCustomization + + XCTAssertEqual(UIFont.systemFont(ofSize: 1), stdsCustomization.headingFont) + XCTAssertEqual(stdsCustomization.headingFont, customization.headingFont) + + XCTAssertEqual(UIColor.red, stdsCustomization.headingTextColor) + XCTAssertEqual(stdsCustomization.headingTextColor, customization.headingTextColor) + + XCTAssertEqual(UIFont.systemFont(ofSize: 2), stdsCustomization.font) + XCTAssertEqual(stdsCustomization.font, customization.font) + + XCTAssertEqual(UIColor.blue, stdsCustomization.textColor) + XCTAssertEqual(stdsCustomization.textColor, customization.textColor) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSNavigationBarCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSNavigationBarCustomizationTest.swift new file mode 100644 index 00000000..1d09877d --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSNavigationBarCustomizationTest.swift @@ -0,0 +1,48 @@ +// +// STPThreeDSNavigationBarCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/17/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSNavigationBarCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSNavigationBarCustomization.defaultSettings() + customization.font = UIFont.italicSystemFont(ofSize: 1) + customization.textColor = UIColor.blue + customization.barTintColor = UIColor.red + customization.barStyle = UIBarStyle.black + customization.translucent = false + customization.headerText = "foo" + customization.buttonText = "bar" + + let stdsCustomization = customization.navigationBarCustomization + XCTAssertEqual(UIFont.italicSystemFont(ofSize: 1), stdsCustomization.font) + XCTAssertEqual(stdsCustomization.font, customization.font) + + XCTAssertEqual(UIColor.blue, stdsCustomization.textColor) + XCTAssertEqual(stdsCustomization.textColor, customization.textColor) + + XCTAssertEqual(UIColor.red, stdsCustomization.barTintColor) + XCTAssertEqual(stdsCustomization.barTintColor, customization.barTintColor) + + XCTAssertEqual(UIBarStyle.black, stdsCustomization.barStyle) + XCTAssertEqual(stdsCustomization.barStyle, customization.barStyle) + + XCTAssertEqual(false, stdsCustomization.translucent) + XCTAssertEqual(stdsCustomization.translucent, customization.translucent) + + XCTAssertEqual("foo", stdsCustomization.headerText) + XCTAssertEqual(stdsCustomization.headerText, customization.headerText) + + XCTAssertEqual("bar", stdsCustomization.buttonText) + XCTAssertEqual(stdsCustomization.buttonText, customization.buttonText) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSSelectionCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSSelectionCustomizationTest.swift new file mode 100644 index 00000000..344f04e1 --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSSelectionCustomizationTest.swift @@ -0,0 +1,42 @@ +// +// STPThreeDSSelectionCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/18/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSSelectionCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSSelectionCustomization.defaultSettings() + customization.primarySelectedColor = UIColor.red + customization.secondarySelectedColor = UIColor.blue + customization.unselectedBorderColor = UIColor.brown + customization.unselectedBackgroundColor = UIColor.cyan + + let stdsCustomization = customization.selectionCustomization + XCTAssertEqual(UIColor.red, stdsCustomization.primarySelectedColor) + XCTAssertEqual(stdsCustomization.primarySelectedColor, customization.primarySelectedColor) + + XCTAssertEqual(UIColor.blue, stdsCustomization.secondarySelectedColor) + XCTAssertEqual( + stdsCustomization.secondarySelectedColor, + customization.secondarySelectedColor + ) + + XCTAssertEqual(UIColor.brown, stdsCustomization.unselectedBorderColor) + XCTAssertEqual(stdsCustomization.unselectedBorderColor, customization.unselectedBorderColor) + + XCTAssertEqual(UIColor.cyan, stdsCustomization.unselectedBackgroundColor) + XCTAssertEqual( + stdsCustomization.unselectedBackgroundColor, + customization.unselectedBackgroundColor + ) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSTextFieldCustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSTextFieldCustomizationTest.swift new file mode 100644 index 00000000..2bc87ac9 --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSTextFieldCustomizationTest.swift @@ -0,0 +1,48 @@ +// +// STPThreeDSTextFieldCustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/18/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSTextFieldCustomizationTest: XCTestCase { + func testPropertiesAreForwarded() { + let customization = STPThreeDSTextFieldCustomization.defaultSettings() + customization.font = UIFont.italicSystemFont(ofSize: 1) + customization.textColor = UIColor.blue + customization.borderWidth = -1 + customization.borderColor = UIColor.red + customization.cornerRadius = -8 + customization.keyboardAppearance = .alert + customization.placeholderTextColor = UIColor.green + + let stdsCustomization = customization.textFieldCustomization + XCTAssertEqual(UIFont.italicSystemFont(ofSize: 1), stdsCustomization.font) + XCTAssertEqual(stdsCustomization.font, customization.font) + + XCTAssertEqual(UIColor.blue, stdsCustomization.textColor) + XCTAssertEqual(stdsCustomization.textColor, customization.textColor) + + XCTAssertEqual(-1, stdsCustomization.borderWidth, accuracy: 0.1) + XCTAssertEqual(stdsCustomization.borderWidth, customization.borderWidth, accuracy: 0.1) + + XCTAssertEqual(UIColor.red, stdsCustomization.borderColor) + XCTAssertEqual(stdsCustomization.borderColor, customization.borderColor) + + XCTAssertEqual(-8, stdsCustomization.cornerRadius, accuracy: 0.1) + XCTAssertEqual(stdsCustomization.cornerRadius, customization.cornerRadius, accuracy: 0.1) + + XCTAssertEqual(UIKeyboardAppearance.alert, stdsCustomization.keyboardAppearance) + XCTAssertEqual(stdsCustomization.keyboardAppearance, customization.keyboardAppearance) + + XCTAssertEqual(UIColor.green, stdsCustomization.placeholderTextColor) + XCTAssertEqual(stdsCustomization.placeholderTextColor, customization.placeholderTextColor) + } +} diff --git a/Stripe/StripeiOSTests/STPThreeDSUICustomizationTest.swift b/Stripe/StripeiOSTests/STPThreeDSUICustomizationTest.swift new file mode 100644 index 00000000..93968d8e --- /dev/null +++ b/Stripe/StripeiOSTests/STPThreeDSUICustomizationTest.swift @@ -0,0 +1,137 @@ +// +// STPThreeDSUICustomizationTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 6/17/19. +// Copyright © 2019 Stripe, Inc. All rights reserved. +// + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPThreeDSUICustomizationTest: XCTestCase { + func testPropertiesPassedThrough() { + let customization = STPThreeDSUICustomization.defaultSettings() + + // Maintains button customization objects + customization.buttonCustomization(for: .next).backgroundColor = UIColor.cyan + customization.buttonCustomization(for: .resend).backgroundColor = UIColor.cyan + customization.buttonCustomization(for: .submit).backgroundColor = UIColor.cyan + customization.buttonCustomization(for: .continue).backgroundColor = UIColor.cyan + customization.buttonCustomization(for: .cancel).backgroundColor = UIColor.cyan + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .next).backgroundColor, + UIColor.cyan + ) + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .resend).backgroundColor, + UIColor.cyan + ) + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .submit).backgroundColor, + UIColor.cyan + ) + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .continue).backgroundColor, + UIColor.cyan + ) + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .cancel).backgroundColor, + UIColor.cyan + ) + + let buttonCustomization = STPThreeDSButtonCustomization.defaultSettings(for: .next) + customization.setButtonCustomization(buttonCustomization, for: .next) + XCTAssertEqual( + customization.uiCustomization.buttonCustomization(for: .next), + buttonCustomization.buttonCustomization + ) + + // Footer + customization.footerCustomization.backgroundColor = UIColor.cyan + XCTAssertEqual( + customization.uiCustomization.footerCustomization.backgroundColor, + UIColor.cyan + ) + + let footerCustomization = STPThreeDSFooterCustomization.defaultSettings() + customization.footerCustomization = footerCustomization + XCTAssertEqual( + customization.uiCustomization.footerCustomization, + footerCustomization.footerCustomization + ) + + // Label + customization.labelCustomization.textColor = UIColor.cyan + XCTAssertEqual(customization.uiCustomization.labelCustomization.textColor, UIColor.cyan) + + let labelCustomization = STPThreeDSLabelCustomization.defaultSettings() + customization.labelCustomization = labelCustomization + XCTAssertEqual( + customization.uiCustomization.labelCustomization, + labelCustomization.labelCustomization + ) + + // Navigation Bar + customization.navigationBarCustomization.textColor = UIColor.cyan + XCTAssertEqual( + customization.uiCustomization.navigationBarCustomization.textColor, + UIColor.cyan + ) + + let navigationBar = STPThreeDSNavigationBarCustomization.defaultSettings() + customization.navigationBarCustomization = navigationBar + XCTAssertEqual( + customization.uiCustomization.navigationBarCustomization, + navigationBar.navigationBarCustomization + ) + + // Selection + customization.selectionCustomization.primarySelectedColor = UIColor.cyan + XCTAssertEqual( + customization.uiCustomization.selectionCustomization.primarySelectedColor, + UIColor.cyan + ) + + let selection = STPThreeDSSelectionCustomization.defaultSettings() + customization.selectionCustomization = selection + XCTAssertEqual( + customization.uiCustomization.selectionCustomization, + selection.selectionCustomization + ) + + // Text Field + customization.textFieldCustomization.textColor = UIColor.cyan + XCTAssertEqual(customization.uiCustomization.textFieldCustomization.textColor, UIColor.cyan) + + let textField = STPThreeDSTextFieldCustomization.defaultSettings() + customization.textFieldCustomization = textField + XCTAssertEqual( + customization.uiCustomization.textFieldCustomization, + textField.textFieldCustomization + ) + + // Other + customization.backgroundColor = UIColor.red + customization.activityIndicatorViewStyle = UIActivityIndicatorView.Style.large + customization.blurStyle = UIBlurEffect.Style.dark + + XCTAssertEqual(UIColor.red, customization.backgroundColor) + XCTAssertEqual(customization.backgroundColor, customization.uiCustomization.backgroundColor) + + XCTAssertEqual( + UIActivityIndicatorView.Style.large, + customization.activityIndicatorViewStyle + ) + XCTAssertEqual( + customization.activityIndicatorViewStyle, + customization.uiCustomization.activityIndicatorViewStyle + ) + + XCTAssertEqual(UIBlurEffect.Style.dark, customization.blurStyle) + XCTAssertEqual(customization.blurStyle, customization.uiCustomization.blurStyle) + } +} diff --git a/Stripe/StripeiOSTests/STPTokenTest.swift b/Stripe/StripeiOSTests/STPTokenTest.swift new file mode 100644 index 00000000..e4c03570 --- /dev/null +++ b/Stripe/StripeiOSTests/STPTokenTest.swift @@ -0,0 +1,67 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPTokenTest.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/9/12. +// +// + +import Stripe +import XCTest + +class STPTokenTest: XCTestCase { + func buildTokenResponse() -> [AnyHashable: Any]? { + let cardDict = [ + "id": "card_123", + "exp_month": "12", + "exp_year": "2013", + "name": "Smerlock Smolmes", + "address_line1": "221A Baker Street", + "address_city": "New York", + "address_state": "NY", + "address_zip": "12345", + "address_country": "US", + "last4": "1234", + "brand": "Visa", + "fingerprint": "Fingolfin", + "country": "JP", + ] + + let tokenDict = [ + "id": "id_for_token", + "object": "token", + "livemode": NSNumber(value: false), + "created": NSNumber(value: 1353025450.0), + "used": NSNumber(value: false), + "card": cardDict, + "type": "card", + ] as [String: Any] + return tokenDict + } + + func testCreatingTokenWithAttributeDictionarySetsAttributes() { + guard + let token = STPToken.decodedObject(fromAPIResponse: buildTokenResponse()), + let timeInterval = token.created?.timeIntervalSince1970 + else { + XCTFail() + return + } + XCTAssertEqual(token.tokenId, "id_for_token") + XCTAssertEqual(token.livemode, false, "Generated token has the correct livemode") + XCTAssertEqual(token.type, STPTokenType.card, "Generated token has incorrect type") + + XCTAssertEqual(timeInterval, 1353025450.0, accuracy: 1.0, "Generated token has the correct created time") + } + + func testCreatingTokenSetsAdditionalResponseFields() { + var tokenResponse = buildTokenResponse() + tokenResponse?["foo"] = "bar" + let token = STPToken.decodedObject(fromAPIResponse: tokenResponse) + let allResponseFields = token?.allResponseFields + XCTAssertEqual(allResponseFields?["foo"] as? String, "bar") + XCTAssertEqual(allResponseFields?["livemode"] as? NSNumber, NSNumber(value: false)) + XCTAssertNil(allResponseFields?["baz"]) + } +} diff --git a/Stripe/StripeiOSTests/STPUIVCStripeParentViewControllerTests.swift b/Stripe/StripeiOSTests/STPUIVCStripeParentViewControllerTests.swift new file mode 100644 index 00000000..63f00064 --- /dev/null +++ b/Stripe/StripeiOSTests/STPUIVCStripeParentViewControllerTests.swift @@ -0,0 +1,39 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPUIVCStripeParentViewControllerTests.m +// Stripe +// +// Created by Jack Flintermann on 1/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +@testable import Stripe +import UIKit +import XCTest + +class TestViewController: UIViewController { +} + +class STPUIVCStripeParentViewControllerTests: XCTestCase { + func testNilParent() { + let vc = UIViewController() + XCTAssertNil(vc.stp_parentViewControllerOf(UIViewController.self)) + } + + func testNavigationController() { + let vc = UIViewController() + let nav = UINavigationController(rootViewController: vc) + let parent = vc.stp_parentViewControllerOf(UINavigationController.self) as? UINavigationController + XCTAssertEqual(nav, parent) + } + + func testDeepHeirarchy() { + let topLevel = TestViewController() + let vc = UIViewController() + let nav = UINavigationController(rootViewController: vc) + topLevel.addChild(nav) + nav.didMove(toParent: topLevel) + let parent = vc.stp_parentViewControllerOf(TestViewController.self) as? TestViewController + XCTAssertEqual(topLevel, parent) + } +} diff --git a/Stripe/StripeiOSTests/STPViewWithSeparatorSnapshotTests.swift b/Stripe/StripeiOSTests/STPViewWithSeparatorSnapshotTests.swift new file mode 100644 index 00000000..a329c80e --- /dev/null +++ b/Stripe/StripeiOSTests/STPViewWithSeparatorSnapshotTests.swift @@ -0,0 +1,28 @@ +// Converted to Swift 5.8.1 by Swiftify v5.8.28463 - https://swiftify.com/ +// +// STPViewWithSeparatorSnapshotTests.m +// StripeiOS Tests +// +// Created by Cameron Sabol on 3/13/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCaseCore +import StripeCoreTestUtils +@testable import StripePaymentsUI + +class STPViewWithSeparatorSnapshotTests: STPSnapshotTestCase { + + func testDefaultAppearance() { + let view = STPViewWithSeparator(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 44.0)) + view.backgroundColor = UIColor.white + STPSnapshotVerifyView(view, identifier: "STPViewWithSeparator.defaultAppearance") + } + + func testHiddenTopSeparator() { + let view = STPViewWithSeparator(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 44.0)) + view.backgroundColor = UIColor.white + view.topSeparatorHidden = true + STPSnapshotVerifyView(view, identifier: "STPViewWithSeparator.hiddenTopSeparator") + } +} diff --git a/Stripe/StripeiOSTests/ServerErrorMapperTest.swift b/Stripe/StripeiOSTests/ServerErrorMapperTest.swift new file mode 100644 index 00000000..8087ec8b --- /dev/null +++ b/Stripe/StripeiOSTests/ServerErrorMapperTest.swift @@ -0,0 +1,94 @@ +// +// ServerErrorMapperTest.swift +// StripeiOS Tests +// +// Created by Nick Porter on 9/13/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class ServerErrorMapperTest: XCTestCase { + + func testFromMissingPublishableKey() { + let serverErrorMessage = + "You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. \'Authorization: Bearer YOUR_SECRET_KEY\'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/." + + XCTAssertTrue( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: HTTPURLResponse() + )!.hasPrefix("No valid API key provided. Set `STPAPIClient.shared") + ) + } + + func testFromInvalidPublishableKey() { + let serverErrorMessage = "Invalid API Key provided: pk_test" + + XCTAssertTrue( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: HTTPURLResponse() + )!.hasPrefix("No valid API key provided. Set `STPAPIClient.shared") + ) + } + + func testFromInvalidCustEphKey() { + let serverErrorMessage = "Invalid API Key provided: badCustEphKey" + + let url = URL(string: "https://api.stripe.com/v1/payment_methods?customer=")! + let httpResponse = HTTPURLResponse( + url: url, + statusCode: 401, + httpVersion: nil, + headerFields: nil + ) + XCTAssertTrue( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: httpResponse + )!.hasPrefix("Invalid customer ephemeral key secret.") + ) + } + + func testFromNoSuchPaymentIntent() { + let serverErrorMessage = "No such payment_intent: pi_123" + + XCTAssertTrue( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: HTTPURLResponse() + )!.hasPrefix("No matching PaymentIntent could") + ) + } + + func testFromNoSuchSetupIntent() { + let serverErrorMessage = "No such setup_intent: si_123" + + XCTAssertTrue( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: HTTPURLResponse() + )!.hasPrefix("No matching SetupIntent could") + ) + } + + func testFromUnknownErrorMessage() { + let serverErrorMessage = + "This error message is not known to ServerErrorMapper, should return nil." + + XCTAssertNil( + ServerErrorMapper.mobileErrorMessage( + from: serverErrorMessage, + httpResponse: HTTPURLResponse() + ) + ) + } + +} diff --git a/Stripe/StripeiOSTests/StripeErrorTest.swift b/Stripe/StripeiOSTests/StripeErrorTest.swift new file mode 100644 index 00000000..7c42f17e --- /dev/null +++ b/Stripe/StripeiOSTests/StripeErrorTest.swift @@ -0,0 +1,273 @@ +// +// StripeErrorTest.swift +// StripeiOS Tests +// +// Created by Ben Guo on 4/14/17. +// Copyright © 2017 Stripe, Inc. All rights reserved. +// + +import Foundation +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class StripeErrorTest: XCTestCase { + func testEmptyResponse() { + let response: [AnyHashable: Any] = [:] + let error = NSError.stp_error(fromStripeResponse: response) + XCTAssertNil(error) + } + + func testResponseWithUnknownTypeAndNoMessage() { + let response = [ + "error": [ + "type": "foo", + "code": "error_code", + ], + ] + let error = NSError.stp_error(fromStripeResponse: response)! + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.apiError.rawValue) + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as! String, + NSError.stp_unexpectedErrorMessage() + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorTypeKey] as! String, + response["error"]!["type"]! + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorCodeKey] as! String, + response["error"]!["code"]! + ) + XCTAssertTrue( + (error.userInfo[STPError.errorMessageKey]! as! String).hasPrefix( + "Could not interpret the error response" + ) + ) + } + + func testAPIError() { + let response = [ + "error": [ + "type": "api_error", + "message": "some message", + ], + ] + let error = NSError.stp_error(fromStripeResponse: response)! + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.apiError.rawValue) + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as! String?, + NSError.stp_unexpectedErrorMessage() + ) + XCTAssertEqual( + error.userInfo[STPError.errorMessageKey] as! String?, + response["error"]!["message"] + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorTypeKey] as! String?, + response["error"]!["type"] + ) + } + + func testInvalidRequestErrorMissingParameter() { + let response = [ + "error": [ + "type": "invalid_request_error", + "message": "The payment method `card` requires the parameter: card[exp_year].", + "param": "card[exp_year]", + ], + ] + let error = NSError.stp_error(fromStripeResponse: response)! + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.invalidRequestError.rawValue) + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as? String, + NSError.stp_unexpectedErrorMessage() + ) + XCTAssertEqual( + error.userInfo[STPError.errorMessageKey] as? String, + response["error"]!["message"] + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorTypeKey] as? String, + response["error"]!["type"] + ) + XCTAssertEqual(error.userInfo[STPError.errorParameterKey] as! String, "card[expYear]") + } + + func testAuthenticationError() { + // Given an `invalid_request_error` response + let response = [ + "error": [ + "type": "invalid_request_error", + "message": "Invalid API Key provided: pk_test_***************************00", + ], + ] + + // with a `401` HTTP status code + let httpResponse = HTTPURLResponse( + url: URL(string: "https://api.stripe.com/v1/payment_intents")!, + statusCode: 401, + httpVersion: "1.1", + headerFields: nil + ) + + let error = NSError.stp_error(fromStripeResponse: response, httpResponse: httpResponse)! + + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual( + error.code, + STPErrorCode.authenticationError.rawValue, + "`error.code` should be equals to `STPErrorCode.authenticationError`" + ) + } + + func testAuthenticationErrorDueToExpiredKey() { + // Given an `invalid_request_error` response due to an expired key + let response = [ + "error": [ + "code": "api_key_expired", + "type": "invalid_request_error", + "message": "Expired API Key provided: pk_test_***************************00", + ], + ] + + // with a `401` HTTP status code + let httpResponse = HTTPURLResponse( + url: URL(string: "https://api.stripe.com/v1/payment_intents")!, + statusCode: 401, + httpVersion: "1.1", + headerFields: nil + ) + + let error = NSError.stp_error(fromStripeResponse: response, httpResponse: httpResponse)! + + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual( + error.code, + STPErrorCode.authenticationError.rawValue, + "`error.code` should be equals to `STPErrorCode.authenticationError`" + ) + } + + func testInvalidRequestErrorIncorrectNumber() { + let response = [ + "error": [ + "type": "invalid_request_error", + "message": "Your card number is incorrect.", + "code": "incorrect_number", + ], + ] + let error = NSError.stp_error(fromStripeResponse: response)! + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.invalidRequestError.rawValue) + // Error type is not `card_error`, so `NSLocalizedDescription` will be a generic error. + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as! String, + NSError.stp_unexpectedErrorMessage() + ) + XCTAssertEqual( + error.userInfo[STPError.cardErrorCodeKey] as! String, + STPError.incorrectNumber + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorTypeKey] as? String, + response["error"]!["type"] + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorCodeKey] as? String, + response["error"]!["code"] + ) + XCTAssertEqual( + error.userInfo[STPError.errorMessageKey] as? String, + response["error"]!["message"] + ) + } + + func testCardErrorIncorrectNumber() { + let response = [ + "error": [ + "type": "card_error", + "message": "Your card number is incorrect.", + "code": "incorrect_number", + ], + ] + let error = NSError.stp_error(fromStripeResponse: response)! + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.cardError.rawValue) + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as! String, + "Your card number is incorrect." + ) + XCTAssertEqual( + error.userInfo[STPError.cardErrorCodeKey] as! String, + STPError.incorrectNumber + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorTypeKey] as? String, + response["error"]!["type"] + ) + XCTAssertEqual( + error.userInfo[STPError.stripeErrorCodeKey] as? String, + response["error"]!["code"] + ) + XCTAssertEqual( + error.userInfo[STPError.errorMessageKey] as? String, + response["error"]!["message"] + ) + } + + func testCardDeclinedError() { + let response = [ + "error": [ + "type": "card_error", + "code": "card_declined", + "decline_code": "insufficient_funds", + ], + ] + guard let error = NSError.stp_error(fromStripeResponse: response) else { + XCTFail() + return + } + XCTAssertEqual(error.domain, STPError.stripeDomain) + XCTAssertEqual(error.code, STPErrorCode.cardError.rawValue) + XCTAssertEqual( + error.userInfo[STPError.cardErrorCodeKey] as? String, + STPCardErrorCode.cardDeclined.rawValue + ) + // Response didn't include a message, so a built in message will be used. + XCTAssertEqual( + error.userInfo[NSLocalizedDescriptionKey] as? String, + NSError.stp_cardErrorDeclinedUserMessage() + ) + XCTAssertEqual( + error.userInfo[STPError.stripeDeclineCodeKey] as? String, + "insufficient_funds" + ) + } + + func testErrorAddsRequestIdToUserInfo() { + let response = [ + "error": [ + "type": "card_error", + "code": "card_declined", + "decline_code": "insufficient_funds", + ], + ] + let httpURLResponse = HTTPURLResponse(url: URL(string: "https://api.stripe.com/v1/some_endpoint")!, statusCode: 400, httpVersion: nil, headerFields: ["request-id": "req_123"]) + guard let error = NSError.stp_error(fromStripeResponse: response, httpResponse: httpURLResponse) else { + XCTFail() + return + } + XCTAssertEqual( + error.userInfo[STPError.stripeRequestIDKey] as? String, + "req_123" + ) + } +} diff --git a/Stripe/StripeiOSTests/StripeTests-Prefix.pch b/Stripe/StripeiOSTests/StripeTests-Prefix.pch new file mode 100644 index 00000000..0540b07f --- /dev/null +++ b/Stripe/StripeiOSTests/StripeTests-Prefix.pch @@ -0,0 +1,21 @@ +// +// StripeTests-Prefix.pch +// StripeiOS Tests +// +// Created by David Estes on 10/8/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +#ifndef StripeTests_Prefix_pch +#define StripeTests_Prefix_pch + +// Include any system framework and library headers here that should be included in all compilation units. +// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. + +#import "STPBlocks.h" +@import StripeApplePay; +@import Stripe; +@import StripePayments; +@import StripePaymentsUI; + +#endif /* StripeTests_Prefix_pch */ diff --git a/Stripe/StripeiOSTests/StripeiOS Tests-Bridging-Header.h b/Stripe/StripeiOSTests/StripeiOS Tests-Bridging-Header.h new file mode 100644 index 00000000..8a95b39a --- /dev/null +++ b/Stripe/StripeiOSTests/StripeiOS Tests-Bridging-Header.h @@ -0,0 +1,7 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +@import StripePaymentsObjcTestUtils; + +#import "STPMocks.h" diff --git a/Stripe/StripeiOSTests/TextFieldElement+IBANTest.swift b/Stripe/StripeiOSTests/TextFieldElement+IBANTest.swift new file mode 100644 index 00000000..01b65d5c --- /dev/null +++ b/Stripe/StripeiOSTests/TextFieldElement+IBANTest.swift @@ -0,0 +1,140 @@ +// +// TextFieldElement+IBANTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/23/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI +@testable@_spi(STP) import StripeUICore + +class TextFieldElementIBANTest: XCTestCase { + typealias IBANError = TextFieldElement.IBANError + typealias Error = TextFieldElement.Error + + func testValidation() throws { + let testcases: [String: TextFieldElement.ValidationState] = [ + "": .invalid(Error.empty), + "G": .invalid(IBANError.incomplete), + "GB": .invalid(IBANError.incomplete), + "GB1": .invalid(IBANError.incomplete), + "GB12": .invalid(IBANError.incomplete), + + "1": .invalid(IBANError.shouldStartWithCountryCode), + "12": .invalid(IBANError.shouldStartWithCountryCode), + "Z1": .invalid(IBANError.shouldStartWithCountryCode), + "🤦🏻🇺🇸": .invalid(IBANError.shouldStartWithCountryCode), + + "ZZ": .invalid(IBANError.invalidCountryCode(countryCode: "ZZ")), + + "GB82WEST12345698765432🇺🇸": .invalid(IBANError.invalidFormat), + // https://www.iban.com/testibans + "GB94BARC20201530093459": .invalid(IBANError.invalidFormat), + + "GB33BUKB20201555555555": .valid, + "GB94BARC10201530093459": .valid, + "SK6902000000001933504555": .valid, + "BG09STSA93000021741508": .valid, + "FR1420041010050500013M02606": .valid, + "AT611904300234573201": .valid, + "AT861904300235473202": .valid, + ] + + let config = TextFieldElement.IBANConfiguration(defaultValue: nil) + for (text, expected) in testcases { + let actual = config.validate(text: text, isOptional: false) + XCTAssertTrue( + actual == expected, + "Input \"\(text)\": expected \(expected) but got \(actual)" + ) + } + } + + func testValidateCountryCode() { + let testcases: [String: TextFieldElement.ValidationState] = [ + "": .invalid(IBANError.incomplete), + "A": .invalid(IBANError.incomplete), + "D": .invalid(IBANError.incomplete), + + "ū": .invalid(IBANError.shouldStartWithCountryCode), + "1": .invalid(IBANError.shouldStartWithCountryCode), + ".": .invalid(IBANError.shouldStartWithCountryCode), + + "AT": .valid, + "DE": .valid, + ] + for (test, expected) in testcases { + let actual = TextFieldElement.IBANConfiguration.validateCountryCode(test) + XCTAssertTrue(actual == expected) + } + } + + func testTransformToASCIIDigits() { + let testcases: [String: String] = [ + "": "", + "1234": "1234", + "GB82": "161182", + "AAAA": "10101010", + "ZZZZ": "35353535", + ] + for (test, expected) in testcases { + let actual = TextFieldElement.IBANConfiguration.transformToASCIIDigits(test) + XCTAssertTrue(actual == expected) + } + } + + func testMod97() { + let testcases: [String: Int?] = [ + "0": 0, + "97": 0, + "96": 96, + "00001": 1, + "13985713857180375018375081735081735": 15, + "13985713857180375018375081735081720": 0, + ] + for (test, expected) in testcases { + let actual = TextFieldElement.IBANConfiguration.mod97(test) + XCTAssertTrue(actual == expected) + } + + for _ in 0...100 { + let test = Int.random(in: 0...Int.max) + let actual = TextFieldElement.IBANConfiguration.mod97(String(test)) + XCTAssertTrue(actual == test % 97) + } + } +} + +// MARK: - Helpers + +// TODO(mludowise): These should get migrated to a shared StripeUICoreTestUtils target + +extension TextFieldElement.ValidationState: Equatable { + public static func == ( + lhs: TextFieldElement.ValidationState, + rhs: TextFieldElement.ValidationState + ) -> Bool { + switch (lhs, rhs) { + case (.valid, .valid): + return true + case (.invalid(let lhsError), .invalid(let rhsError)): + return lhsError == rhsError + default: + return false + } + } +} + +func == (lhs: TextFieldValidationError, rhs: TextFieldValidationError) -> Bool { + guard String(describing: lhs) == String(describing: rhs) else { + return false + } + return (lhs as NSError).isEqual(rhs as NSError) +} diff --git a/Stripe/StripeiOSTests/UINavigationBar+StripeTest.m b/Stripe/StripeiOSTests/UINavigationBar+StripeTest.m new file mode 100644 index 00000000..bc5a2bd8 --- /dev/null +++ b/Stripe/StripeiOSTests/UINavigationBar+StripeTest.m @@ -0,0 +1,52 @@ +// +// UINavigationBar+StripeTest.m +// Stripe +// +// Created by Brian Dorfman on 12/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +@import Stripe; +#import "STPMocks.h" +@import StripePaymentsObjcTestUtils; + +@interface UINavigationBar_StripeTest : XCTestCase + +@end + +@implementation UINavigationBar_StripeTest + +- (STPPaymentOptionsViewController *)buildPaymentOptionsViewController { + id customerContext = [STPMocks staticCustomerContext]; + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + STPTheme *theme = [STPTheme defaultTheme]; + id delegate = OCMProtocolMock(@protocol(STPPaymentOptionsViewControllerDelegate)); + STPPaymentOptionsViewController *paymentOptionsVC = [[STPPaymentOptionsViewController alloc] initWithConfiguration:config + theme:theme + customerContext:customerContext + delegate:delegate]; + return paymentOptionsVC; +} + +- (void)testVCUsesNavigationBarColor { + STPPaymentOptionsViewController *paymentOptionsVC = [self buildPaymentOptionsViewController]; + STPTheme *navTheme = [STPTheme new]; + navTheme.accentColor = [UIColor purpleColor]; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:paymentOptionsVC]; + navController.navigationBar.stp_theme = navTheme; + __unused UIView *view = paymentOptionsVC.view; + XCTAssertEqualObjects(paymentOptionsVC.navigationItem.leftBarButtonItem.tintColor, [UIColor purpleColor]); +} + +- (void)testVCDoesNotUseNavigationBarColor { + STPPaymentOptionsViewController *paymentOptionsVC = [self buildPaymentOptionsViewController]; + __unused UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:paymentOptionsVC]; + __unused UIView *view = paymentOptionsVC.view; + XCTAssertEqualObjects(paymentOptionsVC.navigationItem.leftBarButtonItem.tintColor, [STPTheme defaultTheme].accentColor); +} + + +@end diff --git a/Stripe/StripeiOSTests/UserDefaults+StripeTest.swift b/Stripe/StripeiOSTests/UserDefaults+StripeTest.swift new file mode 100644 index 00000000..4803d5df --- /dev/null +++ b/Stripe/StripeiOSTests/UserDefaults+StripeTest.swift @@ -0,0 +1,33 @@ +// +// UserDefaults+StripeTest.swift +// StripeiOS Tests +// +// Created by Yuki Tokuhiro on 5/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentsUI + +class UserDefaults_StripeTest: XCTestCase { + func testFraudDetectionData() throws { + let fraudDetectionData = FraudDetectionData( + sid: UUID().uuidString, + muid: UUID().uuidString, + guid: UUID().uuidString, + sidCreationDate: Date() + ) + UserDefaults.standard.fraudDetectionData = fraudDetectionData + XCTAssertEqual(UserDefaults.standard.fraudDetectionData, fraudDetectionData) + } + + func testCustomerToLastSelectedPaymentMethod() throws { + let c = [UUID().uuidString: UUID().uuidString] + UserDefaults.standard.customerToLastSelectedPaymentMethod = c + XCTAssertEqual(UserDefaults.standard.customerToLastSelectedPaymentMethod, c) + } +} diff --git a/Stripe/StripeiOSTests/WalletHeaderViewSnapshotTests.swift b/Stripe/StripeiOSTests/WalletHeaderViewSnapshotTests.swift new file mode 100644 index 00000000..c58c98e5 --- /dev/null +++ b/Stripe/StripeiOSTests/WalletHeaderViewSnapshotTests.swift @@ -0,0 +1,185 @@ +// +// WalletHeaderViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 12/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +import UIKit + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePaymentSheet + +class WalletHeaderViewSnapshotTests: STPSnapshotTestCase { + + func testApplePayButton() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: .applePay, + delegate: nil + ) + verify(headerView) + } + + func testApplePayButtonWithCustomCta() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: .applePay, + applePayButtonType: .buy, + delegate: nil + ) + verify(headerView) + } + + func testLinkButton() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: .link, + delegate: nil + ) + verify(headerView) + } + + // Tests UI elements that adapt their color based on the `PaymentSheet.Appearance` + func testAdaptiveElements() { + var darkMode = false + + var appearance = PaymentSheet.Appearance() + appearance.colors.background = UIColor.init(dynamicProvider: { _ in + if darkMode { + return .black + } + + return .white + }) + + appearance.cornerRadius = 0 + let headerView = PaymentSheetViewController.WalletHeaderView( + options: .applePay, + appearance: appearance, + delegate: nil + ) + + verify(headerView, identifier: "Light") + + darkMode = true + headerView.traitCollectionDidChange(nil) + + verify(headerView, identifier: "Dark") + } + + // Tests UI elements that adapt their color based on the `PaymentSheet.Appearance` + func testAdaptiveElementsWithCustomApplePayCta() { + var darkMode = false + + var appearance = PaymentSheet.Appearance() + appearance.colors.background = UIColor.init(dynamicProvider: { _ in + if darkMode { + return .black + } + + return .white + }) + + appearance.cornerRadius = 0 + let headerView = PaymentSheetViewController.WalletHeaderView( + options: .applePay, + appearance: appearance, + applePayButtonType: .buy, + delegate: nil + ) + + verify(headerView, identifier: "Light") + + darkMode = true + headerView.traitCollectionDidChange(nil) + + verify(headerView, identifier: "Dark") + } + + func testAllButtons() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + delegate: nil + ) + verify(headerView) + + headerView.showsCardPaymentMessage = true + verify(headerView, identifier: "Card only") + } + + func testAllButtonsWithCustomApplePayCta() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + applePayButtonType: .buy, + delegate: nil + ) + verify(headerView) + + headerView.showsCardPaymentMessage = true + verify(headerView, identifier: "Card only") + } + + func testCustomFont() throws { + var appearance = PaymentSheet.Appearance.default + appearance.font.base = try XCTUnwrap(UIFont(name: "AmericanTypewriter", size: 12.0)) + + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + appearance: appearance, + delegate: nil + ) + + verify(headerView) + } + + func testCustomFontScales() throws { + var appearance = PaymentSheet.Appearance.default + appearance.font.base = try XCTUnwrap(UIFont(name: "AmericanTypewriter", size: 12.0)) + appearance.font.sizeScaleFactor = 1.25 + + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + appearance: appearance, + delegate: nil + ) + + verify(headerView) + } + + func testCustomCornerRadius() { + var appearance = PaymentSheet.Appearance.default + appearance.cornerRadius = 14.5 + + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + appearance: appearance, + delegate: nil + ) + + verify(headerView) + } + + func testAllButtonsSetupIntent() { + let headerView = PaymentSheetViewController.WalletHeaderView( + options: [.applePay, .link], + isPaymentIntent: false, + delegate: nil + ) + verify(headerView) + + headerView.showsCardPaymentMessage = true + verify(headerView, identifier: "Card only") + } + + func verify( + _ view: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + view.autosizeHeight(width: 300) + STPSnapshotVerifyView(view, identifier: identifier, file: file, line: line) + } +} diff --git a/Stripe3DS2/BuildConfigurations/Project-Debug.xcconfig b/Stripe3DS2/BuildConfigurations/Project-Debug.xcconfig new file mode 100644 index 00000000..039738ae --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Project-Debug.xcconfig @@ -0,0 +1,15 @@ +// +// Project-Debug.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Project-Shared.xcconfig" + +ENABLE_TESTABILITY = YES +GCC_DYNAMIC_NO_PIC = NO +GCC_OPTIMIZATION_LEVEL = 0 +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) +MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE +ONLY_ACTIVE_ARCH = YES \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Project-Release.xcconfig b/Stripe3DS2/BuildConfigurations/Project-Release.xcconfig new file mode 100644 index 00000000..e2ec59ca --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Project-Release.xcconfig @@ -0,0 +1,12 @@ +// +// Project-Release.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Project-Shared.xcconfig" + +ENABLE_NS_ASSERTIONS = NO +MTL_ENABLE_DEBUG_INFO = NO +VALIDATE_PRODUCT = YES \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Project-Shared.xcconfig b/Stripe3DS2/BuildConfigurations/Project-Shared.xcconfig new file mode 100644 index 00000000..cca99b2c --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Project-Shared.xcconfig @@ -0,0 +1,63 @@ +// +// Project-Shared.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +ALWAYS_SEARCH_USER_PATHS = NO +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +CLANG_CXX_LIBRARY = libc++ +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +CLANG_ENABLE_OBJC_WEAK = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_ASSIGN_ENUM = YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_OBJC_INTERFACE_IVARS = YES +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN_UNREACHABLE_CODE = YES +COPY_PHASE_STRIP = NO +CURRENT_PROJECT_VERSION = 1.0 +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +ENABLE_STRICT_OBJC_MSGSEND = YES +GCC_C_LANGUAGE_STANDARD = gnu11 +GCC_NO_COMMON_BLOCKS = YES +GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES +GCC_TREAT_WARNINGS_AS_ERRORS = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_SIGN_COMPARE = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES +IPHONEOS_DEPLOYMENT_TARGET = 13.0 +MTL_FAST_MATH = YES +SDKROOT = iphoneos +SWIFT_VERSION=5.0 +VERSION_INFO_PREFIX = +VERSIONING_SYSTEM = apple-generic diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2-Debug.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Debug.xcconfig new file mode 100644 index 00000000..34a46e79 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Debug.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2-Debug.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2-Release.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Release.xcconfig new file mode 100644 index 00000000..1885c226 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Release.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2-Release.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2-Shared.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Shared.xcconfig new file mode 100644 index 00000000..02d8fe27 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2-Shared.xcconfig @@ -0,0 +1,23 @@ +// +// Stripe3DS2-Shared.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +APPLICATION_EXTENSION_API_ONLY = YES +BUILD_LIBRARY_FOR_DISTRIBUTION = YES +CODE_SIGN_STYLE = Automatic +DEFINES_MODULE = YES +DEPLOYMENT_POSTPROCESSING = YES +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +DYLIB_INSTALL_NAME_BASE = @rpath +GCC_PREFIX_HEADER = $(SRCROOT)/Stripe3DS2/include/Stripe3DS2-Prefix.pch +INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks +IPHONEOS_DEPLOYMENT_TARGET = 13.0 +OTHER_LDFLAGS = +SKIP_INSTALL = YES +STRIP_STYLE = non-global +TARGETED_DEVICE_FAMILY = 1,2 +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Debug.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Debug.xcconfig new file mode 100644 index 00000000..bd71fac1 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Debug.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2DemoUI-Debug.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2DemoUI-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Release.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Release.xcconfig new file mode 100644 index 00000000..baf74545 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Release.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2DemoUI-Release.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2DemoUI-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Shared.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Shared.xcconfig new file mode 100644 index 00000000..fb8baac4 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUI-Shared.xcconfig @@ -0,0 +1,13 @@ +// +// Stripe3DS2DemoUI-Shared.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon +CODE_SIGN_STYLE = Automatic +IPHONEOS_DEPLOYMENT_TARGET = 13.0 +TARGETED_DEVICE_FAMILY = 1,2 +DYLIB_INSTALL_NAME_BASE = @rpath +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Debug.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Debug.xcconfig new file mode 100644 index 00000000..53c3f9ff --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Debug.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2DemoUITests-Debug.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2DemoUITests-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Release.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Release.xcconfig new file mode 100644 index 00000000..7251a5cc --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Release.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2DemoUITests-Release.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2DemoUITests-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Shared.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Shared.xcconfig new file mode 100644 index 00000000..e520a8af --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2DemoUITests-Shared.xcconfig @@ -0,0 +1,13 @@ +// +// Stripe3DS2DemoUITests-Shared.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +BUNDLE_LOADER = $(TEST_HOST) +CODE_SIGN_STYLE = Automatic +TARGETED_DEVICE_FAMILY = 1,2 +TEST_HOST = $(BUILT_PRODUCTS_DIR)/Stripe3DS2DemoUI.app/Stripe3DS2DemoUI +DYLIB_INSTALL_NAME_BASE = @rpath +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Debug.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Debug.xcconfig new file mode 100644 index 00000000..1744b025 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Debug.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2Tests-Debug.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2Tests-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Release.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Release.xcconfig new file mode 100644 index 00000000..f03b2ac4 --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Release.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2Tests-Release.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +#include "Stripe3DS2Tests-Shared.xcconfig" + +//********************************************// +//* Currently no build settings in this file *// +//********************************************// \ No newline at end of file diff --git a/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Shared.xcconfig b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Shared.xcconfig new file mode 100644 index 00000000..2e597a2e --- /dev/null +++ b/Stripe3DS2/BuildConfigurations/Stripe3DS2Tests-Shared.xcconfig @@ -0,0 +1,12 @@ +// +// Stripe3DS2Tests-Shared.xcconfig +// +// Generated by BuildSettingExtractor on 12/5/22 +// https://buildsettingextractor.com +// + +CODE_SIGN_STYLE = Automatic +OTHER_LDFLAGS = -ObjC +TARGETED_DEVICE_FAMILY = 1,2 +DYLIB_INSTALL_NAME_BASE = @rpath +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks diff --git a/Stripe3DS2/Stripe3DS2.xcodeproj/project.pbxproj b/Stripe3DS2/Stripe3DS2.xcodeproj/project.pbxproj new file mode 100644 index 00000000..5f91c512 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2.xcodeproj/project.pbxproj @@ -0,0 +1,1750 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 01706B4660728A5BFAC12840 /* STDSRuntimeException.m in Sources */ = {isa = PBXBuildFile; fileRef = CD33421F13A675DCFE597FFB /* STDSRuntimeException.m */; }; + 05647ABCFBDBE1209D5044AC /* STDSEphemeralKeyPair.m in Sources */ = {isa = PBXBuildFile; fileRef = F9D521B45783D36C8E83F0EA /* STDSEphemeralKeyPair.m */; }; + 0615DD02C0B022AE207DADF0 /* STDSUICustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = 210B22FF4DCB0C6E7C763EAB /* STDSUICustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 081347606D48F64161D95B45 /* mastercard.der in Resources */ = {isa = PBXBuildFile; fileRef = 1104D5855D28E49E104CA004 /* mastercard.der */; }; + 089DAFECA444861762781974 /* NSError+Stripe3DS2.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C20D4B77372845627D6466 /* NSError+Stripe3DS2.m */; }; + 08ECC7E70E7478E76793ED1E /* UIViewController+Stripe3DS2.m in Sources */ = {isa = PBXBuildFile; fileRef = 99451064136843047C7881AD /* UIViewController+Stripe3DS2.m */; }; + 09DFAF7B38EAB76BC66AC9C8 /* STDSChallengeResponseObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 24CE086326AF3DE336BF4F4C /* STDSChallengeResponseObject.m */; }; + 09F0B1945CC12FB1215119FD /* STDSProcessingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 95641ECBE1AE1CC198013405 /* STDSProcessingView.h */; }; + 0A2BC6A9E388B242C46C09D9 /* UIButton+CustomInitialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD55BECC3FB85216FE46218 /* UIButton+CustomInitialization.h */; }; + 0BA0F0CADC4CFF419E1F7EFC /* STDSLabelCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = 6932DD26C7C16938DFA0E198 /* STDSLabelCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0D3D9C95CB14A610EAAAEF6E /* STDSJSONEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0681C043F732148587737233 /* STDSJSONEncoder.m */; }; + 0D4B42EC5019D213BDBC84E7 /* NSDictionary+DecodingHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = C566D6E444FCA4BCEC65F3D2 /* NSDictionary+DecodingHelpers.m */; }; + 0EEFDE04392FD017EA0A017A /* STDSDeviceInformationParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 25C7D18868DDADEF4CB6220C /* STDSDeviceInformationParameter.m */; }; + 1134F81C7D015BA5EA52BFD1 /* NSDictionary+DecodingHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = C78AA1D7AD2BAB52EE58D078 /* NSDictionary+DecodingHelpers.h */; }; + 1156B25EBE0135B627E174D2 /* STDSProgressViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84967ED0D9B206B3F5AA4F02 /* STDSProgressViewController.m */; }; + 125427F1322501AA12EAEBE6 /* visa.der in Resources */ = {isa = PBXBuildFile; fileRef = A70071543985284E0D9DAC64 /* visa.der */; }; + 128B64380D6724F016EE8D56 /* STDSDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 382C583385FAECA91366769E /* STDSDemoViewController.m */; }; + 136255493F3F0E930FBA8606 /* STDSImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = A09A89BBE7360F93195641C9 /* STDSImageLoader.m */; }; + 1469551DB874336B68F6C39D /* Stripe3DS2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */; }; + 149FE0C2910F08910B250BD8 /* STDSException+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 461F938CDE7ED6D06D9D2700 /* STDSException+Internal.h */; }; + 14BDBD98F8E15576403255C9 /* STDSChallengeInformationView.m in Sources */ = {isa = PBXBuildFile; fileRef = D24777EE9931075405F370DB /* STDSChallengeInformationView.m */; }; + 15D4A2F07EA477B84CD48B15 /* STDSRuntimeErrorEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7D85369E012B15702D8DF9 /* STDSRuntimeErrorEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1767509FDC216602A5E43F0E /* STDSErrorMessageTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 278F2C40987F5218BD031E3D /* STDSErrorMessageTest.m */; }; + 1B20B454D0C309613A1FAF68 /* STDSThreeDSProtocolVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AC648332A706809F1BDFD59 /* STDSThreeDSProtocolVersion.m */; }; + 206B93BDE91CFED74A053B87 /* STDSStackView.h in Headers */ = {isa = PBXBuildFile; fileRef = B7AE3B4732D203134FE096FE /* STDSStackView.h */; }; + 2086DFD1FC02783FB106AD35 /* UIColor+DefaultColors.m in Sources */ = {isa = PBXBuildFile; fileRef = 869733153BCA6BE2EB7A452F /* UIColor+DefaultColors.m */; }; + 2224463CE5FB99D4BEE6A479 /* STDSJSONEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 06C5207432FB07BD71703BCC /* STDSJSONEncoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24D49F833E1ED28DE557F219 /* STDSException.h in Headers */ = {isa = PBXBuildFile; fileRef = B71A1C110DCC23A9CE929837 /* STDSException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24F626CD0F93B7AF59B1D6AA /* STDSThreeDS2Service.m in Sources */ = {isa = PBXBuildFile; fileRef = 77646CEC7EE754F47EDBDA4F /* STDSThreeDS2Service.m */; }; + 27F3EA1BB7E7B56A1A8524BA /* UIColor+ThirteenSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A8B0001705400C661678617 /* UIColor+ThirteenSupport.h */; }; + 284F72D3FD8C06C0E5974086 /* STDSTextFieldCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = 97163B5E9598CA3A298920B9 /* STDSTextFieldCustomization.m */; }; + 28AD7EC6F6DA2AECD7F61BA7 /* NSLayoutConstraint+LayoutSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = E0949399E96663964091C0EF /* NSLayoutConstraint+LayoutSupport.h */; }; + 2938D9CC41899DC78D01EF9F /* STDSDeviceInformationParameter.h in Headers */ = {isa = PBXBuildFile; fileRef = 172AF5C85EDBD92A1030E361 /* STDSDeviceInformationParameter.h */; }; + 2970D4880EBFAEFABA2E8EBD /* STDSChallengeRequestParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 60693BC560C4927ACFD2E6C3 /* STDSChallengeRequestParameters.m */; }; + 2AB06EC20B22306F1DDF9F4B /* STDSChallengeResponseImageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A9F828840A3B361842E38DE /* STDSChallengeResponseImageObject.m */; }; + 2B09B7FDF8C4AA551F7EE18E /* STDSDeviceInformationParameterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FC52C3C844BDA981EE34BAB9 /* STDSDeviceInformationParameterTests.m */; }; + 2B2787A7103C0D0AB5F00B78 /* STDSAuthenticationResponseObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 138E82072FC6572612A8E248 /* STDSAuthenticationResponseObject.h */; }; + 2BBA82878804711E06CBFCCB /* STDSErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 645D0BB927B7F8A0D37EE04D /* STDSErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2C5621AABECF6192F92A20CF /* STDSStripe3DS2Error.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F4376BB07FD1EE783249AED /* STDSStripe3DS2Error.m */; }; + 2C6DB6516699B4EFADDF3360 /* STDSRuntimeException.h in Headers */ = {isa = PBXBuildFile; fileRef = B8619CA38E2A5B49DBF8546B /* STDSRuntimeException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2CB6DF5CCCBC1EB9ABD03143 /* STDSNotInitializedException.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FCABBF51CBA7F9FEF757B4C /* STDSNotInitializedException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2D42EA44FC01C834209CFED4 /* Stripe3DS2.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E1CB633CA5917B2168BB928 /* Stripe3DS2.xcassets */; }; + 2E2022723BC7A4B2DD5ECE76 /* STDSErrorMessage+Internal.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CB591796654445B8434A501 /* STDSErrorMessage+Internal.m */; }; + 30632F7C730BF8D23B607CF7 /* STDSWarningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 474F39AA3F47D84F8D54E5B7 /* STDSWarningTests.m */; }; + 30B25163BEDEB99CDD101B5F /* NSString+EmptyChecking.h in Headers */ = {isa = PBXBuildFile; fileRef = A8A77DB1C711B8696B2E5FEF /* NSString+EmptyChecking.h */; }; + 310E6B542C52C9EF006A03EC /* 150-issuer.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B4E2C52C9EF006A03EC /* 150-issuer.png */; }; + 310E6B552C52C9EF006A03EC /* 300-issuer.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B4F2C52C9EF006A03EC /* 300-issuer.png */; }; + 310E6B562C52C9EF006A03EC /* 150-payment.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B502C52C9EF006A03EC /* 150-payment.png */; }; + 310E6B572C52C9EF006A03EC /* 300-payment.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B512C52C9EF006A03EC /* 300-payment.png */; }; + 310E6B582C52C9EF006A03EC /* 450-issuer.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B522C52C9EF006A03EC /* 450-issuer.png */; }; + 310E6B592C52C9EF006A03EC /* 450-payment.png in Resources */ = {isa = PBXBuildFile; fileRef = 310E6B532C52C9EF006A03EC /* 450-payment.png */; }; + 3192FCC841152A7E5E6F19D6 /* STDSAlreadyInitializedException.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F1FD11407319293954C6D81 /* STDSAlreadyInitializedException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 319DD1C82B0D50FF0083BA32 /* STDSVisionSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 319DD1C72B0D50F80083BA32 /* STDSVisionSupport.h */; }; + 31C636A25BF83316EE7EB57D /* STDSThreeDS2Service.h in Headers */ = {isa = PBXBuildFile; fileRef = 5EAA444FA7C82BDA4AD907BF /* STDSThreeDS2Service.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 31CDFC322BA8E58100B3DD91 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 31CDFC312BA8E58100B3DD91 /* PrivacyInfo.xcprivacy */; }; + 326AD1DD7BB8A2A6DA66EC67 /* STDSThreeDSProtocolVersion+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 47C29E9F3D2A62676B424CD1 /* STDSThreeDSProtocolVersion+Private.h */; }; + 32BE38D04DD76CA4490389C6 /* STDSConfigParametersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F551808E65CDADD9B3A9CB8 /* STDSConfigParametersTests.m */; }; + 3343DA94A5032627843A343C /* STDSEphemeralKeyPair+Testing.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D6E4F76CBA258163E57DF3 /* STDSEphemeralKeyPair+Testing.h */; }; + 335553A547A3CA80B3901CCF /* STDSStripe3DS2Error.h in Headers */ = {isa = PBXBuildFile; fileRef = AE53BAA72835AFA3B35C58D3 /* STDSStripe3DS2Error.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34256504F0E0E519D586B7CE /* STDSErrorMessage+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5463EB1CC8E85BE3BD12DEFF /* STDSErrorMessage+Internal.h */; }; + 3617B07ABD719F1A5F8302FA /* NSError+Stripe3DS2.h in Headers */ = {isa = PBXBuildFile; fileRef = A6778F8CE36F769DF608F932 /* NSError+Stripe3DS2.h */; }; + 3844F0E21742F43BCB32499A /* STDSDeviceInformationParameter+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4869D6E14F01EDB960CB3065 /* STDSDeviceInformationParameter+Private.h */; }; + 390691CA0EAF81418B0C65DB /* UIView+LayoutSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0F0485B7048DDAA7DA8F9B /* UIView+LayoutSupport.m */; }; + 3909D8028AC485BCCF17B48B /* STDSStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D6269F8B91341C387A911DB /* STDSStackView.m */; }; + 3B430F172A3AD6AA9AFDFC04 /* STDSNavigationBarCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = E1630D876436E43547FF69CE /* STDSNavigationBarCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3C6D16D8E7B5BC865D956A0B /* iOSSnapshotTestCase in Frameworks */ = {isa = PBXBuildFile; productRef = 0118969F6608A92583CC6C98 /* iOSSnapshotTestCase */; }; + 3C88AE37A760B91E93D423CF /* STDSException.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F030410FDABFF833CD8FE46 /* STDSException.m */; }; + 3C8C3BCDDDDEF6B9E150D4E1 /* STDSChallengeResponseSelectionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 52EEBC7D74623E1A1D7E3219 /* STDSChallengeResponseSelectionInfo.h */; }; + 3D674B4EE5ABACD65EB21A1E /* discover.der in Resources */ = {isa = PBXBuildFile; fileRef = 04AB48F014A8D5BA56FEF463 /* discover.der */; }; + 3F2624C33291FCE00D596768 /* STDSDeviceInformationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9580A82A59EB2B0103EDF47A /* STDSDeviceInformationManager.m */; }; + 3F30A8F418870D176A626F00 /* STDSSpacerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E4A57094A671EB14380882 /* STDSSpacerView.m */; }; + 41246FCC0695BABE1489C53E /* UIViewController+Stripe3DS2.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F72413F99F8E50FD0FA085D /* UIViewController+Stripe3DS2.h */; }; + 4142A3AB3E384F91A1E5550B /* STDSExpandableInformationView.m in Sources */ = {isa = PBXBuildFile; fileRef = AE1BBF3A441B1F782B766F61 /* STDSExpandableInformationView.m */; }; + 425849564519D6454DDA3424 /* NSData+JWEHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 5B05A609753EE71502409082 /* NSData+JWEHelpers.h */; }; + 435EFC9119D4B42B2C6A6F88 /* STDSAuthenticationResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EA4AB3BA2FB41B3D5F38978 /* STDSAuthenticationResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4518AD83580FC222B883DAFB /* STDSSimulatorChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 14D60BDF245487FE3362BE07 /* STDSSimulatorChecker.m */; }; + 458BC5A3D0E7B4D05549474F /* STDSBrandingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 2CFAC8D74AFFC2E61BAD82A5 /* STDSBrandingView.h */; }; + 480CD62EC91F4796D1D8D8DC /* STDSSecTypeUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 30D22985091381DFCDF077E9 /* STDSSecTypeUtilitiesTests.m */; }; + 48316C1AB1D9151C205A59A4 /* STDSChallengeResponseMessageExtensionObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BAAC56EDE0AAD2E50E02E9AE /* STDSChallengeResponseMessageExtensionObject.h */; }; + 4A9423CC79312BC3A0DEC38B /* STDSChallengeRequestParametersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EAD29E57D206B5D6BAF9099 /* STDSChallengeRequestParametersTest.m */; }; + 4C0E13298F08D391FE8600CF /* STDSJSONWebSignatureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E4C668442518B446D573AD24 /* STDSJSONWebSignatureTests.m */; }; + 4C267E9A30ADC18E71408ED5 /* STDSTextFieldCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = 68ED2A0E11E1B27980E0C60A /* STDSTextFieldCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C573A44AB0A9CB28D76C621 /* STDSJSONWebEncryption.h in Headers */ = {isa = PBXBuildFile; fileRef = 76EA9DF8572C992E30BCF8A3 /* STDSJSONWebEncryption.h */; }; + 4D73C2FBAC9B96ECB45BDC94 /* Stripe3DS2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */; }; + 4D79F0CFC54E8B923E9238CF /* Stripe3DS2.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4E35267801D94FB84816C519 /* STDSInvalidInputException.h in Headers */ = {isa = PBXBuildFile; fileRef = 853A361AB630A2BD402D1B47 /* STDSInvalidInputException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E4E4BC1E2D041FA530336EE /* STDSSwiftTryCatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 9946D28A86E85A6C84EB6C7B /* STDSSwiftTryCatch.m */; }; + 4FE8C076102C195D609977F5 /* STDSChallengeSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = A4D460893CB5DDE52AA0AB85 /* STDSChallengeSelectionView.m */; }; + 511513A9180F9835B08CBE56 /* STDSProgressViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = E629F85B783E42244E37DFA9 /* STDSProgressViewController.h */; }; + 574A7976213046F02F7F60C4 /* STDSProcessingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AC1C5F6F1311F50D9C37291 /* STDSProcessingView.m */; }; + 57E58A3B88D9EEB9FAE9B383 /* STDSDeviceInformation.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FBE88F6E2C3153A8315EDC6 /* STDSDeviceInformation.h */; }; + 59756023104E4FB886B48936 /* STDSTextChallengeView.h in Headers */ = {isa = PBXBuildFile; fileRef = 82C41E6DC2CC247ECC17F2B9 /* STDSTextChallengeView.h */; }; + 5B16918BF5F67B0F5FB1AFD5 /* ARes.json in Resources */ = {isa = PBXBuildFile; fileRef = 237977B021FE538E5E4FA35C /* ARes.json */; }; + 5C83A3A4631E51EEECBEAA0F /* STDSEllipticCurvePoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 5049A72A6BEB558D8559A5EB /* STDSEllipticCurvePoint.h */; }; + 5DAC25EBAB19555229654E44 /* STDSAuthenticationResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 28E32DA72CAFBF2AF8099151 /* STDSAuthenticationResponseTests.m */; }; + 5FE5FB933E2651C8D84FA477 /* STDSAuthenticationResponseObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 5043E1CF6955176EC3D43F88 /* STDSAuthenticationResponseObject.m */; }; + 602C526C0B52DF81846DA664 /* STDSOSVersionChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = F363439353A6DD554FC44AC2 /* STDSOSVersionChecker.m */; }; + 613EF855524F7861A6DDD8C1 /* STDSImageLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = ECC649704CDDA50E73DB3C10 /* STDSImageLoader.h */; }; + 63AFB3C739DD987EE56F801F /* STDSDirectoryServerCertificate.h in Headers */ = {isa = PBXBuildFile; fileRef = 069990DA025BE9F33602E96A /* STDSDirectoryServerCertificate.h */; }; + 64D05353B50FAF3BB76D6527 /* STDSRuntimeErrorEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 1501A7A3010FFE53D00E318F /* STDSRuntimeErrorEvent.m */; }; + 67D431374DECE36B2606D944 /* STDSProtocolErrorEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A6D20D1DAB7D769E5E97528 /* STDSProtocolErrorEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68F314D63323954EFE309E49 /* STDSOSVersionChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = FAE7026135B5049B732CEDEB /* STDSOSVersionChecker.h */; }; + 6B3AA85EE71FC42EF9BD999F /* STDSEllipticCurvePoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EF5AA537586EF706E4056FD /* STDSEllipticCurvePoint.m */; }; + 6BA3F565B6C0B78F6DED8826 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F31A6580385C6390910AFD93 /* Localizable.strings */; }; + 6DB54DBAEFD08967367B6BF7 /* STDSChallengeParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 23C929BF760735CC01E1E8F5 /* STDSChallengeParameters.m */; }; + 6E727D2B738D7A3527952D28 /* STDSTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE2481364C454058BF2377 /* STDSTransaction.m */; }; + 72F430506E684D61BD5CFE3B /* STDSChallengeStatusReceiver.h in Headers */ = {isa = PBXBuildFile; fileRef = B7A75140FB62A71261CAF5EC /* STDSChallengeStatusReceiver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7337FB20DC0EF491B5922913 /* STDSChallengeResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 315AEE1534F9D9974DC1674B /* STDSChallengeResponse.h */; }; + 75150C4678B8207AA6E7D969 /* STDSBrandingView.m in Sources */ = {isa = PBXBuildFile; fileRef = F9C39A42ECBA58571EFA03C5 /* STDSBrandingView.m */; }; + 75CDFDABD7F9813563E3F618 /* Stripe3DS2-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F024DEAD6628A0229856656 /* Stripe3DS2-Bridging-Header.h */; }; + 793D61AD55630DD336A141A7 /* STDSChallengeResponseMessageExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = AA8BE982004398CF8AAA7D1E /* STDSChallengeResponseMessageExtension.h */; }; + 79557307ECD48D17C566997A /* STDSFooterCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = BA96C9FFA48B85F9C42E8C68 /* STDSFooterCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7C20B044ACFD3A50FD4AC9A8 /* acs_challenge.html in Resources */ = {isa = PBXBuildFile; fileRef = 7971EB56716D5E2F59053B6D /* acs_challenge.html */; }; + 7C7073B54628ED836F422F1D /* STDSJailbreakChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 94943BEAB94E949AF9830EFA /* STDSJailbreakChecker.h */; }; + 7EC8E5523F29B0F01FE827DB /* STDSWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = 581EEE576D24047004C828DF /* STDSWebView.h */; }; + 7ED98325E7E54A54BFE54D6C /* STDSAuthenticationRequestParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = AB871F64FC72EBC7A91F96C1 /* STDSAuthenticationRequestParameters.m */; }; + 81B1C12449852CEB3C59793F /* ul-test.der in Resources */ = {isa = PBXBuildFile; fileRef = B6275E7099E55F0C04688890 /* ul-test.der */; }; + 8233B9F131602DD7143FF96F /* STDSBundleLocator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9564F43EF7CF4F60C3C202CD /* STDSBundleLocator.m */; }; + 825ACBE0734BDCE746E66AB0 /* STDSWhitelistView.h in Headers */ = {isa = PBXBuildFile; fileRef = 33CEADC8006E456FD82CE134 /* STDSWhitelistView.h */; }; + 8322973CD09F2E1C88B6046E /* UIColor+DefaultColors.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A567E08748C86FCE2A75FEA /* UIColor+DefaultColors.h */; }; + 83878337274D8C43D492EC45 /* STDSUICustomizationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CB3154597040B803ADE14F9E /* STDSUICustomizationTests.m */; }; + 847926A68A88BEB58C92D9BC /* STDSThreeDSProtocolVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = 5E6A9565E97DF2544254AA94 /* STDSThreeDSProtocolVersion.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 88197445522F39DB1E9F6AE7 /* NSString+JWEHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 43CB8F416E4F4CEA34391481 /* NSString+JWEHelpers.m */; }; + 884D59AF7FD70889276AFC02 /* STDSWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = AA85BC717C46C1B95AF8C1E3 /* STDSWebView.m */; }; + 890E22971E6F7B0FA1C8D649 /* STDSJSONEncodable.h in Headers */ = {isa = PBXBuildFile; fileRef = E6A3C5D4C46E681B7D76B2BA /* STDSJSONEncodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 893713F4464A88FAB92BC2C6 /* Stripe3DS2.h in Headers */ = {isa = PBXBuildFile; fileRef = 28C6B4F6BA431B9342970FD6 /* Stripe3DS2.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8A0872706BF4CDE72EF0C480 /* STDSConfigParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 89B5ED899439D1C1054CB448 /* STDSConfigParameters.m */; }; + 8A64AFBB2AC15E0E0F57ED25 /* STDSNotInitializedException.m in Sources */ = {isa = PBXBuildFile; fileRef = B7BD1E24EA9427121E148DFC /* STDSNotInitializedException.m */; }; + 8C9BD4B150F384111CBD3BD3 /* STDSErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DDB6222A2EF31FDB7592368 /* STDSErrorMessage.m */; }; + 8E0501C3BB849C2D2D99F34E /* STDSLocalizedString.h in Headers */ = {isa = PBXBuildFile; fileRef = 41E0F638E776A7F3456BDA3E /* STDSLocalizedString.h */; }; + 8E27285FE76F8BECFD70286D /* STDSCompletionEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 10DB3CD8F2541AC06681F2C8 /* STDSCompletionEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8E833B7FCBC3A99072582FEA /* UIFont+DefaultFonts.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E844798B2733C8511AC080E /* UIFont+DefaultFonts.h */; }; + 9146B2E3B89EF3F489D7E233 /* STDSIPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB00E968CF4DD07FB8BF2AAA /* STDSIPAddress.m */; }; + 955CBE0C979291F89567D8A7 /* STDSJSONWebEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 99392D3F702EC6BF7CE15291 /* STDSJSONWebEncryption.m */; }; + 95A2D58051AADDBE16CCA0B6 /* STDSChallengeResponseImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 10C45EE3433B731253E21E24 /* STDSChallengeResponseImage.h */; }; + 9673946B78A7763070E05CD0 /* STDSAuthenticationRequestParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = C91B239583899192C7B26FCF /* STDSAuthenticationRequestParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 97CE8B57A97D037E977E62F3 /* STDSBundleLocator.h in Headers */ = {isa = PBXBuildFile; fileRef = A137A31BEFCCB646FB754EE2 /* STDSBundleLocator.h */; }; + 98075195759ABB91B0D19847 /* STDSJSONWebEncryptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 97505E93BE83F233E69BB5A9 /* STDSJSONWebEncryptionTests.m */; }; + 9843F6AABF7B0A623E7DF979 /* STDSDeviceInformationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A637DA708BC866C2903E0EA /* STDSDeviceInformationManager.h */; }; + 987347CA74818CEB716B9DB3 /* UIFont+DefaultFonts.m in Sources */ = {isa = PBXBuildFile; fileRef = 634095EA97A4E48BF7CAA03F /* UIFont+DefaultFonts.m */; }; + 9B32E9A4CD2BDA61C730F5EB /* STDSJSONDecodable.h in Headers */ = {isa = PBXBuildFile; fileRef = B219360BFB325779485BF702 /* STDSJSONDecodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9B7081D378D1441B98B81207 /* STDSTransactionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A3CCBE9A2E8C1A73D8214E /* STDSTransactionTest.m */; }; + 9C225B9C556C20D1A6F97180 /* STDSChallengeResponseSelectionInfoObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CCA15E0819F9A9D89FEF9EA /* STDSChallengeResponseSelectionInfoObject.h */; }; + 9DC5612697835A383DC6606E /* STDSTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = F40CC91AA69F5296B0AA985C /* STDSTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9E46777893FA2D6691F496D7 /* STDSChallengeResponseViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = E45303DCFFDC390971E6E122 /* STDSChallengeResponseViewController.h */; }; + 9ED9C602C10682CD5DD91C12 /* NSLayoutConstraint+LayoutSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = F99B290CF2B4987A4CFE5DCE /* NSLayoutConstraint+LayoutSupport.m */; }; + 9F0F6085FBD892BF5F14CF86 /* STDSWarning.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BEB5006261C5E070FEE62BF /* STDSWarning.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9F52F67A1A3A2199AEA598FC /* STDSEllipticCurvePointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DA1EE83D91993BCAF13EC72 /* STDSEllipticCurvePointTests.m */; }; + 9F9BB9E7FA18FEDA44F7042E /* STDSDirectoryServerCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9754FA4F3CC6AA921787916B /* STDSDirectoryServerCertificate.m */; }; + A026717FFF299A7C1C51565F /* UIView+LayoutSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 99CB2B66DBD356B5D222EDA9 /* UIView+LayoutSupport.h */; }; + A33A549CF12209280E3B1DBA /* STDSAuthenticationRequestParametersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 969DE2FA3845BB7939D3CD2E /* STDSAuthenticationRequestParametersTest.m */; }; + A39CFB56BF33B21A89987F9C /* STDSChallengeResponseObject+TestObjects.m in Sources */ = {isa = PBXBuildFile; fileRef = 99DB24A9B44979EA19347A76 /* STDSChallengeResponseObject+TestObjects.m */; }; + A4CFDBFC3099BD7E1A694E3E /* UIButton+CustomInitialization.m in Sources */ = {isa = PBXBuildFile; fileRef = AA29B74DD3963B1205AA1C0C /* UIButton+CustomInitialization.m */; }; + A5193DADCD0F48911F775D88 /* NSString+JWEHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 018F4AA25D84E9CBD2546BB5 /* NSString+JWEHelpers.h */; }; + A56AE2CD941D7ADAC7FC8BCC /* STDSBase64URLEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E3760B1369DB1A33C9DFE /* STDSBase64URLEncodingTests.m */; }; + A6909D6C1A68963504733939 /* STDSDebuggerChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = C29F78CC37253B0D33FF86F2 /* STDSDebuggerChecker.m */; }; + A78F19DA3D146A258FB9DE3C /* STDSACSNetworkingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D827473D1858B2BED85BDFA1 /* STDSACSNetworkingManager.h */; }; + A8A86B9BCE9702D74E0D2DB6 /* Stripe3DS2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */; }; + A8CAD8276ED8057A760147CB /* NSData+JWEHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FC15225A5E83948EF546B71 /* NSData+JWEHelpers.m */; }; + A97E1DF84D381952A0FB4C1C /* amex.der in Resources */ = {isa = PBXBuildFile; fileRef = D8B7EE0F935EF44EB2EF7D45 /* amex.der */; }; + AA4D157F31A246890D446981 /* STDSChallengeResponseSelectionInfoObject.m in Sources */ = {isa = PBXBuildFile; fileRef = F046DE266BAAE3DA0AC43785 /* STDSChallengeResponseSelectionInfoObject.m */; }; + AA94519687902E34E5243AEA /* STDSSpacerView.h in Headers */ = {isa = PBXBuildFile; fileRef = 72DBE992B33F45C059EDB597 /* STDSSpacerView.h */; }; + AC474013841CE1DED97F3FA8 /* STDSSimulatorChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = A0ADFB365AD2DE9E84873BB1 /* STDSSimulatorChecker.h */; }; + AE1894FEA510AC394DE73C4B /* STDSTransaction+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A9F5C315222AFCD14C9342F /* STDSTransaction+Private.h */; }; + B012C4A8ACA1D064C40A1B63 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C22D410C297E2C3AFE727A6 /* main.m */; }; + B1C2AA6259CE34B0042A1FB1 /* STDSDeviceInformationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A4050F381183D6FDCC990D01 /* STDSDeviceInformationManagerTests.m */; }; + B31168F5FEC3464AC212DB85 /* STDSACSNetworkingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D6004F2D9880055C0C0BCF1 /* STDSACSNetworkingManager.m */; }; + B426123CC7FCA337E13304F9 /* NSDictionary+DecodingHelpersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8BF51F49E161737406B3A /* NSDictionary+DecodingHelpersTest.m */; }; + B51D1C218F460F67B58A1F0D /* STDSProtocolErrorEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 3255706D2304FB551C97305F /* STDSProtocolErrorEvent.m */; }; + B84ED7FBD49865F23B774067 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B59CC3349FD9BBF37624667 /* AppDelegate.m */; }; + B94E5A5BF5F22998E4CA9ED3 /* STDSEphemeralKeyPair.h in Headers */ = {isa = PBXBuildFile; fileRef = 20533E2C69C4B80BB33A4766 /* STDSEphemeralKeyPair.h */; }; + B9A456E52E62E9DAE9F424A7 /* STDSChallengeResponseMessageExtensionObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D02FAFC403BC901931A629F /* STDSChallengeResponseMessageExtensionObject.m */; }; + BA00D11D792C99B7D5749F59 /* STDSJailbreakChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BFBB92DBE97E16DE9D18B4A /* STDSJailbreakChecker.m */; }; + BA65731CDB75C3124834586C /* STDSSynchronousLocationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F02456AB660E709F332C0C7D /* STDSSynchronousLocationManagerTests.m */; }; + BB379E68231A7DA62F87E628 /* STDSNavigationBarCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F1A5A8E5922748A9BEC724 /* STDSNavigationBarCustomization.m */; }; + BF06B99FECD98C26F7B64665 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08D577934984712C20C5E903 /* XCTest.framework */; }; + BF2651A9F3A31CB1AFEC44BB /* STDSCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = A8E62EC973FC5C9905A40CA8 /* STDSCustomization.m */; }; + BF342613EE89845EB1C4B72A /* STDSChallengeRequestParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 762DEC6662DF8240FE19CFFC /* STDSChallengeRequestParameters.h */; }; + BFC5852E14724750E70DA2A6 /* STDSEphemeralKeyPairTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF80634E06E8D6F598CFC667 /* STDSEphemeralKeyPairTests.m */; }; + C2BA2E6ABDD8E80963FFC671 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08D577934984712C20C5E903 /* XCTest.framework */; }; + C430332104547BA508416B41 /* STDSButtonCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = 5489DCAA65624C6F966816B6 /* STDSButtonCustomization.m */; }; + C4BA75544A7F51355BF4B5F8 /* STDSDirectoryServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 6AA43E7E90E4002DD56B7C3D /* STDSDirectoryServer.h */; }; + CA617B6F5CDFB8FDEEE38636 /* STDSJSONWebSignature.h in Headers */ = {isa = PBXBuildFile; fileRef = 3AA2E6DA60350400C5270E08 /* STDSJSONWebSignature.h */; }; + CCC376FE7424029342A8D15E /* STDSChallengeResponseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B064352BB2876CA7266C410A /* STDSChallengeResponseViewController.m */; }; + CEA1EB91858043A837638FE0 /* STDSSecTypeUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = C8F0E11AA6794BFB1715685E /* STDSSecTypeUtilities.h */; }; + D1427392DA8AD8F5B4118781 /* STDSACSNetworkingManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6118F98576E9A39A7292C6C6 /* STDSACSNetworkingManagerTest.m */; }; + D189220981C7705913D93687 /* STDSJSONEncoderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9B49A5CF64DCB23CD0AEDE /* STDSJSONEncoderTest.m */; }; + D3432C5BF5211A60D5C15D9E /* STDSIntegrityChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = C02DF839637771684BC57B3A /* STDSIntegrityChecker.h */; }; + D3AA14D1E059E90F5A965CC4 /* STDSSelectionCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = 43540AB7D13C0C9A563C3F4D /* STDSSelectionCustomization.m */; }; + D41F091BE1579B801D1BBC20 /* STDSSelectionButton.m in Sources */ = {isa = PBXBuildFile; fileRef = B458FCA7DFDBF75E62A43BA1 /* STDSSelectionButton.m */; }; + D5706ACF84F0A993CE1885D4 /* STDSTextChallengeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E6B6D3CAE363114723472F /* STDSTextChallengeView.m */; }; + D63E3DDC92DD46062A265D86 /* STDSChallengeParametersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 89ED2133B61092FE1B478204 /* STDSChallengeParametersTests.m */; }; + D819C341ECEE1EFE6FE66085 /* STDSChallengeResponseImageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 14F83E51E99D453DF9C2DDE1 /* STDSChallengeResponseImageObject.h */; }; + D83D2FF20387FACC383A2A0F /* STDSAlreadyInitializedException.m in Sources */ = {isa = PBXBuildFile; fileRef = 27AC486DA2A80A43C1F517B2 /* STDSAlreadyInitializedException.m */; }; + D927789F89B413C08DE4D388 /* STDSThreeDS2ServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF8ACC073E814B7DEBA7647 /* STDSThreeDS2ServiceTests.m */; }; + D9F8BA88953A91DC082FA6DA /* STDSSelectionButton.h in Headers */ = {isa = PBXBuildFile; fileRef = C4777F15603AC755362BD846 /* STDSSelectionButton.h */; }; + DB96817598FCE73F5131437E /* STDSDirectoryServerCertificate+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B015B937DD3657D62C61CE58 /* STDSDirectoryServerCertificate+Internal.h */; }; + DBDB8B56FFF5C4234705E698 /* STDSSecTypeUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CF3DED43BD4E0226BFB0759D /* STDSSecTypeUtilities.m */; }; + DC0D9A57F2E312663C088FB8 /* STDSChallengeParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = D03B98D5861739833B197D8F /* STDSChallengeParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC32281BF0847A98B100C3A1 /* STDSConfigParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = A1BFB8640032796CDA016765 /* STDSConfigParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDB98914E1135E8D445545C8 /* STDSIPAddress.h in Headers */ = {isa = PBXBuildFile; fileRef = A3DFB001DB5BA7F1AFA7353A /* STDSIPAddress.h */; }; + DE32EAA8F5C2645652FCFB48 /* STDSSwiftTryCatch.h in Headers */ = {isa = PBXBuildFile; fileRef = E5BC34A661D9080A2EE239F8 /* STDSSwiftTryCatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEEF260365A4D9B0D83E557D /* NSString+EmptyChecking.m in Sources */ = {isa = PBXBuildFile; fileRef = D8D309E4FBE7E73D0BB83BF5 /* NSString+EmptyChecking.m */; }; + DF4D38DC0AF89EAAA8718AAE /* STDSUICustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = A55B2474CD1A39D70869B75B /* STDSUICustomization.m */; }; + E0644CE0260178023E643B23 /* STDSSynchronousLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A5DBDDEB5C5DAE2EA8C95CC3 /* STDSSynchronousLocationManager.m */; }; + E08AF96E690D75F6AB52021F /* STDSWarning.m in Sources */ = {isa = PBXBuildFile; fileRef = A502CCC5190F6B642EEC0CE6 /* STDSWarning.m */; }; + E19C8104FC1A9BAEA0BE8A9D /* UIColor+ThirteenSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 37ED28756222539D37554CC7 /* UIColor+ThirteenSupport.m */; }; + E4DA2CE8DC8C60DE40222185 /* STDSDirectoryServerCertificateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6AA75957DD13FA92FA866AF3 /* STDSDirectoryServerCertificateTests.m */; }; + E630DCBBEFAAD0435BEF989C /* STDSDeviceInformation.m in Sources */ = {isa = PBXBuildFile; fileRef = 56B1A52548593FE78D801734 /* STDSDeviceInformation.m */; }; + E70CFDC59731BA62DD89906C /* STDSWhitelistView.m in Sources */ = {isa = PBXBuildFile; fileRef = 615F1BD510661B38D6825128 /* STDSWhitelistView.m */; }; + E73028FD4537F6E48F927127 /* NSString+EmptyCheckingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B8822D21AADE7DA51931CC /* NSString+EmptyCheckingTests.m */; }; + E7A72BB7CC8DE8DA7FA0DEC5 /* ErrorMessage.json in Resources */ = {isa = PBXBuildFile; fileRef = E12798864D5CFFB7157D4CF5 /* ErrorMessage.json */; }; + E7E424C5264A4DE4A1EC19B6 /* STDSChallengeSelectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = EB910E98EDD6D3E00802793C /* STDSChallengeSelectionView.h */; }; + E90749131EDA0C86BBBFCE5C /* STDSChallengeResponseObject.h in Headers */ = {isa = PBXBuildFile; fileRef = EB1BDF12DBD6264C6199FFA7 /* STDSChallengeResponseObject.h */; }; + EA19A7AEC298F3EC010D1199 /* STDSTestJSONUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FF3235EDD908CC1500399A1 /* STDSTestJSONUtils.m */; }; + EA89021709AFB5F04961EB78 /* cartes-bancaires.der in Resources */ = {isa = PBXBuildFile; fileRef = 83838563F29179FB1E9F2E66 /* cartes-bancaires.der */; }; + EB791827A943AA61976D8C29 /* STDSIntegrityChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = BB065D6900E95C5E90864C16 /* STDSIntegrityChecker.m */; }; + EC51189277D7D68D08C2A65C /* STDSButtonCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = BD94CAE01A17E083E5617E56 /* STDSButtonCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + ECF8755D2602E0E06DCB3210 /* STDSChallengeResponseObjectTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E401845EE5759CCD30786AA /* STDSChallengeResponseObjectTest.m */; }; + EE3854BC2A14F87E8D51B0D0 /* STDSInvalidInputException.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FDAC320458399141EB161E2 /* STDSInvalidInputException.m */; }; + EEE77034728D2CFE38CC5A85 /* STDSLabelCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D320CD5ADA49F17B8BC1B0 /* STDSLabelCustomization.m */; }; + EFC72E766F9FD4EE18D309BC /* STDSJSONWebSignature.m in Sources */ = {isa = PBXBuildFile; fileRef = A1DC6B47C06B522B22EB19FF /* STDSJSONWebSignature.m */; }; + F04482C663F6DF8E522B0833 /* STDSCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = AFF187D1D58405C736474042 /* STDSCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B9EFD359B1AB28A695CB48 /* STDSSynchronousLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C12AE31D386D4A193C00004B /* STDSSynchronousLocationManager.h */; }; + F13A032A4AF15BB61EA18A8D /* STDSChallengeInformationView.h in Headers */ = {isa = PBXBuildFile; fileRef = C893FD867C964775E68AE85F /* STDSChallengeInformationView.h */; }; + F28AD6CA8AB1A5DEE7A8F429 /* STDSDebuggerChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = D122FAE093BC4F649CC6E7AB /* STDSDebuggerChecker.h */; }; + F35963DB86D90320CFA7BCB9 /* STDSExpandableInformationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 939C45AE068CF4F5BEB4C138 /* STDSExpandableInformationView.h */; }; + F4E8353A470750D9BCEA3CD8 /* CRes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8C6D262891811FB3FE0C947E /* CRes.json */; }; + F5513045A25210C524A05DF5 /* STDSSelectionCustomization.h in Headers */ = {isa = PBXBuildFile; fileRef = 7CD753D9BBFC378C1B491B00 /* STDSSelectionCustomization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F569B584D357DC5C55542015 /* STDSFooterCustomization.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CFF141AC98F213122D037C6 /* STDSFooterCustomization.m */; }; + F939E05536AE838DC489C3AA /* STDSChallengeResponseViewControllerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 466DF80D33CEC20DF4E844A9 /* STDSChallengeResponseViewControllerSnapshotTests.m */; }; + FD6456BD86F07C446B9FB6C8 /* ec_test.der in Resources */ = {isa = PBXBuildFile; fileRef = 3E4C02D9D6978AD5C3D7E3D2 /* ec_test.der */; }; + FD8A81873C3BC7579ED07460 /* STDSCompletionEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B6B71C61CF5ADC7796441F5 /* STDSCompletionEvent.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 35B6793A7F3FB130F40209F1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8D219187B704575AD3CD3EC5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5907C55B1F111921112DF2BF; + remoteInfo = Stripe3DS2DemoUI; + }; + 52E1EB51359DA6E75D851D71 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8D219187B704575AD3CD3EC5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57AEC53510AE0DC0539730F3; + remoteInfo = Stripe3DS2; + }; + 6C355B127D670833C76237D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8D219187B704575AD3CD3EC5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57AEC53510AE0DC0539730F3; + remoteInfo = Stripe3DS2; + }; + 947ED275EAB25AD4CD701936 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8D219187B704575AD3CD3EC5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 57AEC53510AE0DC0539730F3; + remoteInfo = Stripe3DS2; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 00E415680C602ED1D6D8E71F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 4D79F0CFC54E8B923E9238CF /* Stripe3DS2.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 43B749EC915FB6D2FF5ABEE0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 58FA2FE74AE67CD3CDAFD7E5 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 726DD7176204D90BD7552B84 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00658E077ECB223C90484AF7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 018F4AA25D84E9CBD2546BB5 /* NSString+JWEHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+JWEHelpers.h"; sourceTree = ""; }; + 01D320CD5ADA49F17B8BC1B0 /* STDSLabelCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSLabelCustomization.m; sourceTree = ""; }; + 02D762271DBCC1AE80E0F9D4 /* Stripe3DS2DemoUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stripe3DS2DemoUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 037C294011A01E4B4DCE5BB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 04AB48F014A8D5BA56FEF463 /* discover.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = discover.der; sourceTree = ""; }; + 04E6B6D3CAE363114723472F /* STDSTextChallengeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSTextChallengeView.m; sourceTree = ""; }; + 0681C043F732148587737233 /* STDSJSONEncoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONEncoder.m; sourceTree = ""; }; + 069990DA025BE9F33602E96A /* STDSDirectoryServerCertificate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDirectoryServerCertificate.h; sourceTree = ""; }; + 06C5207432FB07BD71703BCC /* STDSJSONEncoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJSONEncoder.h; sourceTree = ""; }; + 08D577934984712C20C5E903 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 0BEB5006261C5E070FEE62BF /* STDSWarning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSWarning.h; sourceTree = ""; }; + 0DA1EE83D91993BCAF13EC72 /* STDSEllipticCurvePointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSEllipticCurvePointTests.m; sourceTree = ""; }; + 0E38A775DB9F529427E900CF /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; + 0E3C05BE55CF3086738AA182 /* Stripe3DS2DemoUITests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2DemoUITests-Debug.xcconfig"; sourceTree = ""; }; + 0F024DEAD6628A0229856656 /* Stripe3DS2-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Stripe3DS2-Bridging-Header.h"; sourceTree = ""; }; + 10C45EE3433B731253E21E24 /* STDSChallengeResponseImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseImage.h; sourceTree = ""; }; + 10DB3CD8F2541AC06681F2C8 /* STDSCompletionEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSCompletionEvent.h; sourceTree = ""; }; + 1104D5855D28E49E104CA004 /* mastercard.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = mastercard.der; sourceTree = ""; }; + 138E82072FC6572612A8E248 /* STDSAuthenticationResponseObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSAuthenticationResponseObject.h; sourceTree = ""; }; + 13ED1CC076C6B3FB49C9197A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + 14D60BDF245487FE3362BE07 /* STDSSimulatorChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSimulatorChecker.m; sourceTree = ""; }; + 14F83E51E99D453DF9C2DDE1 /* STDSChallengeResponseImageObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseImageObject.h; sourceTree = ""; }; + 1501A7A3010FFE53D00E318F /* STDSRuntimeErrorEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSRuntimeErrorEvent.m; sourceTree = ""; }; + 157970FF5B4A817041E3D668 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + 172AF5C85EDBD92A1030E361 /* STDSDeviceInformationParameter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDeviceInformationParameter.h; sourceTree = ""; }; + 18D88DE3E34106F934015BF9 /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mt; path = mt.lproj/Localizable.strings; sourceTree = ""; }; + 1BBADD412A7C2D1FF35A7C86 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 1BFBB92DBE97E16DE9D18B4A /* STDSJailbreakChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJailbreakChecker.m; sourceTree = ""; }; + 1D6269F8B91341C387A911DB /* STDSStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSStackView.m; sourceTree = ""; }; + 1E844798B2733C8511AC080E /* UIFont+DefaultFonts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+DefaultFonts.h"; sourceTree = ""; }; + 1F0DADEABA0B043A6CA36DD6 /* STDSDemoViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDemoViewController.h; sourceTree = ""; }; + 1F1FD11407319293954C6D81 /* STDSAlreadyInitializedException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSAlreadyInitializedException.h; sourceTree = ""; }; + 1FBE88F6E2C3153A8315EDC6 /* STDSDeviceInformation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDeviceInformation.h; sourceTree = ""; }; + 1FC15225A5E83948EF546B71 /* NSData+JWEHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+JWEHelpers.m"; sourceTree = ""; }; + 20533E2C69C4B80BB33A4766 /* STDSEphemeralKeyPair.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSEphemeralKeyPair.h; sourceTree = ""; }; + 210B22FF4DCB0C6E7C763EAB /* STDSUICustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSUICustomization.h; sourceTree = ""; }; + 2192708AD201F80F6E1A39F7 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 237977B021FE538E5E4FA35C /* ARes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ARes.json; sourceTree = ""; }; + 23A3DDCE911F8C43CF74CDF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 23C929BF760735CC01E1E8F5 /* STDSChallengeParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeParameters.m; sourceTree = ""; }; + 24CE086326AF3DE336BF4F4C /* STDSChallengeResponseObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseObject.m; sourceTree = ""; }; + 25C7D18868DDADEF4CB6220C /* STDSDeviceInformationParameter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDeviceInformationParameter.m; sourceTree = ""; }; + 26C20D4B77372845627D6466 /* NSError+Stripe3DS2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+Stripe3DS2.m"; sourceTree = ""; }; + 278F2C40987F5218BD031E3D /* STDSErrorMessageTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSErrorMessageTest.m; sourceTree = ""; }; + 27AC486DA2A80A43C1F517B2 /* STDSAlreadyInitializedException.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSAlreadyInitializedException.m; sourceTree = ""; }; + 28C6B4F6BA431B9342970FD6 /* Stripe3DS2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stripe3DS2.h; sourceTree = ""; }; + 28E32DA72CAFBF2AF8099151 /* STDSAuthenticationResponseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSAuthenticationResponseTests.m; sourceTree = ""; }; + 2C2666DF7D23EBC3FFE7CCF1 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 2CB591796654445B8434A501 /* STDSErrorMessage+Internal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "STDSErrorMessage+Internal.m"; sourceTree = ""; }; + 2CFAC8D74AFFC2E61BAD82A5 /* STDSBrandingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSBrandingView.h; sourceTree = ""; }; + 2D6004F2D9880055C0C0BCF1 /* STDSACSNetworkingManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSACSNetworkingManager.m; sourceTree = ""; }; + 2EF5AA537586EF706E4056FD /* STDSEllipticCurvePoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSEllipticCurvePoint.m; sourceTree = ""; }; + 2FCABBF51CBA7F9FEF757B4C /* STDSNotInitializedException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSNotInitializedException.h; sourceTree = ""; }; + 30D22985091381DFCDF077E9 /* STDSSecTypeUtilitiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSecTypeUtilitiesTests.m; sourceTree = ""; }; + 310E6B4E2C52C9EF006A03EC /* 150-issuer.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "150-issuer.png"; sourceTree = ""; }; + 310E6B4F2C52C9EF006A03EC /* 300-issuer.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "300-issuer.png"; sourceTree = ""; }; + 310E6B502C52C9EF006A03EC /* 150-payment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "150-payment.png"; sourceTree = ""; }; + 310E6B512C52C9EF006A03EC /* 300-payment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "300-payment.png"; sourceTree = ""; }; + 310E6B522C52C9EF006A03EC /* 450-issuer.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "450-issuer.png"; sourceTree = ""; }; + 310E6B532C52C9EF006A03EC /* 450-payment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "450-payment.png"; sourceTree = ""; }; + 315AEE1534F9D9974DC1674B /* STDSChallengeResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponse.h; sourceTree = ""; }; + 319DD1C72B0D50F80083BA32 /* STDSVisionSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSVisionSupport.h; sourceTree = ""; }; + 31CDFC312BA8E58100B3DD91 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3255706D2304FB551C97305F /* STDSProtocolErrorEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSProtocolErrorEvent.m; sourceTree = ""; }; + 33CEADC8006E456FD82CE134 /* STDSWhitelistView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSWhitelistView.h; sourceTree = ""; }; + 371F8076630A8839C1F6A508 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 37ED28756222539D37554CC7 /* UIColor+ThirteenSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIColor+ThirteenSupport.m"; sourceTree = ""; }; + 382C583385FAECA91366769E /* STDSDemoViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDemoViewController.m; sourceTree = ""; }; + 38572D4E32BF3670F1C83409 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = ""; }; + 38F3A7D625E4B0D3506A37F6 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + 3A6D20D1DAB7D769E5E97528 /* STDSProtocolErrorEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSProtocolErrorEvent.h; sourceTree = ""; }; + 3A8CD68A006F970DDC54469A /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + 3A9F828840A3B361842E38DE /* STDSChallengeResponseImageObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseImageObject.m; sourceTree = ""; }; + 3AA2E6DA60350400C5270E08 /* STDSJSONWebSignature.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJSONWebSignature.h; sourceTree = ""; }; + 3B6B71C61CF5ADC7796441F5 /* STDSCompletionEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSCompletionEvent.m; sourceTree = ""; }; + 3B9A821F35B93898A7AC1ADF /* sl-SI */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sl-SI"; path = "sl-SI.lproj/Localizable.strings"; sourceTree = ""; }; + 3C22D410C297E2C3AFE727A6 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 3E4C02D9D6978AD5C3D7E3D2 /* ec_test.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = ec_test.der; sourceTree = ""; }; + 3F017BF6ECE0EBE4E0DFCF9C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 406D88ADED27D64DED2002A2 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + 40DC4913154C3D3A27E35207 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/Localizable.strings"; sourceTree = ""; }; + 41E0F638E776A7F3456BDA3E /* STDSLocalizedString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSLocalizedString.h; sourceTree = ""; }; + 43540AB7D13C0C9A563C3F4D /* STDSSelectionCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSelectionCustomization.m; sourceTree = ""; }; + 43CB8F416E4F4CEA34391481 /* NSString+JWEHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+JWEHelpers.m"; sourceTree = ""; }; + 461F938CDE7ED6D06D9D2700 /* STDSException+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSException+Internal.h"; sourceTree = ""; }; + 461FDC130846625171164A9E /* et-EE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "et-EE"; path = "et-EE.lproj/Localizable.strings"; sourceTree = ""; }; + 466DF80D33CEC20DF4E844A9 /* STDSChallengeResponseViewControllerSnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseViewControllerSnapshotTests.m; sourceTree = ""; }; + 474F39AA3F47D84F8D54E5B7 /* STDSWarningTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSWarningTests.m; sourceTree = ""; }; + 47C29E9F3D2A62676B424CD1 /* STDSThreeDSProtocolVersion+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSThreeDSProtocolVersion+Private.h"; sourceTree = ""; }; + 4869D6E14F01EDB960CB3065 /* STDSDeviceInformationParameter+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSDeviceInformationParameter+Private.h"; sourceTree = ""; }; + 49EF79D69693F937FEC46906 /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Localizable.strings; sourceTree = ""; }; + 4A567E08748C86FCE2A75FEA /* UIColor+DefaultColors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIColor+DefaultColors.h"; sourceTree = ""; }; + 4B1E3760B1369DB1A33C9DFE /* STDSBase64URLEncodingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSBase64URLEncodingTests.m; sourceTree = ""; }; + 4BC4D9FD1D3869D491CC2CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 4C158B552B0CBE635A2AF7BB /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = ""; }; + 4CCA15E0819F9A9D89FEF9EA /* STDSChallengeResponseSelectionInfoObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseSelectionInfoObject.h; sourceTree = ""; }; + 4DB91F58CF23E72636C69C41 /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + 4DD55BECC3FB85216FE46218 /* UIButton+CustomInitialization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIButton+CustomInitialization.h"; sourceTree = ""; }; + 4EA4AB3BA2FB41B3D5F38978 /* STDSAuthenticationResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSAuthenticationResponse.h; sourceTree = ""; }; + 4EAD29E57D206B5D6BAF9099 /* STDSChallengeRequestParametersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeRequestParametersTest.m; sourceTree = ""; }; + 4EB746D4E46ABD2452A154AE /* Stripe3DS2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Stripe3DS2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5043E1CF6955176EC3D43F88 /* STDSAuthenticationResponseObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSAuthenticationResponseObject.m; sourceTree = ""; }; + 5049A72A6BEB558D8559A5EB /* STDSEllipticCurvePoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSEllipticCurvePoint.h; sourceTree = ""; }; + 51E4A57094A671EB14380882 /* STDSSpacerView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSpacerView.m; sourceTree = ""; }; + 52D6E4F76CBA258163E57DF3 /* STDSEphemeralKeyPair+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSEphemeralKeyPair+Testing.h"; sourceTree = ""; }; + 52EEBC7D74623E1A1D7E3219 /* STDSChallengeResponseSelectionInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseSelectionInfo.h; sourceTree = ""; }; + 5316D0759F7F2ED0BF1D3B2E /* Stripe3DS2DemoUI-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2DemoUI-Debug.xcconfig"; sourceTree = ""; }; + 5463EB1CC8E85BE3BD12DEFF /* STDSErrorMessage+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSErrorMessage+Internal.h"; sourceTree = ""; }; + 5489DCAA65624C6F966816B6 /* STDSButtonCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSButtonCustomization.m; sourceTree = ""; }; + 56B1A52548593FE78D801734 /* STDSDeviceInformation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDeviceInformation.m; sourceTree = ""; }; + 57B8822D21AADE7DA51931CC /* NSString+EmptyCheckingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+EmptyCheckingTests.m"; sourceTree = ""; }; + 581EEE576D24047004C828DF /* STDSWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSWebView.h; sourceTree = ""; }; + 58C38FF9205C9B7D79C51A37 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; + 5B05A609753EE71502409082 /* NSData+JWEHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+JWEHelpers.h"; sourceTree = ""; }; + 5D93713CADD5589B5E6F6A0D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 5E401845EE5759CCD30786AA /* STDSChallengeResponseObjectTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseObjectTest.m; sourceTree = ""; }; + 5E6A9565E97DF2544254AA94 /* STDSThreeDSProtocolVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSThreeDSProtocolVersion.h; sourceTree = ""; }; + 5EAA444FA7C82BDA4AD907BF /* STDSThreeDS2Service.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSThreeDS2Service.h; sourceTree = ""; }; + 5EE8BF51F49E161737406B3A /* NSDictionary+DecodingHelpersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+DecodingHelpersTest.m"; sourceTree = ""; }; + 5F551808E65CDADD9B3A9CB8 /* STDSConfigParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSConfigParametersTests.m; sourceTree = ""; }; + 5FDE2481364C454058BF2377 /* STDSTransaction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSTransaction.m; sourceTree = ""; }; + 5FF3235EDD908CC1500399A1 /* STDSTestJSONUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSTestJSONUtils.m; sourceTree = ""; }; + 60693BC560C4927ACFD2E6C3 /* STDSChallengeRequestParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeRequestParameters.m; sourceTree = ""; }; + 6118F98576E9A39A7292C6C6 /* STDSACSNetworkingManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSACSNetworkingManagerTest.m; sourceTree = ""; }; + 615F1BD510661B38D6825128 /* STDSWhitelistView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSWhitelistView.m; sourceTree = ""; }; + 62773D5C85C567F28BCE0DA5 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; + 634095EA97A4E48BF7CAA03F /* UIFont+DefaultFonts.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+DefaultFonts.m"; sourceTree = ""; }; + 645D0BB927B7F8A0D37EE04D /* STDSErrorMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSErrorMessage.h; sourceTree = ""; }; + 6690476C1BEA59C37576FB36 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + 68ED2A0E11E1B27980E0C60A /* STDSTextFieldCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSTextFieldCustomization.h; sourceTree = ""; }; + 6932DD26C7C16938DFA0E198 /* STDSLabelCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSLabelCustomization.h; sourceTree = ""; }; + 6940D75CA36F7DB9FB56196B /* Stripe3DS2DemoUI-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2DemoUI-Release.xcconfig"; sourceTree = ""; }; + 694DF6F47767A651907D55D4 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 6AA43E7E90E4002DD56B7C3D /* STDSDirectoryServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDirectoryServer.h; sourceTree = ""; }; + 6AA75957DD13FA92FA866AF3 /* STDSDirectoryServerCertificateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDirectoryServerCertificateTests.m; sourceTree = ""; }; + 6B487D8DD39DAC17042A3F04 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 6B9B49A5CF64DCB23CD0AEDE /* STDSJSONEncoderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONEncoderTest.m; sourceTree = ""; }; + 6CFF141AC98F213122D037C6 /* STDSFooterCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSFooterCustomization.m; sourceTree = ""; }; + 6DDB6222A2EF31FDB7592368 /* STDSErrorMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSErrorMessage.m; sourceTree = ""; }; + 6F030410FDABFF833CD8FE46 /* STDSException.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSException.m; sourceTree = ""; }; + 6FDAC320458399141EB161E2 /* STDSInvalidInputException.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSInvalidInputException.m; sourceTree = ""; }; + 71494F94F36F4B731A27D182 /* lt-LT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lt-LT"; path = "lt-LT.lproj/Localizable.strings"; sourceTree = ""; }; + 72DBE992B33F45C059EDB597 /* STDSSpacerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSpacerView.h; sourceTree = ""; }; + 762DEC6662DF8240FE19CFFC /* STDSChallengeRequestParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeRequestParameters.h; sourceTree = ""; }; + 76EA9DF8572C992E30BCF8A3 /* STDSJSONWebEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJSONWebEncryption.h; sourceTree = ""; }; + 77646CEC7EE754F47EDBDA4F /* STDSThreeDS2Service.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSThreeDS2Service.m; sourceTree = ""; }; + 7859D635964B2854A07B5285 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = ""; }; + 7971EB56716D5E2F59053B6D /* acs_challenge.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = acs_challenge.html; sourceTree = ""; }; + 7A637DA708BC866C2903E0EA /* STDSDeviceInformationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDeviceInformationManager.h; sourceTree = ""; }; + 7AC1C5F6F1311F50D9C37291 /* STDSProcessingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSProcessingView.m; sourceTree = ""; }; + 7CD753D9BBFC378C1B491B00 /* STDSSelectionCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSelectionCustomization.h; sourceTree = ""; }; + 7D02FAFC403BC901931A629F /* STDSChallengeResponseMessageExtensionObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseMessageExtensionObject.m; sourceTree = ""; }; + 7FF8ACC073E814B7DEBA7647 /* STDSThreeDS2ServiceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSThreeDS2ServiceTests.m; sourceTree = ""; }; + 82C41E6DC2CC247ECC17F2B9 /* STDSTextChallengeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSTextChallengeView.h; sourceTree = ""; }; + 83838563F29179FB1E9F2E66 /* cartes-bancaires.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "cartes-bancaires.der"; sourceTree = ""; }; + 83BDD516620860DC08B36B96 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + 84967ED0D9B206B3F5AA4F02 /* STDSProgressViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSProgressViewController.m; sourceTree = ""; }; + 853A361AB630A2BD402D1B47 /* STDSInvalidInputException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSInvalidInputException.h; sourceTree = ""; }; + 869733153BCA6BE2EB7A452F /* UIColor+DefaultColors.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIColor+DefaultColors.m"; sourceTree = ""; }; + 89B5ED899439D1C1054CB448 /* STDSConfigParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSConfigParameters.m; sourceTree = ""; }; + 89ED2133B61092FE1B478204 /* STDSChallengeParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeParametersTests.m; sourceTree = ""; }; + 8A6035A8211B3D50D10EB947 /* bg-BG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bg-BG"; path = "bg-BG.lproj/Localizable.strings"; sourceTree = ""; }; + 8A9F5C315222AFCD14C9342F /* STDSTransaction+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSTransaction+Private.h"; sourceTree = ""; }; + 8AC648332A706809F1BDFD59 /* STDSThreeDSProtocolVersion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSThreeDSProtocolVersion.m; sourceTree = ""; }; + 8B0F0485B7048DDAA7DA8F9B /* UIView+LayoutSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+LayoutSupport.m"; sourceTree = ""; }; + 8B59CC3349FD9BBF37624667 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B8D429F1C03603DB218700F /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + 8C6D262891811FB3FE0C947E /* CRes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CRes.json; sourceTree = ""; }; + 8F72413F99F8E50FD0FA085D /* UIViewController+Stripe3DS2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Stripe3DS2.h"; sourceTree = ""; }; + 939C45AE068CF4F5BEB4C138 /* STDSExpandableInformationView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSExpandableInformationView.h; sourceTree = ""; }; + 94943BEAB94E949AF9830EFA /* STDSJailbreakChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJailbreakChecker.h; sourceTree = ""; }; + 95641ECBE1AE1CC198013405 /* STDSProcessingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSProcessingView.h; sourceTree = ""; }; + 9564F43EF7CF4F60C3C202CD /* STDSBundleLocator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSBundleLocator.m; sourceTree = ""; }; + 9580A82A59EB2B0103EDF47A /* STDSDeviceInformationManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDeviceInformationManager.m; sourceTree = ""; }; + 969DE2FA3845BB7939D3CD2E /* STDSAuthenticationRequestParametersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSAuthenticationRequestParametersTest.m; sourceTree = ""; }; + 97163B5E9598CA3A298920B9 /* STDSTextFieldCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSTextFieldCustomization.m; sourceTree = ""; }; + 97505E93BE83F233E69BB5A9 /* STDSJSONWebEncryptionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONWebEncryptionTests.m; sourceTree = ""; }; + 9754FA4F3CC6AA921787916B /* STDSDirectoryServerCertificate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDirectoryServerCertificate.m; sourceTree = ""; }; + 99392D3F702EC6BF7CE15291 /* STDSJSONWebEncryption.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONWebEncryption.m; sourceTree = ""; }; + 99451064136843047C7881AD /* UIViewController+Stripe3DS2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Stripe3DS2.m"; sourceTree = ""; }; + 9946D28A86E85A6C84EB6C7B /* STDSSwiftTryCatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSwiftTryCatch.m; sourceTree = ""; }; + 99CB2B66DBD356B5D222EDA9 /* UIView+LayoutSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+LayoutSupport.h"; sourceTree = ""; }; + 99DB24A9B44979EA19347A76 /* STDSChallengeResponseObject+TestObjects.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "STDSChallengeResponseObject+TestObjects.m"; sourceTree = ""; }; + 9A8B0001705400C661678617 /* UIColor+ThirteenSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIColor+ThirteenSupport.h"; sourceTree = ""; }; + 9E1CB633CA5917B2168BB928 /* Stripe3DS2.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Stripe3DS2.xcassets; sourceTree = ""; }; + 9F4376BB07FD1EE783249AED /* STDSStripe3DS2Error.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSStripe3DS2Error.m; sourceTree = ""; }; + 9F5C194C81AF1F5578698A27 /* STDSChallengeResponseObject+TestObjects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSChallengeResponseObject+TestObjects.h"; sourceTree = ""; }; + A09A89BBE7360F93195641C9 /* STDSImageLoader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSImageLoader.m; sourceTree = ""; }; + A0ADFB365AD2DE9E84873BB1 /* STDSSimulatorChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSimulatorChecker.h; sourceTree = ""; }; + A137A31BEFCCB646FB754EE2 /* STDSBundleLocator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSBundleLocator.h; sourceTree = ""; }; + A1BFB8640032796CDA016765 /* STDSConfigParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSConfigParameters.h; sourceTree = ""; }; + A1DC6B47C06B522B22EB19FF /* STDSJSONWebSignature.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONWebSignature.m; sourceTree = ""; }; + A3DFB001DB5BA7F1AFA7353A /* STDSIPAddress.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSIPAddress.h; sourceTree = ""; }; + A4050F381183D6FDCC990D01 /* STDSDeviceInformationManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDeviceInformationManagerTests.m; sourceTree = ""; }; + A4620559FBB8A42E15044E32 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + A4A3CCBE9A2E8C1A73D8214E /* STDSTransactionTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSTransactionTest.m; sourceTree = ""; }; + A4D460893CB5DDE52AA0AB85 /* STDSChallengeSelectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeSelectionView.m; sourceTree = ""; }; + A502CCC5190F6B642EEC0CE6 /* STDSWarning.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSWarning.m; sourceTree = ""; }; + A55B2474CD1A39D70869B75B /* STDSUICustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSUICustomization.m; sourceTree = ""; }; + A5DBDDEB5C5DAE2EA8C95CC3 /* STDSSynchronousLocationManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSynchronousLocationManager.m; sourceTree = ""; }; + A6778F8CE36F769DF608F932 /* NSError+Stripe3DS2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSError+Stripe3DS2.h"; sourceTree = ""; }; + A70071543985284E0D9DAC64 /* visa.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = visa.der; sourceTree = ""; }; + A8A77DB1C711B8696B2E5FEF /* NSString+EmptyChecking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+EmptyChecking.h"; sourceTree = ""; }; + A8E62EC973FC5C9905A40CA8 /* STDSCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSCustomization.m; sourceTree = ""; }; + A9CD7EC589C5CC6CE5CF9310 /* Stripe3DS2-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2-Debug.xcconfig"; sourceTree = ""; }; + AA29B74DD3963B1205AA1C0C /* UIButton+CustomInitialization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIButton+CustomInitialization.m"; sourceTree = ""; }; + AA85BC717C46C1B95AF8C1E3 /* STDSWebView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSWebView.m; sourceTree = ""; }; + AA8A113E655E23381CB3BDA4 /* Stripe3DS2DemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Stripe3DS2DemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AA8BE982004398CF8AAA7D1E /* STDSChallengeResponseMessageExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseMessageExtension.h; sourceTree = ""; }; + AB871F64FC72EBC7A91F96C1 /* STDSAuthenticationRequestParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSAuthenticationRequestParameters.m; sourceTree = ""; }; + AE1BBF3A441B1F782B766F61 /* STDSExpandableInformationView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSExpandableInformationView.m; sourceTree = ""; }; + AE53BAA72835AFA3B35C58D3 /* STDSStripe3DS2Error.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSStripe3DS2Error.h; sourceTree = ""; }; + AF389752CCEB6FA734AAE31E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + AFF187D1D58405C736474042 /* STDSCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSCustomization.h; sourceTree = ""; }; + B015B937DD3657D62C61CE58 /* STDSDirectoryServerCertificate+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "STDSDirectoryServerCertificate+Internal.h"; sourceTree = ""; }; + B064352BB2876CA7266C410A /* STDSChallengeResponseViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseViewController.m; sourceTree = ""; }; + B1A7D41498B79E35BD724681 /* ms-MY */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ms-MY"; path = "ms-MY.lproj/Localizable.strings"; sourceTree = ""; }; + B219360BFB325779485BF702 /* STDSJSONDecodable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJSONDecodable.h; sourceTree = ""; }; + B458FCA7DFDBF75E62A43BA1 /* STDSSelectionButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSelectionButton.m; sourceTree = ""; }; + B6275E7099E55F0C04688890 /* ul-test.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ul-test.der"; sourceTree = ""; }; + B71A1C110DCC23A9CE929837 /* STDSException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSException.h; sourceTree = ""; }; + B7A75140FB62A71261CAF5EC /* STDSChallengeStatusReceiver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeStatusReceiver.h; sourceTree = ""; }; + B7AE3B4732D203134FE096FE /* STDSStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSStackView.h; sourceTree = ""; }; + B7BD1E24EA9427121E148DFC /* STDSNotInitializedException.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSNotInitializedException.m; sourceTree = ""; }; + B8619CA38E2A5B49DBF8546B /* STDSRuntimeException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSRuntimeException.h; sourceTree = ""; }; + B97D91CB54F48F64CAC9E378 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + BA96C9FFA48B85F9C42E8C68 /* STDSFooterCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSFooterCustomization.h; sourceTree = ""; }; + BAAC56EDE0AAD2E50E02E9AE /* STDSChallengeResponseMessageExtensionObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseMessageExtensionObject.h; sourceTree = ""; }; + BB065D6900E95C5E90864C16 /* STDSIntegrityChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSIntegrityChecker.m; sourceTree = ""; }; + BC3EE020DC9358FF56389BBE /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; + BD94CAE01A17E083E5617E56 /* STDSButtonCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSButtonCustomization.h; sourceTree = ""; }; + BF80634E06E8D6F598CFC667 /* STDSEphemeralKeyPairTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSEphemeralKeyPairTests.m; sourceTree = ""; }; + C02DF839637771684BC57B3A /* STDSIntegrityChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSIntegrityChecker.h; sourceTree = ""; }; + C12AE31D386D4A193C00004B /* STDSSynchronousLocationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSynchronousLocationManager.h; sourceTree = ""; }; + C29F78CC37253B0D33FF86F2 /* STDSDebuggerChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDebuggerChecker.m; sourceTree = ""; }; + C4777F15603AC755362BD846 /* STDSSelectionButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSelectionButton.h; sourceTree = ""; }; + C566D6E444FCA4BCEC65F3D2 /* NSDictionary+DecodingHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+DecodingHelpers.m"; sourceTree = ""; }; + C78AA1D7AD2BAB52EE58D078 /* NSDictionary+DecodingHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+DecodingHelpers.h"; sourceTree = ""; }; + C893FD867C964775E68AE85F /* STDSChallengeInformationView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeInformationView.h; sourceTree = ""; }; + C8F0E11AA6794BFB1715685E /* STDSSecTypeUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSecTypeUtilities.h; sourceTree = ""; }; + C91B239583899192C7B26FCF /* STDSAuthenticationRequestParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSAuthenticationRequestParameters.h; sourceTree = ""; }; + CB00E968CF4DD07FB8BF2AAA /* STDSIPAddress.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSIPAddress.m; sourceTree = ""; }; + CB3154597040B803ADE14F9E /* STDSUICustomizationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSUICustomizationTests.m; sourceTree = ""; }; + CD33421F13A675DCFE597FFB /* STDSRuntimeException.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSRuntimeException.m; sourceTree = ""; }; + CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stripe3DS2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CECC1E22D0F0336039B435DE /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; + CF3DED43BD4E0226BFB0759D /* STDSSecTypeUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSecTypeUtilities.m; sourceTree = ""; }; + D03B98D5861739833B197D8F /* STDSChallengeParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeParameters.h; sourceTree = ""; }; + D122FAE093BC4F649CC6E7AB /* STDSDebuggerChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSDebuggerChecker.h; sourceTree = ""; }; + D24777EE9931075405F370DB /* STDSChallengeInformationView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeInformationView.m; sourceTree = ""; }; + D3F5A6D5A680F008C00A2D69 /* nn-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nn-NO"; path = "nn-NO.lproj/Localizable.strings"; sourceTree = ""; }; + D7A9D36FEF7E97CF735D82B8 /* Stripe3DS2DemoUITests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2DemoUITests-Release.xcconfig"; sourceTree = ""; }; + D827473D1858B2BED85BDFA1 /* STDSACSNetworkingManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSACSNetworkingManager.h; sourceTree = ""; }; + D8B7EE0F935EF44EB2EF7D45 /* amex.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = amex.der; sourceTree = ""; }; + D8D309E4FBE7E73D0BB83BF5 /* NSString+EmptyChecking.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+EmptyChecking.m"; sourceTree = ""; }; + DE7D85369E012B15702D8DF9 /* STDSRuntimeErrorEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSRuntimeErrorEvent.h; sourceTree = ""; }; + E0949399E96663964091C0EF /* NSLayoutConstraint+LayoutSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSLayoutConstraint+LayoutSupport.h"; sourceTree = ""; }; + E12798864D5CFFB7157D4CF5 /* ErrorMessage.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ErrorMessage.json; sourceTree = ""; }; + E1630D876436E43547FF69CE /* STDSNavigationBarCustomization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSNavigationBarCustomization.h; sourceTree = ""; }; + E45303DCFFDC390971E6E122 /* STDSChallengeResponseViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseViewController.h; sourceTree = ""; }; + E4C668442518B446D573AD24 /* STDSJSONWebSignatureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSJSONWebSignatureTests.m; sourceTree = ""; }; + E5BC34A661D9080A2EE239F8 /* STDSSwiftTryCatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSSwiftTryCatch.h; sourceTree = ""; }; + E629F85B783E42244E37DFA9 /* STDSProgressViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSProgressViewController.h; sourceTree = ""; }; + E64C700DED163CB77DCDEA78 /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; + E6A3C5D4C46E681B7D76B2BA /* STDSJSONEncodable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSJSONEncodable.h; sourceTree = ""; }; + E9F1A5A8E5922748A9BEC724 /* STDSNavigationBarCustomization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSNavigationBarCustomization.m; sourceTree = ""; }; + EB1BDF12DBD6264C6199FFA7 /* STDSChallengeResponseObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeResponseObject.h; sourceTree = ""; }; + EB910E98EDD6D3E00802793C /* STDSChallengeSelectionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSChallengeSelectionView.h; sourceTree = ""; }; + ECC649704CDDA50E73DB3C10 /* STDSImageLoader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSImageLoader.h; sourceTree = ""; }; + EF5219762616ACF204F08C19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + F02456AB660E709F332C0C7D /* STDSSynchronousLocationManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSSynchronousLocationManagerTests.m; sourceTree = ""; }; + F046DE266BAAE3DA0AC43785 /* STDSChallengeResponseSelectionInfoObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSChallengeResponseSelectionInfoObject.m; sourceTree = ""; }; + F112967E9FE8EE02FFFDC313 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; + F139E48FDEFD921FF410892F /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + F2BC7014D26158AC9D74CC67 /* Stripe3DS2Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2Tests-Debug.xcconfig"; sourceTree = ""; }; + F363439353A6DD554FC44AC2 /* STDSOSVersionChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSOSVersionChecker.m; sourceTree = ""; }; + F40CC91AA69F5296B0AA985C /* STDSTransaction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSTransaction.h; sourceTree = ""; }; + F4D6DB90E3D01495C1F9BFE9 /* ro-RO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ro-RO"; path = "ro-RO.lproj/Localizable.strings"; sourceTree = ""; }; + F5686FB8EFA3AE3B39F85BBC /* Stripe3DS2Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2Tests-Release.xcconfig"; sourceTree = ""; }; + F86F34D1339F7467D0FDAC75 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + F8F6FFED1A0966A49B78F252 /* Stripe3DS2-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Stripe3DS2-Release.xcconfig"; sourceTree = ""; }; + F99B290CF2B4987A4CFE5DCE /* NSLayoutConstraint+LayoutSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSLayoutConstraint+LayoutSupport.m"; sourceTree = ""; }; + F9C39A42ECBA58571EFA03C5 /* STDSBrandingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSBrandingView.m; sourceTree = ""; }; + F9D521B45783D36C8E83F0EA /* STDSEphemeralKeyPair.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSEphemeralKeyPair.m; sourceTree = ""; }; + FAE7026135B5049B732CEDEB /* STDSOSVersionChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = STDSOSVersionChecker.h; sourceTree = ""; }; + FC52C3C844BDA981EE34BAB9 /* STDSDeviceInformationParameterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STDSDeviceInformationParameterTests.m; sourceTree = ""; }; + FE1DE9D978E6E682CB094128 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 16B0A41D6C0F157DDF229397 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2BA2E6ABDD8E80963FFC671 /* XCTest.framework in Frameworks */, + 4D73C2FBAC9B96ECB45BDC94 /* Stripe3DS2.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8ECF69FED0DDA991C821CF6A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1469551DB874336B68F6C39D /* Stripe3DS2.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A56B40D43D552FDE77670CB5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BF06B99FECD98C26F7B64665 /* XCTest.framework in Frameworks */, + A8A86B9BCE9702D74E0D2DB6 /* Stripe3DS2.framework in Frameworks */, + 3C6D16D8E7B5BC865D956A0B /* iOSSnapshotTestCase in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C65BFA70E847549921E39F4E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 148D1BA6A2E8B2B60CA9951F /* Stripe3DS2 */ = { + isa = PBXGroup; + children = ( + E4F3BE80D5F61EAC1694F954 /* include */, + A3DDF4E193616FA951B1B120 /* Resources */, + 5D93713CADD5589B5E6F6A0D /* Info.plist */, + 5B05A609753EE71502409082 /* NSData+JWEHelpers.h */, + 1FC15225A5E83948EF546B71 /* NSData+JWEHelpers.m */, + C78AA1D7AD2BAB52EE58D078 /* NSDictionary+DecodingHelpers.h */, + C566D6E444FCA4BCEC65F3D2 /* NSDictionary+DecodingHelpers.m */, + A6778F8CE36F769DF608F932 /* NSError+Stripe3DS2.h */, + 26C20D4B77372845627D6466 /* NSError+Stripe3DS2.m */, + E0949399E96663964091C0EF /* NSLayoutConstraint+LayoutSupport.h */, + F99B290CF2B4987A4CFE5DCE /* NSLayoutConstraint+LayoutSupport.m */, + A8A77DB1C711B8696B2E5FEF /* NSString+EmptyChecking.h */, + D8D309E4FBE7E73D0BB83BF5 /* NSString+EmptyChecking.m */, + 018F4AA25D84E9CBD2546BB5 /* NSString+JWEHelpers.h */, + 43CB8F416E4F4CEA34391481 /* NSString+JWEHelpers.m */, + D827473D1858B2BED85BDFA1 /* STDSACSNetworkingManager.h */, + 2D6004F2D9880055C0C0BCF1 /* STDSACSNetworkingManager.m */, + 138E82072FC6572612A8E248 /* STDSAuthenticationResponseObject.h */, + 5043E1CF6955176EC3D43F88 /* STDSAuthenticationResponseObject.m */, + 2CFAC8D74AFFC2E61BAD82A5 /* STDSBrandingView.h */, + F9C39A42ECBA58571EFA03C5 /* STDSBrandingView.m */, + A137A31BEFCCB646FB754EE2 /* STDSBundleLocator.h */, + 9564F43EF7CF4F60C3C202CD /* STDSBundleLocator.m */, + C893FD867C964775E68AE85F /* STDSChallengeInformationView.h */, + D24777EE9931075405F370DB /* STDSChallengeInformationView.m */, + 762DEC6662DF8240FE19CFFC /* STDSChallengeRequestParameters.h */, + 60693BC560C4927ACFD2E6C3 /* STDSChallengeRequestParameters.m */, + 315AEE1534F9D9974DC1674B /* STDSChallengeResponse.h */, + 10C45EE3433B731253E21E24 /* STDSChallengeResponseImage.h */, + 14F83E51E99D453DF9C2DDE1 /* STDSChallengeResponseImageObject.h */, + 3A9F828840A3B361842E38DE /* STDSChallengeResponseImageObject.m */, + AA8BE982004398CF8AAA7D1E /* STDSChallengeResponseMessageExtension.h */, + BAAC56EDE0AAD2E50E02E9AE /* STDSChallengeResponseMessageExtensionObject.h */, + 7D02FAFC403BC901931A629F /* STDSChallengeResponseMessageExtensionObject.m */, + EB1BDF12DBD6264C6199FFA7 /* STDSChallengeResponseObject.h */, + 24CE086326AF3DE336BF4F4C /* STDSChallengeResponseObject.m */, + 52EEBC7D74623E1A1D7E3219 /* STDSChallengeResponseSelectionInfo.h */, + 4CCA15E0819F9A9D89FEF9EA /* STDSChallengeResponseSelectionInfoObject.h */, + F046DE266BAAE3DA0AC43785 /* STDSChallengeResponseSelectionInfoObject.m */, + E45303DCFFDC390971E6E122 /* STDSChallengeResponseViewController.h */, + B064352BB2876CA7266C410A /* STDSChallengeResponseViewController.m */, + EB910E98EDD6D3E00802793C /* STDSChallengeSelectionView.h */, + A4D460893CB5DDE52AA0AB85 /* STDSChallengeSelectionView.m */, + D122FAE093BC4F649CC6E7AB /* STDSDebuggerChecker.h */, + C29F78CC37253B0D33FF86F2 /* STDSDebuggerChecker.m */, + 1FBE88F6E2C3153A8315EDC6 /* STDSDeviceInformation.h */, + 56B1A52548593FE78D801734 /* STDSDeviceInformation.m */, + 7A637DA708BC866C2903E0EA /* STDSDeviceInformationManager.h */, + 9580A82A59EB2B0103EDF47A /* STDSDeviceInformationManager.m */, + 172AF5C85EDBD92A1030E361 /* STDSDeviceInformationParameter.h */, + 25C7D18868DDADEF4CB6220C /* STDSDeviceInformationParameter.m */, + 4869D6E14F01EDB960CB3065 /* STDSDeviceInformationParameter+Private.h */, + 6AA43E7E90E4002DD56B7C3D /* STDSDirectoryServer.h */, + 069990DA025BE9F33602E96A /* STDSDirectoryServerCertificate.h */, + 9754FA4F3CC6AA921787916B /* STDSDirectoryServerCertificate.m */, + B015B937DD3657D62C61CE58 /* STDSDirectoryServerCertificate+Internal.h */, + 5049A72A6BEB558D8559A5EB /* STDSEllipticCurvePoint.h */, + 2EF5AA537586EF706E4056FD /* STDSEllipticCurvePoint.m */, + 20533E2C69C4B80BB33A4766 /* STDSEphemeralKeyPair.h */, + F9D521B45783D36C8E83F0EA /* STDSEphemeralKeyPair.m */, + 52D6E4F76CBA258163E57DF3 /* STDSEphemeralKeyPair+Testing.h */, + 5463EB1CC8E85BE3BD12DEFF /* STDSErrorMessage+Internal.h */, + 2CB591796654445B8434A501 /* STDSErrorMessage+Internal.m */, + 461F938CDE7ED6D06D9D2700 /* STDSException+Internal.h */, + 939C45AE068CF4F5BEB4C138 /* STDSExpandableInformationView.h */, + AE1BBF3A441B1F782B766F61 /* STDSExpandableInformationView.m */, + ECC649704CDDA50E73DB3C10 /* STDSImageLoader.h */, + A09A89BBE7360F93195641C9 /* STDSImageLoader.m */, + C02DF839637771684BC57B3A /* STDSIntegrityChecker.h */, + BB065D6900E95C5E90864C16 /* STDSIntegrityChecker.m */, + A3DFB001DB5BA7F1AFA7353A /* STDSIPAddress.h */, + CB00E968CF4DD07FB8BF2AAA /* STDSIPAddress.m */, + 94943BEAB94E949AF9830EFA /* STDSJailbreakChecker.h */, + 1BFBB92DBE97E16DE9D18B4A /* STDSJailbreakChecker.m */, + 76EA9DF8572C992E30BCF8A3 /* STDSJSONWebEncryption.h */, + 99392D3F702EC6BF7CE15291 /* STDSJSONWebEncryption.m */, + 3AA2E6DA60350400C5270E08 /* STDSJSONWebSignature.h */, + A1DC6B47C06B522B22EB19FF /* STDSJSONWebSignature.m */, + 41E0F638E776A7F3456BDA3E /* STDSLocalizedString.h */, + FAE7026135B5049B732CEDEB /* STDSOSVersionChecker.h */, + F363439353A6DD554FC44AC2 /* STDSOSVersionChecker.m */, + 95641ECBE1AE1CC198013405 /* STDSProcessingView.h */, + 7AC1C5F6F1311F50D9C37291 /* STDSProcessingView.m */, + E629F85B783E42244E37DFA9 /* STDSProgressViewController.h */, + 84967ED0D9B206B3F5AA4F02 /* STDSProgressViewController.m */, + C8F0E11AA6794BFB1715685E /* STDSSecTypeUtilities.h */, + CF3DED43BD4E0226BFB0759D /* STDSSecTypeUtilities.m */, + C4777F15603AC755362BD846 /* STDSSelectionButton.h */, + B458FCA7DFDBF75E62A43BA1 /* STDSSelectionButton.m */, + A0ADFB365AD2DE9E84873BB1 /* STDSSimulatorChecker.h */, + 14D60BDF245487FE3362BE07 /* STDSSimulatorChecker.m */, + 72DBE992B33F45C059EDB597 /* STDSSpacerView.h */, + 51E4A57094A671EB14380882 /* STDSSpacerView.m */, + B7AE3B4732D203134FE096FE /* STDSStackView.h */, + 1D6269F8B91341C387A911DB /* STDSStackView.m */, + C12AE31D386D4A193C00004B /* STDSSynchronousLocationManager.h */, + A5DBDDEB5C5DAE2EA8C95CC3 /* STDSSynchronousLocationManager.m */, + 82C41E6DC2CC247ECC17F2B9 /* STDSTextChallengeView.h */, + 04E6B6D3CAE363114723472F /* STDSTextChallengeView.m */, + 8AC648332A706809F1BDFD59 /* STDSThreeDSProtocolVersion.m */, + 47C29E9F3D2A62676B424CD1 /* STDSThreeDSProtocolVersion+Private.h */, + 8A9F5C315222AFCD14C9342F /* STDSTransaction+Private.h */, + 581EEE576D24047004C828DF /* STDSWebView.h */, + AA85BC717C46C1B95AF8C1E3 /* STDSWebView.m */, + 33CEADC8006E456FD82CE134 /* STDSWhitelistView.h */, + 615F1BD510661B38D6825128 /* STDSWhitelistView.m */, + 0F024DEAD6628A0229856656 /* Stripe3DS2-Bridging-Header.h */, + 319DD1C72B0D50F80083BA32 /* STDSVisionSupport.h */, + 4DD55BECC3FB85216FE46218 /* UIButton+CustomInitialization.h */, + AA29B74DD3963B1205AA1C0C /* UIButton+CustomInitialization.m */, + 4A567E08748C86FCE2A75FEA /* UIColor+DefaultColors.h */, + 869733153BCA6BE2EB7A452F /* UIColor+DefaultColors.m */, + 9A8B0001705400C661678617 /* UIColor+ThirteenSupport.h */, + 37ED28756222539D37554CC7 /* UIColor+ThirteenSupport.m */, + 1E844798B2733C8511AC080E /* UIFont+DefaultFonts.h */, + 634095EA97A4E48BF7CAA03F /* UIFont+DefaultFonts.m */, + 99CB2B66DBD356B5D222EDA9 /* UIView+LayoutSupport.h */, + 8B0F0485B7048DDAA7DA8F9B /* UIView+LayoutSupport.m */, + 8F72413F99F8E50FD0FA085D /* UIViewController+Stripe3DS2.h */, + 99451064136843047C7881AD /* UIViewController+Stripe3DS2.m */, + 31CDFC312BA8E58100B3DD91 /* PrivacyInfo.xcprivacy */, + ); + path = Stripe3DS2; + sourceTree = ""; + }; + 18B6E1F5666C797A11E2BE03 = { + isa = PBXGroup; + children = ( + EB6FA022FDF5831C6D162E11 /* Project */, + 280289821F799A8274082E14 /* Frameworks */, + 398BBCD5E4B06CA5006CCC40 /* Products */, + ); + sourceTree = ""; + }; + 20B0FECAB4F18569649603E9 /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + 3A8CD68A006F970DDC54469A /* Project-Debug.xcconfig */, + 4DB91F58CF23E72636C69C41 /* Project-Release.xcconfig */, + A9CD7EC589C5CC6CE5CF9310 /* Stripe3DS2-Debug.xcconfig */, + F8F6FFED1A0966A49B78F252 /* Stripe3DS2-Release.xcconfig */, + 5316D0759F7F2ED0BF1D3B2E /* Stripe3DS2DemoUI-Debug.xcconfig */, + 6940D75CA36F7DB9FB56196B /* Stripe3DS2DemoUI-Release.xcconfig */, + 0E3C05BE55CF3086738AA182 /* Stripe3DS2DemoUITests-Debug.xcconfig */, + D7A9D36FEF7E97CF735D82B8 /* Stripe3DS2DemoUITests-Release.xcconfig */, + F2BC7014D26158AC9D74CC67 /* Stripe3DS2Tests-Debug.xcconfig */, + F5686FB8EFA3AE3B39F85BBC /* Stripe3DS2Tests-Release.xcconfig */, + ); + path = BuildConfigurations; + sourceTree = ""; + }; + 280289821F799A8274082E14 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 08D577934984712C20C5E903 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 398BBCD5E4B06CA5006CCC40 /* Products */ = { + isa = PBXGroup; + children = ( + CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */, + 02D762271DBCC1AE80E0F9D4 /* Stripe3DS2DemoUI.app */, + AA8A113E655E23381CB3BDA4 /* Stripe3DS2DemoUITests.xctest */, + 4EB746D4E46ABD2452A154AE /* Stripe3DS2Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 47CD3B0E4EAA05F75DA05252 /* CertificateFiles */ = { + isa = PBXGroup; + children = ( + D8B7EE0F935EF44EB2EF7D45 /* amex.der */, + 83838563F29179FB1E9F2E66 /* cartes-bancaires.der */, + 04AB48F014A8D5BA56FEF463 /* discover.der */, + 3E4C02D9D6978AD5C3D7E3D2 /* ec_test.der */, + 1104D5855D28E49E104CA004 /* mastercard.der */, + B6275E7099E55F0C04688890 /* ul-test.der */, + A70071543985284E0D9DAC64 /* visa.der */, + ); + path = CertificateFiles; + sourceTree = ""; + }; + 79E03C9F2FB6092200EE737F /* Stripe3DS2Tests */ = { + isa = PBXGroup; + children = ( + AD446D5D3D8D07D491373E24 /* JSON */, + 23A3DDCE911F8C43CF74CDF4 /* Info.plist */, + 5EE8BF51F49E161737406B3A /* NSDictionary+DecodingHelpersTest.m */, + 57B8822D21AADE7DA51931CC /* NSString+EmptyCheckingTests.m */, + 6118F98576E9A39A7292C6C6 /* STDSACSNetworkingManagerTest.m */, + 969DE2FA3845BB7939D3CD2E /* STDSAuthenticationRequestParametersTest.m */, + 28E32DA72CAFBF2AF8099151 /* STDSAuthenticationResponseTests.m */, + 4B1E3760B1369DB1A33C9DFE /* STDSBase64URLEncodingTests.m */, + 89ED2133B61092FE1B478204 /* STDSChallengeParametersTests.m */, + 4EAD29E57D206B5D6BAF9099 /* STDSChallengeRequestParametersTest.m */, + 5E401845EE5759CCD30786AA /* STDSChallengeResponseObjectTest.m */, + 5F551808E65CDADD9B3A9CB8 /* STDSConfigParametersTests.m */, + A4050F381183D6FDCC990D01 /* STDSDeviceInformationManagerTests.m */, + FC52C3C844BDA981EE34BAB9 /* STDSDeviceInformationParameterTests.m */, + 6AA75957DD13FA92FA866AF3 /* STDSDirectoryServerCertificateTests.m */, + 0DA1EE83D91993BCAF13EC72 /* STDSEllipticCurvePointTests.m */, + BF80634E06E8D6F598CFC667 /* STDSEphemeralKeyPairTests.m */, + 278F2C40987F5218BD031E3D /* STDSErrorMessageTest.m */, + 6B9B49A5CF64DCB23CD0AEDE /* STDSJSONEncoderTest.m */, + 97505E93BE83F233E69BB5A9 /* STDSJSONWebEncryptionTests.m */, + E4C668442518B446D573AD24 /* STDSJSONWebSignatureTests.m */, + 30D22985091381DFCDF077E9 /* STDSSecTypeUtilitiesTests.m */, + F02456AB660E709F332C0C7D /* STDSSynchronousLocationManagerTests.m */, + 5FF3235EDD908CC1500399A1 /* STDSTestJSONUtils.m */, + 7FF8ACC073E814B7DEBA7647 /* STDSThreeDS2ServiceTests.m */, + A4A3CCBE9A2E8C1A73D8214E /* STDSTransactionTest.m */, + CB3154597040B803ADE14F9E /* STDSUICustomizationTests.m */, + 474F39AA3F47D84F8D54E5B7 /* STDSWarningTests.m */, + ); + path = Stripe3DS2Tests; + sourceTree = ""; + }; + 82BC8EB365BF993731432954 /* Sources */ = { + isa = PBXGroup; + children = ( + A4620559FBB8A42E15044E32 /* AppDelegate.h */, + 8B59CC3349FD9BBF37624667 /* AppDelegate.m */, + 3C22D410C297E2C3AFE727A6 /* main.m */, + 9F5C194C81AF1F5578698A27 /* STDSChallengeResponseObject+TestObjects.h */, + 99DB24A9B44979EA19347A76 /* STDSChallengeResponseObject+TestObjects.m */, + 1F0DADEABA0B043A6CA36DD6 /* STDSDemoViewController.h */, + 382C583385FAECA91366769E /* STDSDemoViewController.m */, + ); + path = Sources; + sourceTree = ""; + }; + 8CF6AFFC9FD5638E0D1E988E /* Stripe3DS2DemoUI */ = { + isa = PBXGroup; + children = ( + 98160E2F8248A228E5EC73F8 /* Resources */, + 82BC8EB365BF993731432954 /* Sources */, + 037C294011A01E4B4DCE5BB7 /* Info.plist */, + ); + path = Stripe3DS2DemoUI; + sourceTree = ""; + }; + 98160E2F8248A228E5EC73F8 /* Resources */ = { + isa = PBXGroup; + children = ( + 310E6B4E2C52C9EF006A03EC /* 150-issuer.png */, + 310E6B502C52C9EF006A03EC /* 150-payment.png */, + 310E6B4F2C52C9EF006A03EC /* 300-issuer.png */, + 310E6B512C52C9EF006A03EC /* 300-payment.png */, + 310E6B522C52C9EF006A03EC /* 450-issuer.png */, + 310E6B532C52C9EF006A03EC /* 450-payment.png */, + 7971EB56716D5E2F59053B6D /* acs_challenge.html */, + ); + path = Resources; + sourceTree = ""; + }; + A3DDF4E193616FA951B1B120 /* Resources */ = { + isa = PBXGroup; + children = ( + 47CD3B0E4EAA05F75DA05252 /* CertificateFiles */, + F31A6580385C6390910AFD93 /* Localizable.strings */, + 9E1CB633CA5917B2168BB928 /* Stripe3DS2.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + AD446D5D3D8D07D491373E24 /* JSON */ = { + isa = PBXGroup; + children = ( + 237977B021FE538E5E4FA35C /* ARes.json */, + 8C6D262891811FB3FE0C947E /* CRes.json */, + E12798864D5CFFB7157D4CF5 /* ErrorMessage.json */, + ); + path = JSON; + sourceTree = ""; + }; + C169A9439FD628D620B218AA /* Stripe3DS2DemoUITests */ = { + isa = PBXGroup; + children = ( + 00658E077ECB223C90484AF7 /* Info.plist */, + 466DF80D33CEC20DF4E844A9 /* STDSChallengeResponseViewControllerSnapshotTests.m */, + ); + path = Stripe3DS2DemoUITests; + sourceTree = ""; + }; + E4F3BE80D5F61EAC1694F954 /* include */ = { + isa = PBXGroup; + children = ( + 1F1FD11407319293954C6D81 /* STDSAlreadyInitializedException.h */, + 27AC486DA2A80A43C1F517B2 /* STDSAlreadyInitializedException.m */, + C91B239583899192C7B26FCF /* STDSAuthenticationRequestParameters.h */, + AB871F64FC72EBC7A91F96C1 /* STDSAuthenticationRequestParameters.m */, + 4EA4AB3BA2FB41B3D5F38978 /* STDSAuthenticationResponse.h */, + BD94CAE01A17E083E5617E56 /* STDSButtonCustomization.h */, + 5489DCAA65624C6F966816B6 /* STDSButtonCustomization.m */, + D03B98D5861739833B197D8F /* STDSChallengeParameters.h */, + 23C929BF760735CC01E1E8F5 /* STDSChallengeParameters.m */, + B7A75140FB62A71261CAF5EC /* STDSChallengeStatusReceiver.h */, + 10DB3CD8F2541AC06681F2C8 /* STDSCompletionEvent.h */, + 3B6B71C61CF5ADC7796441F5 /* STDSCompletionEvent.m */, + A1BFB8640032796CDA016765 /* STDSConfigParameters.h */, + 89B5ED899439D1C1054CB448 /* STDSConfigParameters.m */, + AFF187D1D58405C736474042 /* STDSCustomization.h */, + A8E62EC973FC5C9905A40CA8 /* STDSCustomization.m */, + 645D0BB927B7F8A0D37EE04D /* STDSErrorMessage.h */, + 6DDB6222A2EF31FDB7592368 /* STDSErrorMessage.m */, + B71A1C110DCC23A9CE929837 /* STDSException.h */, + 6F030410FDABFF833CD8FE46 /* STDSException.m */, + BA96C9FFA48B85F9C42E8C68 /* STDSFooterCustomization.h */, + 6CFF141AC98F213122D037C6 /* STDSFooterCustomization.m */, + 853A361AB630A2BD402D1B47 /* STDSInvalidInputException.h */, + 6FDAC320458399141EB161E2 /* STDSInvalidInputException.m */, + B219360BFB325779485BF702 /* STDSJSONDecodable.h */, + E6A3C5D4C46E681B7D76B2BA /* STDSJSONEncodable.h */, + 06C5207432FB07BD71703BCC /* STDSJSONEncoder.h */, + 0681C043F732148587737233 /* STDSJSONEncoder.m */, + 6932DD26C7C16938DFA0E198 /* STDSLabelCustomization.h */, + 01D320CD5ADA49F17B8BC1B0 /* STDSLabelCustomization.m */, + E1630D876436E43547FF69CE /* STDSNavigationBarCustomization.h */, + E9F1A5A8E5922748A9BEC724 /* STDSNavigationBarCustomization.m */, + 2FCABBF51CBA7F9FEF757B4C /* STDSNotInitializedException.h */, + B7BD1E24EA9427121E148DFC /* STDSNotInitializedException.m */, + 3A6D20D1DAB7D769E5E97528 /* STDSProtocolErrorEvent.h */, + 3255706D2304FB551C97305F /* STDSProtocolErrorEvent.m */, + DE7D85369E012B15702D8DF9 /* STDSRuntimeErrorEvent.h */, + 1501A7A3010FFE53D00E318F /* STDSRuntimeErrorEvent.m */, + B8619CA38E2A5B49DBF8546B /* STDSRuntimeException.h */, + CD33421F13A675DCFE597FFB /* STDSRuntimeException.m */, + 7CD753D9BBFC378C1B491B00 /* STDSSelectionCustomization.h */, + 43540AB7D13C0C9A563C3F4D /* STDSSelectionCustomization.m */, + AE53BAA72835AFA3B35C58D3 /* STDSStripe3DS2Error.h */, + 9F4376BB07FD1EE783249AED /* STDSStripe3DS2Error.m */, + E5BC34A661D9080A2EE239F8 /* STDSSwiftTryCatch.h */, + 9946D28A86E85A6C84EB6C7B /* STDSSwiftTryCatch.m */, + 68ED2A0E11E1B27980E0C60A /* STDSTextFieldCustomization.h */, + 97163B5E9598CA3A298920B9 /* STDSTextFieldCustomization.m */, + 5EAA444FA7C82BDA4AD907BF /* STDSThreeDS2Service.h */, + 77646CEC7EE754F47EDBDA4F /* STDSThreeDS2Service.m */, + 5E6A9565E97DF2544254AA94 /* STDSThreeDSProtocolVersion.h */, + F40CC91AA69F5296B0AA985C /* STDSTransaction.h */, + 5FDE2481364C454058BF2377 /* STDSTransaction.m */, + 210B22FF4DCB0C6E7C763EAB /* STDSUICustomization.h */, + A55B2474CD1A39D70869B75B /* STDSUICustomization.m */, + 0BEB5006261C5E070FEE62BF /* STDSWarning.h */, + A502CCC5190F6B642EEC0CE6 /* STDSWarning.m */, + 28C6B4F6BA431B9342970FD6 /* Stripe3DS2.h */, + ); + path = include; + sourceTree = ""; + }; + EB6FA022FDF5831C6D162E11 /* Project */ = { + isa = PBXGroup; + children = ( + 20B0FECAB4F18569649603E9 /* BuildConfigurations */, + 148D1BA6A2E8B2B60CA9951F /* Stripe3DS2 */, + 8CF6AFFC9FD5638E0D1E988E /* Stripe3DS2DemoUI */, + C169A9439FD628D620B218AA /* Stripe3DS2DemoUITests */, + 79E03C9F2FB6092200EE737F /* Stripe3DS2Tests */, + ); + name = Project; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A8117E5A795D5342E5AEF24C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3192FCC841152A7E5E6F19D6 /* STDSAlreadyInitializedException.h in Headers */, + 9673946B78A7763070E05CD0 /* STDSAuthenticationRequestParameters.h in Headers */, + 435EFC9119D4B42B2C6A6F88 /* STDSAuthenticationResponse.h in Headers */, + EC51189277D7D68D08C2A65C /* STDSButtonCustomization.h in Headers */, + DC0D9A57F2E312663C088FB8 /* STDSChallengeParameters.h in Headers */, + 72F430506E684D61BD5CFE3B /* STDSChallengeStatusReceiver.h in Headers */, + 8E27285FE76F8BECFD70286D /* STDSCompletionEvent.h in Headers */, + DC32281BF0847A98B100C3A1 /* STDSConfigParameters.h in Headers */, + F04482C663F6DF8E522B0833 /* STDSCustomization.h in Headers */, + 2BBA82878804711E06CBFCCB /* STDSErrorMessage.h in Headers */, + 24D49F833E1ED28DE557F219 /* STDSException.h in Headers */, + 79557307ECD48D17C566997A /* STDSFooterCustomization.h in Headers */, + 4E35267801D94FB84816C519 /* STDSInvalidInputException.h in Headers */, + 9B32E9A4CD2BDA61C730F5EB /* STDSJSONDecodable.h in Headers */, + 890E22971E6F7B0FA1C8D649 /* STDSJSONEncodable.h in Headers */, + 2224463CE5FB99D4BEE6A479 /* STDSJSONEncoder.h in Headers */, + 0BA0F0CADC4CFF419E1F7EFC /* STDSLabelCustomization.h in Headers */, + 3B430F172A3AD6AA9AFDFC04 /* STDSNavigationBarCustomization.h in Headers */, + 2CB6DF5CCCBC1EB9ABD03143 /* STDSNotInitializedException.h in Headers */, + 67D431374DECE36B2606D944 /* STDSProtocolErrorEvent.h in Headers */, + 15D4A2F07EA477B84CD48B15 /* STDSRuntimeErrorEvent.h in Headers */, + 2C6DB6516699B4EFADDF3360 /* STDSRuntimeException.h in Headers */, + F5513045A25210C524A05DF5 /* STDSSelectionCustomization.h in Headers */, + 335553A547A3CA80B3901CCF /* STDSStripe3DS2Error.h in Headers */, + DE32EAA8F5C2645652FCFB48 /* STDSSwiftTryCatch.h in Headers */, + 4C267E9A30ADC18E71408ED5 /* STDSTextFieldCustomization.h in Headers */, + 31C636A25BF83316EE7EB57D /* STDSThreeDS2Service.h in Headers */, + 847926A68A88BEB58C92D9BC /* STDSThreeDSProtocolVersion.h in Headers */, + 9DC5612697835A383DC6606E /* STDSTransaction.h in Headers */, + 0615DD02C0B022AE207DADF0 /* STDSUICustomization.h in Headers */, + 9F0F6085FBD892BF5F14CF86 /* STDSWarning.h in Headers */, + 893713F4464A88FAB92BC2C6 /* Stripe3DS2.h in Headers */, + 425849564519D6454DDA3424 /* NSData+JWEHelpers.h in Headers */, + 1134F81C7D015BA5EA52BFD1 /* NSDictionary+DecodingHelpers.h in Headers */, + 3617B07ABD719F1A5F8302FA /* NSError+Stripe3DS2.h in Headers */, + 28AD7EC6F6DA2AECD7F61BA7 /* NSLayoutConstraint+LayoutSupport.h in Headers */, + 30B25163BEDEB99CDD101B5F /* NSString+EmptyChecking.h in Headers */, + A5193DADCD0F48911F775D88 /* NSString+JWEHelpers.h in Headers */, + A78F19DA3D146A258FB9DE3C /* STDSACSNetworkingManager.h in Headers */, + 2B2787A7103C0D0AB5F00B78 /* STDSAuthenticationResponseObject.h in Headers */, + 458BC5A3D0E7B4D05549474F /* STDSBrandingView.h in Headers */, + 97CE8B57A97D037E977E62F3 /* STDSBundleLocator.h in Headers */, + F13A032A4AF15BB61EA18A8D /* STDSChallengeInformationView.h in Headers */, + BF342613EE89845EB1C4B72A /* STDSChallengeRequestParameters.h in Headers */, + 7337FB20DC0EF491B5922913 /* STDSChallengeResponse.h in Headers */, + 95A2D58051AADDBE16CCA0B6 /* STDSChallengeResponseImage.h in Headers */, + D819C341ECEE1EFE6FE66085 /* STDSChallengeResponseImageObject.h in Headers */, + 793D61AD55630DD336A141A7 /* STDSChallengeResponseMessageExtension.h in Headers */, + 48316C1AB1D9151C205A59A4 /* STDSChallengeResponseMessageExtensionObject.h in Headers */, + E90749131EDA0C86BBBFCE5C /* STDSChallengeResponseObject.h in Headers */, + 3C8C3BCDDDDEF6B9E150D4E1 /* STDSChallengeResponseSelectionInfo.h in Headers */, + 9C225B9C556C20D1A6F97180 /* STDSChallengeResponseSelectionInfoObject.h in Headers */, + 9E46777893FA2D6691F496D7 /* STDSChallengeResponseViewController.h in Headers */, + E7E424C5264A4DE4A1EC19B6 /* STDSChallengeSelectionView.h in Headers */, + F28AD6CA8AB1A5DEE7A8F429 /* STDSDebuggerChecker.h in Headers */, + 57E58A3B88D9EEB9FAE9B383 /* STDSDeviceInformation.h in Headers */, + 9843F6AABF7B0A623E7DF979 /* STDSDeviceInformationManager.h in Headers */, + 3844F0E21742F43BCB32499A /* STDSDeviceInformationParameter+Private.h in Headers */, + 2938D9CC41899DC78D01EF9F /* STDSDeviceInformationParameter.h in Headers */, + C4BA75544A7F51355BF4B5F8 /* STDSDirectoryServer.h in Headers */, + DB96817598FCE73F5131437E /* STDSDirectoryServerCertificate+Internal.h in Headers */, + 63AFB3C739DD987EE56F801F /* STDSDirectoryServerCertificate.h in Headers */, + 5C83A3A4631E51EEECBEAA0F /* STDSEllipticCurvePoint.h in Headers */, + 3343DA94A5032627843A343C /* STDSEphemeralKeyPair+Testing.h in Headers */, + B94E5A5BF5F22998E4CA9ED3 /* STDSEphemeralKeyPair.h in Headers */, + 34256504F0E0E519D586B7CE /* STDSErrorMessage+Internal.h in Headers */, + 149FE0C2910F08910B250BD8 /* STDSException+Internal.h in Headers */, + F35963DB86D90320CFA7BCB9 /* STDSExpandableInformationView.h in Headers */, + DDB98914E1135E8D445545C8 /* STDSIPAddress.h in Headers */, + 613EF855524F7861A6DDD8C1 /* STDSImageLoader.h in Headers */, + D3432C5BF5211A60D5C15D9E /* STDSIntegrityChecker.h in Headers */, + 319DD1C82B0D50FF0083BA32 /* STDSVisionSupport.h in Headers */, + 4C573A44AB0A9CB28D76C621 /* STDSJSONWebEncryption.h in Headers */, + CA617B6F5CDFB8FDEEE38636 /* STDSJSONWebSignature.h in Headers */, + 7C7073B54628ED836F422F1D /* STDSJailbreakChecker.h in Headers */, + 8E0501C3BB849C2D2D99F34E /* STDSLocalizedString.h in Headers */, + 68F314D63323954EFE309E49 /* STDSOSVersionChecker.h in Headers */, + 09F0B1945CC12FB1215119FD /* STDSProcessingView.h in Headers */, + 511513A9180F9835B08CBE56 /* STDSProgressViewController.h in Headers */, + CEA1EB91858043A837638FE0 /* STDSSecTypeUtilities.h in Headers */, + D9F8BA88953A91DC082FA6DA /* STDSSelectionButton.h in Headers */, + AC474013841CE1DED97F3FA8 /* STDSSimulatorChecker.h in Headers */, + AA94519687902E34E5243AEA /* STDSSpacerView.h in Headers */, + 206B93BDE91CFED74A053B87 /* STDSStackView.h in Headers */, + F0B9EFD359B1AB28A695CB48 /* STDSSynchronousLocationManager.h in Headers */, + 59756023104E4FB886B48936 /* STDSTextChallengeView.h in Headers */, + 326AD1DD7BB8A2A6DA66EC67 /* STDSThreeDSProtocolVersion+Private.h in Headers */, + AE1894FEA510AC394DE73C4B /* STDSTransaction+Private.h in Headers */, + 7EC8E5523F29B0F01FE827DB /* STDSWebView.h in Headers */, + 825ACBE0734BDCE746E66AB0 /* STDSWhitelistView.h in Headers */, + 75CDFDABD7F9813563E3F618 /* Stripe3DS2-Bridging-Header.h in Headers */, + 0A2BC6A9E388B242C46C09D9 /* UIButton+CustomInitialization.h in Headers */, + 8322973CD09F2E1C88B6046E /* UIColor+DefaultColors.h in Headers */, + 27F3EA1BB7E7B56A1A8524BA /* UIColor+ThirteenSupport.h in Headers */, + 8E833B7FCBC3A99072582FEA /* UIFont+DefaultFonts.h in Headers */, + A026717FFF299A7C1C51565F /* UIView+LayoutSupport.h in Headers */, + 41246FCC0695BABE1489C53E /* UIViewController+Stripe3DS2.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 57AEC53510AE0DC0539730F3 /* Stripe3DS2 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5C9BBB6FD4175090CFCAA126 /* Build configuration list for PBXNativeTarget "Stripe3DS2" */; + buildPhases = ( + A8117E5A795D5342E5AEF24C /* Headers */, + 559A957A4AA29D178C3E3D8F /* Sources */, + 563147EAF54FC4A425A06D46 /* Resources */, + 726DD7176204D90BD7552B84 /* Embed Frameworks */, + C65BFA70E847549921E39F4E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Stripe3DS2; + productName = Stripe3DS2; + productReference = CE4030C50383B488C97F4B56 /* Stripe3DS2.framework */; + productType = "com.apple.product-type.framework"; + }; + 5907C55B1F111921112DF2BF /* Stripe3DS2DemoUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 37237B2A5629D2257C454AD1 /* Build configuration list for PBXNativeTarget "Stripe3DS2DemoUI" */; + buildPhases = ( + 5D618F152EF19018F5C7E579 /* Sources */, + C4175027D57AF53025CB9719 /* Resources */, + 00E415680C602ED1D6D8E71F /* Embed Frameworks */, + 8ECF69FED0DDA991C821CF6A /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C7515969CD69027C276886AB /* PBXTargetDependency */, + ); + name = Stripe3DS2DemoUI; + productName = Stripe3DS2DemoUI; + productReference = 02D762271DBCC1AE80E0F9D4 /* Stripe3DS2DemoUI.app */; + productType = "com.apple.product-type.application"; + }; + 7DA168BC86CE957505FA091B /* Stripe3DS2DemoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5F64A3CED17753DA2643AA63 /* Build configuration list for PBXNativeTarget "Stripe3DS2DemoUITests" */; + buildPhases = ( + 7A43CA17E1E2D826DCBDD88D /* Sources */, + F3AE035E64AB50AB8B90A58E /* Resources */, + 58FA2FE74AE67CD3CDAFD7E5 /* Embed Frameworks */, + A56B40D43D552FDE77670CB5 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 0AE2FB81071258D69C9FCAD4 /* PBXTargetDependency */, + 026B55E92602E24EE7139E1F /* PBXTargetDependency */, + ); + name = Stripe3DS2DemoUITests; + packageProductDependencies = ( + 0118969F6608A92583CC6C98 /* iOSSnapshotTestCase */, + ); + productName = Stripe3DS2DemoUITests; + productReference = AA8A113E655E23381CB3BDA4 /* Stripe3DS2DemoUITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F2D50FA32F27498F56CD08DD /* Stripe3DS2Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 271DFDD5ABBB719F3BD1AB56 /* Build configuration list for PBXNativeTarget "Stripe3DS2Tests" */; + buildPhases = ( + 0D8CCC419762E2FD4F945EB0 /* Sources */, + 4FDEAB55B960B360B76B3F41 /* Resources */, + 43B749EC915FB6D2FF5ABEE0 /* Embed Frameworks */, + 16B0A41D6C0F157DDF229397 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 798525809F1F67D0C6634927 /* PBXTargetDependency */, + ); + name = Stripe3DS2Tests; + productName = Stripe3DS2Tests; + productReference = 4EB746D4E46ABD2452A154AE /* Stripe3DS2Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8D219187B704575AD3CD3EC5 /* Project object */ = { + isa = PBXProject; + attributes = { + TargetAttributes = { + 7DA168BC86CE957505FA091B = { + TestTargetID = 5907C55B1F111921112DF2BF; + }; + }; + }; + buildConfigurationList = 57B08C966701355644CF5CEB /* Build configuration list for PBXProject "Stripe3DS2" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + "bg-BG", + "ca-ES", + "cs-CZ", + da, + de, + "el-GR", + en, + "en-GB", + es, + "es-419", + "et-EE", + fi, + fil, + fr, + "fr-CA", + hr, + hu, + id, + it, + ja, + ko, + "lt-LT", + "lv-LV", + "ms-MY", + mt, + nb, + nl, + "nn-NO", + "pl-PL", + "pt-BR", + "pt-PT", + "ro-RO", + ru, + "sk-SK", + "sl-SI", + sv, + tr, + vi, + "zh-HK", + "zh-Hans", + "zh-Hant", + ); + mainGroup = 18B6E1F5666C797A11E2BE03; + packageReferences = ( + 176A29DF97FAFE939C7F667B /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */, + ); + productRefGroup = 398BBCD5E4B06CA5006CCC40 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 57AEC53510AE0DC0539730F3 /* Stripe3DS2 */, + F2D50FA32F27498F56CD08DD /* Stripe3DS2Tests */, + 5907C55B1F111921112DF2BF /* Stripe3DS2DemoUI */, + 7DA168BC86CE957505FA091B /* Stripe3DS2DemoUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4FDEAB55B960B360B76B3F41 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B16918BF5F67B0F5FB1AFD5 /* ARes.json in Resources */, + F4E8353A470750D9BCEA3CD8 /* CRes.json in Resources */, + E7A72BB7CC8DE8DA7FA0DEC5 /* ErrorMessage.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 563147EAF54FC4A425A06D46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A97E1DF84D381952A0FB4C1C /* amex.der in Resources */, + EA89021709AFB5F04961EB78 /* cartes-bancaires.der in Resources */, + 3D674B4EE5ABACD65EB21A1E /* discover.der in Resources */, + FD6456BD86F07C446B9FB6C8 /* ec_test.der in Resources */, + 081347606D48F64161D95B45 /* mastercard.der in Resources */, + 81B1C12449852CEB3C59793F /* ul-test.der in Resources */, + 125427F1322501AA12EAEBE6 /* visa.der in Resources */, + 2D42EA44FC01C834209CFED4 /* Stripe3DS2.xcassets in Resources */, + 31CDFC322BA8E58100B3DD91 /* PrivacyInfo.xcprivacy in Resources */, + 6BA3F565B6C0B78F6DED8826 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C4175027D57AF53025CB9719 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C20B044ACFD3A50FD4AC9A8 /* acs_challenge.html in Resources */, + 310E6B542C52C9EF006A03EC /* 150-issuer.png in Resources */, + 310E6B592C52C9EF006A03EC /* 450-payment.png in Resources */, + 310E6B552C52C9EF006A03EC /* 300-issuer.png in Resources */, + 310E6B572C52C9EF006A03EC /* 300-payment.png in Resources */, + 310E6B582C52C9EF006A03EC /* 450-issuer.png in Resources */, + 310E6B562C52C9EF006A03EC /* 150-payment.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F3AE035E64AB50AB8B90A58E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0D8CCC419762E2FD4F945EB0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B426123CC7FCA337E13304F9 /* NSDictionary+DecodingHelpersTest.m in Sources */, + E73028FD4537F6E48F927127 /* NSString+EmptyCheckingTests.m in Sources */, + D1427392DA8AD8F5B4118781 /* STDSACSNetworkingManagerTest.m in Sources */, + A33A549CF12209280E3B1DBA /* STDSAuthenticationRequestParametersTest.m in Sources */, + 5DAC25EBAB19555229654E44 /* STDSAuthenticationResponseTests.m in Sources */, + A56AE2CD941D7ADAC7FC8BCC /* STDSBase64URLEncodingTests.m in Sources */, + D63E3DDC92DD46062A265D86 /* STDSChallengeParametersTests.m in Sources */, + 4A9423CC79312BC3A0DEC38B /* STDSChallengeRequestParametersTest.m in Sources */, + ECF8755D2602E0E06DCB3210 /* STDSChallengeResponseObjectTest.m in Sources */, + 32BE38D04DD76CA4490389C6 /* STDSConfigParametersTests.m in Sources */, + B1C2AA6259CE34B0042A1FB1 /* STDSDeviceInformationManagerTests.m in Sources */, + 2B09B7FDF8C4AA551F7EE18E /* STDSDeviceInformationParameterTests.m in Sources */, + E4DA2CE8DC8C60DE40222185 /* STDSDirectoryServerCertificateTests.m in Sources */, + 9F52F67A1A3A2199AEA598FC /* STDSEllipticCurvePointTests.m in Sources */, + BFC5852E14724750E70DA2A6 /* STDSEphemeralKeyPairTests.m in Sources */, + 1767509FDC216602A5E43F0E /* STDSErrorMessageTest.m in Sources */, + D189220981C7705913D93687 /* STDSJSONEncoderTest.m in Sources */, + 98075195759ABB91B0D19847 /* STDSJSONWebEncryptionTests.m in Sources */, + 4C0E13298F08D391FE8600CF /* STDSJSONWebSignatureTests.m in Sources */, + 480CD62EC91F4796D1D8D8DC /* STDSSecTypeUtilitiesTests.m in Sources */, + BA65731CDB75C3124834586C /* STDSSynchronousLocationManagerTests.m in Sources */, + EA19A7AEC298F3EC010D1199 /* STDSTestJSONUtils.m in Sources */, + D927789F89B413C08DE4D388 /* STDSThreeDS2ServiceTests.m in Sources */, + 9B7081D378D1441B98B81207 /* STDSTransactionTest.m in Sources */, + 83878337274D8C43D492EC45 /* STDSUICustomizationTests.m in Sources */, + 30632F7C730BF8D23B607CF7 /* STDSWarningTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 559A957A4AA29D178C3E3D8F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A8CAD8276ED8057A760147CB /* NSData+JWEHelpers.m in Sources */, + 0D4B42EC5019D213BDBC84E7 /* NSDictionary+DecodingHelpers.m in Sources */, + 089DAFECA444861762781974 /* NSError+Stripe3DS2.m in Sources */, + 9ED9C602C10682CD5DD91C12 /* NSLayoutConstraint+LayoutSupport.m in Sources */, + DEEF260365A4D9B0D83E557D /* NSString+EmptyChecking.m in Sources */, + 88197445522F39DB1E9F6AE7 /* NSString+JWEHelpers.m in Sources */, + B31168F5FEC3464AC212DB85 /* STDSACSNetworkingManager.m in Sources */, + 5FE5FB933E2651C8D84FA477 /* STDSAuthenticationResponseObject.m in Sources */, + 75150C4678B8207AA6E7D969 /* STDSBrandingView.m in Sources */, + 8233B9F131602DD7143FF96F /* STDSBundleLocator.m in Sources */, + 14BDBD98F8E15576403255C9 /* STDSChallengeInformationView.m in Sources */, + 2970D4880EBFAEFABA2E8EBD /* STDSChallengeRequestParameters.m in Sources */, + 2AB06EC20B22306F1DDF9F4B /* STDSChallengeResponseImageObject.m in Sources */, + B9A456E52E62E9DAE9F424A7 /* STDSChallengeResponseMessageExtensionObject.m in Sources */, + 09DFAF7B38EAB76BC66AC9C8 /* STDSChallengeResponseObject.m in Sources */, + AA4D157F31A246890D446981 /* STDSChallengeResponseSelectionInfoObject.m in Sources */, + CCC376FE7424029342A8D15E /* STDSChallengeResponseViewController.m in Sources */, + 4FE8C076102C195D609977F5 /* STDSChallengeSelectionView.m in Sources */, + A6909D6C1A68963504733939 /* STDSDebuggerChecker.m in Sources */, + E630DCBBEFAAD0435BEF989C /* STDSDeviceInformation.m in Sources */, + 3F2624C33291FCE00D596768 /* STDSDeviceInformationManager.m in Sources */, + 0EEFDE04392FD017EA0A017A /* STDSDeviceInformationParameter.m in Sources */, + 9F9BB9E7FA18FEDA44F7042E /* STDSDirectoryServerCertificate.m in Sources */, + 6B3AA85EE71FC42EF9BD999F /* STDSEllipticCurvePoint.m in Sources */, + 05647ABCFBDBE1209D5044AC /* STDSEphemeralKeyPair.m in Sources */, + 2E2022723BC7A4B2DD5ECE76 /* STDSErrorMessage+Internal.m in Sources */, + 4142A3AB3E384F91A1E5550B /* STDSExpandableInformationView.m in Sources */, + 9146B2E3B89EF3F489D7E233 /* STDSIPAddress.m in Sources */, + 136255493F3F0E930FBA8606 /* STDSImageLoader.m in Sources */, + EB791827A943AA61976D8C29 /* STDSIntegrityChecker.m in Sources */, + 955CBE0C979291F89567D8A7 /* STDSJSONWebEncryption.m in Sources */, + EFC72E766F9FD4EE18D309BC /* STDSJSONWebSignature.m in Sources */, + BA00D11D792C99B7D5749F59 /* STDSJailbreakChecker.m in Sources */, + 602C526C0B52DF81846DA664 /* STDSOSVersionChecker.m in Sources */, + 574A7976213046F02F7F60C4 /* STDSProcessingView.m in Sources */, + 1156B25EBE0135B627E174D2 /* STDSProgressViewController.m in Sources */, + DBDB8B56FFF5C4234705E698 /* STDSSecTypeUtilities.m in Sources */, + D41F091BE1579B801D1BBC20 /* STDSSelectionButton.m in Sources */, + 4518AD83580FC222B883DAFB /* STDSSimulatorChecker.m in Sources */, + 3F30A8F418870D176A626F00 /* STDSSpacerView.m in Sources */, + 3909D8028AC485BCCF17B48B /* STDSStackView.m in Sources */, + E0644CE0260178023E643B23 /* STDSSynchronousLocationManager.m in Sources */, + D5706ACF84F0A993CE1885D4 /* STDSTextChallengeView.m in Sources */, + 1B20B454D0C309613A1FAF68 /* STDSThreeDSProtocolVersion.m in Sources */, + 884D59AF7FD70889276AFC02 /* STDSWebView.m in Sources */, + E70CFDC59731BA62DD89906C /* STDSWhitelistView.m in Sources */, + A4CFDBFC3099BD7E1A694E3E /* UIButton+CustomInitialization.m in Sources */, + 2086DFD1FC02783FB106AD35 /* UIColor+DefaultColors.m in Sources */, + E19C8104FC1A9BAEA0BE8A9D /* UIColor+ThirteenSupport.m in Sources */, + 987347CA74818CEB716B9DB3 /* UIFont+DefaultFonts.m in Sources */, + 390691CA0EAF81418B0C65DB /* UIView+LayoutSupport.m in Sources */, + 08ECC7E70E7478E76793ED1E /* UIViewController+Stripe3DS2.m in Sources */, + D83D2FF20387FACC383A2A0F /* STDSAlreadyInitializedException.m in Sources */, + 7ED98325E7E54A54BFE54D6C /* STDSAuthenticationRequestParameters.m in Sources */, + C430332104547BA508416B41 /* STDSButtonCustomization.m in Sources */, + 6DB54DBAEFD08967367B6BF7 /* STDSChallengeParameters.m in Sources */, + FD8A81873C3BC7579ED07460 /* STDSCompletionEvent.m in Sources */, + 8A0872706BF4CDE72EF0C480 /* STDSConfigParameters.m in Sources */, + BF2651A9F3A31CB1AFEC44BB /* STDSCustomization.m in Sources */, + 8C9BD4B150F384111CBD3BD3 /* STDSErrorMessage.m in Sources */, + 3C88AE37A760B91E93D423CF /* STDSException.m in Sources */, + F569B584D357DC5C55542015 /* STDSFooterCustomization.m in Sources */, + EE3854BC2A14F87E8D51B0D0 /* STDSInvalidInputException.m in Sources */, + 0D3D9C95CB14A610EAAAEF6E /* STDSJSONEncoder.m in Sources */, + EEE77034728D2CFE38CC5A85 /* STDSLabelCustomization.m in Sources */, + BB379E68231A7DA62F87E628 /* STDSNavigationBarCustomization.m in Sources */, + 8A64AFBB2AC15E0E0F57ED25 /* STDSNotInitializedException.m in Sources */, + B51D1C218F460F67B58A1F0D /* STDSProtocolErrorEvent.m in Sources */, + 64D05353B50FAF3BB76D6527 /* STDSRuntimeErrorEvent.m in Sources */, + 01706B4660728A5BFAC12840 /* STDSRuntimeException.m in Sources */, + D3AA14D1E059E90F5A965CC4 /* STDSSelectionCustomization.m in Sources */, + 2C5621AABECF6192F92A20CF /* STDSStripe3DS2Error.m in Sources */, + 4E4E4BC1E2D041FA530336EE /* STDSSwiftTryCatch.m in Sources */, + 284F72D3FD8C06C0E5974086 /* STDSTextFieldCustomization.m in Sources */, + 24F626CD0F93B7AF59B1D6AA /* STDSThreeDS2Service.m in Sources */, + 6E727D2B738D7A3527952D28 /* STDSTransaction.m in Sources */, + DF4D38DC0AF89EAAA8718AAE /* STDSUICustomization.m in Sources */, + E08AF96E690D75F6AB52021F /* STDSWarning.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5D618F152EF19018F5C7E579 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B84ED7FBD49865F23B774067 /* AppDelegate.m in Sources */, + A39CFB56BF33B21A89987F9C /* STDSChallengeResponseObject+TestObjects.m in Sources */, + 128B64380D6724F016EE8D56 /* STDSDemoViewController.m in Sources */, + B012C4A8ACA1D064C40A1B63 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7A43CA17E1E2D826DCBDD88D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F939E05536AE838DC489C3AA /* STDSChallengeResponseViewControllerSnapshotTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 026B55E92602E24EE7139E1F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Stripe3DS2DemoUI; + target = 5907C55B1F111921112DF2BF /* Stripe3DS2DemoUI */; + targetProxy = 35B6793A7F3FB130F40209F1 /* PBXContainerItemProxy */; + }; + 0AE2FB81071258D69C9FCAD4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Stripe3DS2; + target = 57AEC53510AE0DC0539730F3 /* Stripe3DS2 */; + targetProxy = 52E1EB51359DA6E75D851D71 /* PBXContainerItemProxy */; + }; + 798525809F1F67D0C6634927 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Stripe3DS2; + target = 57AEC53510AE0DC0539730F3 /* Stripe3DS2 */; + targetProxy = 6C355B127D670833C76237D3 /* PBXContainerItemProxy */; + }; + C7515969CD69027C276886AB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Stripe3DS2; + target = 57AEC53510AE0DC0539730F3 /* Stripe3DS2 */; + targetProxy = 947ED275EAB25AD4CD701936 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F31A6580385C6390910AFD93 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 8A6035A8211B3D50D10EB947 /* bg-BG */, + 40DC4913154C3D3A27E35207 /* ca-ES */, + 7859D635964B2854A07B5285 /* cs-CZ */, + 8B8D429F1C03603DB218700F /* da */, + 2C2666DF7D23EBC3FFE7CCF1 /* de */, + 4C158B552B0CBE635A2AF7BB /* el-GR */, + EF5219762616ACF204F08C19 /* en */, + F112967E9FE8EE02FFFDC313 /* en-GB */, + 1BBADD412A7C2D1FF35A7C86 /* es */, + 406D88ADED27D64DED2002A2 /* es-419 */, + 461FDC130846625171164A9E /* et-EE */, + F139E48FDEFD921FF410892F /* fi */, + 49EF79D69693F937FEC46906 /* fil */, + 4BC4D9FD1D3869D491CC2CF8 /* fr */, + E64C700DED163CB77DCDEA78 /* fr-CA */, + 0E38A775DB9F529427E900CF /* hr */, + 371F8076630A8839C1F6A508 /* hu */, + 62773D5C85C567F28BCE0DA5 /* id */, + 13ED1CC076C6B3FB49C9197A /* it */, + 157970FF5B4A817041E3D668 /* ja */, + F86F34D1339F7467D0FDAC75 /* ko */, + 71494F94F36F4B731A27D182 /* lt-LT */, + 38572D4E32BF3670F1C83409 /* lv-LV */, + B1A7D41498B79E35BD724681 /* ms-MY */, + 18D88DE3E34106F934015BF9 /* mt */, + 6B487D8DD39DAC17042A3F04 /* nb */, + 83BDD516620860DC08B36B96 /* nl */, + D3F5A6D5A680F008C00A2D69 /* nn-NO */, + FE1DE9D978E6E682CB094128 /* pl-PL */, + 6690476C1BEA59C37576FB36 /* pt-BR */, + BC3EE020DC9358FF56389BBE /* pt-PT */, + F4D6DB90E3D01495C1F9BFE9 /* ro-RO */, + 694DF6F47767A651907D55D4 /* ru */, + 58C38FF9205C9B7D79C51A37 /* sk-SK */, + 3B9A821F35B93898A7AC1ADF /* sl-SI */, + 38F3A7D625E4B0D3506A37F6 /* sv */, + 2192708AD201F80F6E1A39F7 /* tr */, + B97D91CB54F48F64CAC9E378 /* vi */, + 3F017BF6ECE0EBE4E0DFCF9C /* zh-Hans */, + AF389752CCEB6FA734AAE31E /* zh-Hant */, + CECC1E22D0F0336039B435DE /* zh-HK */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03D567A9F86C4B204A11D604 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6940D75CA36F7DB9FB56196B /* Stripe3DS2DemoUI-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Stripe3DS2DemoUI/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2DemoUI; + PRODUCT_NAME = Stripe3DS2DemoUI; + SDKROOT = iphoneos; + }; + name = Release; + }; + 380615C693D7BF5A8884139A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5316D0759F7F2ED0BF1D3B2E /* Stripe3DS2DemoUI-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Stripe3DS2DemoUI/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2DemoUI; + PRODUCT_NAME = Stripe3DS2DemoUI; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 437A50F3FA4CE0127490A271 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4DB91F58CF23E72636C69C41 /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7C7BC68E9581B5EDB7602612 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D7A9D36FEF7E97CF735D82B8 /* Stripe3DS2DemoUITests-Release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = Stripe3DS2DemoUITests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2DemoUITests; + PRODUCT_NAME = Stripe3DS2DemoUITests; + SDKROOT = iphoneos; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stripe3DS2DemoUI.app/Stripe3DS2DemoUI"; + TEST_TARGET_NAME = Stripe3DS2DemoUI; + }; + name = Release; + }; + 872D92C1EE444AB448EB744C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F5686FB8EFA3AE3B39F85BBC /* Stripe3DS2Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = Stripe3DS2Tests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2Tests; + PRODUCT_NAME = Stripe3DS2Tests; + SDKROOT = iphoneos; + }; + name = Release; + }; + 8CD9CB7B7E57B44EE65EF421 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3A8CD68A006F970DDC54469A /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + A084DA5D39CBD41FD4E5294E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F2BC7014D26158AC9D74CC67 /* Stripe3DS2Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = Stripe3DS2Tests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2Tests; + PRODUCT_NAME = Stripe3DS2Tests; + SDKROOT = iphoneos; + }; + name = Debug; + }; + C34CBE7C8AA7B8ADB823B098 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E3C05BE55CF3086738AA182 /* Stripe3DS2DemoUITests-Debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = Stripe3DS2DemoUITests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.Stripe3DS2DemoUITests; + PRODUCT_NAME = Stripe3DS2DemoUITests; + SDKROOT = iphoneos; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stripe3DS2DemoUI.app/Stripe3DS2DemoUI"; + TEST_TARGET_NAME = Stripe3DS2DemoUI; + }; + name = Debug; + }; + D4CD9EDC1DA055683F427D24 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F8F6FFED1A0966A49B78F252 /* Stripe3DS2-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Stripe3DS2/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-3ds2"; + PRODUCT_NAME = Stripe3DS2; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; + FCA19B8EBB1E9578613A65BE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A9CD7EC589C5CC6CE5CF9310 /* Stripe3DS2-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = Stripe3DS2/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-3ds2"; + PRODUCT_NAME = Stripe3DS2; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 271DFDD5ABBB719F3BD1AB56 /* Build configuration list for PBXNativeTarget "Stripe3DS2Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A084DA5D39CBD41FD4E5294E /* Debug */, + 872D92C1EE444AB448EB744C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 37237B2A5629D2257C454AD1 /* Build configuration list for PBXNativeTarget "Stripe3DS2DemoUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 380615C693D7BF5A8884139A /* Debug */, + 03D567A9F86C4B204A11D604 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 57B08C966701355644CF5CEB /* Build configuration list for PBXProject "Stripe3DS2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8CD9CB7B7E57B44EE65EF421 /* Debug */, + 437A50F3FA4CE0127490A271 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5C9BBB6FD4175090CFCAA126 /* Build configuration list for PBXNativeTarget "Stripe3DS2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FCA19B8EBB1E9578613A65BE /* Debug */, + D4CD9EDC1DA055683F427D24 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5F64A3CED17753DA2643AA63 /* Build configuration list for PBXNativeTarget "Stripe3DS2DemoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C34CBE7C8AA7B8ADB823B098 /* Debug */, + 7C7BC68E9581B5EDB7602612 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 176A29DF97FAFE939C7F667B /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uber/ios-snapshot-test-case"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0118969F6608A92583CC6C98 /* iOSSnapshotTestCase */ = { + isa = XCSwiftPackageProductDependency; + productName = iOSSnapshotTestCase; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 8D219187B704575AD3CD3EC5 /* Project object */; +} diff --git a/Stripe3DS2/Stripe3DS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Stripe3DS2/Stripe3DS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2.xcscheme b/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2.xcscheme new file mode 100644 index 00000000..14b59e6f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2DemoUI.xcscheme b/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2DemoUI.xcscheme new file mode 100644 index 00000000..b45aa0a9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2.xcodeproj/xcshareddata/xcschemes/Stripe3DS2DemoUI.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stripe3DS2/Stripe3DS2/Info.plist b/Stripe3DS2/Stripe3DS2/Info.plist new file mode 100644 index 00000000..e1fe4cfb --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.h b/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.h new file mode 100644 index 00000000..71159625 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.h @@ -0,0 +1,20 @@ +// +// NSData+JWEHelpers.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (JWEHelpers) + +- (nullable NSString *)_stds_base64URLEncodedString; +- (nullable NSString *)_stds_base64URLDecodedString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.m b/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.m new file mode 100644 index 00000000..c1bbab4e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSData+JWEHelpers.m @@ -0,0 +1,35 @@ +// +// NSData+JWEHelpers.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSData+JWEHelpers.h" + +#import "NSString+JWEHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSData (STDSJSONWebEncryption) + +- (nullable NSString *)_stds_base64URLEncodedString { + // ref. https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C + NSString *unpaddedBase64EncodedString = [[[[self base64EncodedStringWithOptions:0] + stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]] // remove extra padding + stringByReplacingOccurrencesOfString:@"+" withString:@"-"] // replace "+" character w/ "-" + stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; // replace "/" character w/ "_" + + return unpaddedBase64EncodedString; +} + +- (nullable NSString *)_stds_base64URLDecodedString { + return [[[self base64EncodedStringWithOptions:0] + stringByReplacingOccurrencesOfString:@"-" withString:@"+"] // replace "-" character w/ "+" + stringByReplacingOccurrencesOfString:@"_" withString:@"/"]; // replace "_" character w/ "/" +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.h b/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.h new file mode 100644 index 00000000..70371f9f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.h @@ -0,0 +1,47 @@ +// +// NSDictionary+DecodingHelpers.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + Errors are populated according to the following rules: + - If the field is required and... + - the value is nil or empty -> STDSErrorCodeJSONFieldMissing + - the value is the wrong type -> STDSErrorCodeJSONFieldInvalid + - validator returns NO -> STDSErrorCodeJSONFieldInvalid + + - If the field is not required and... + - the value is nil -> valid, no error + - the value is empty -> STDSErrorCodeJSONFieldInvalid + - the value is the wrong type -> STDSErrorCodeJSONFieldInvalid + - validator returns NO -> STDSErrorCodeJSONFieldInvalid + */ +@interface NSDictionary (DecodingHelpers) + +/// Convenience method to extract an NSArray and populate it with instances of arrayElementType. +/// If isRequired is YES, returns nil without error if the key is not present +- (nullable NSArray *)_stds_arrayForKey:(NSString *)key arrayElementType:(Class)arrayElementType required:(BOOL)isRequired error:(NSError **)error; + +- (nullable NSURL *)_stds_urlForKey:(NSString *)key required:(BOOL)isRequired error:(NSError **)error; + +- (nullable NSDictionary *)_stds_dictionaryForKey:(NSString *)key required:(BOOL)isRequired error:(NSError **)error; + +- (nullable NSNumber *)_stds_boolForKey:(NSString *)key required:(BOOL)isRequired error:(NSError **)error; + +/// Convenience method that calls `_stpStringForKey:validator:required:error:`, passing nil for the validator argument +- (nullable NSString *)_stds_stringForKey:(NSString *)key required:(BOOL)isRequired error:(NSError **)error; + +- (nullable NSString *)_stds_stringForKey:(NSString *)key validator:(nullable BOOL (^)(NSString *))validatorBlock required:(BOOL)isRequired error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.m b/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.m new file mode 100644 index 00000000..4d2e2c09 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSDictionary+DecodingHelpers.m @@ -0,0 +1,147 @@ +// +// NSDictionary+DecodingHelpers.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSDictionary+DecodingHelpers.h" + +#import "NSError+Stripe3DS2.h" + +@implementation NSDictionary (DecodingHelpers) + +#pragma mark - NSArray + +- (nullable NSArray *)_stds_arrayForKey:(NSString *)key arrayElementType:(Class)arrayElementType required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing * _Nullable)error { + id value = self[key]; + + // Missing? + if (value == nil || ([value isKindOfClass:[NSArray class]] && ((NSArray *)value).count == 0)) { + if (isRequired && error) { + *error = [NSError _stds_missingJSONFieldError:key]; + } + return nil; + } + + // Invalid type or value? + if (![value isKindOfClass:[NSArray class]]) { + if (error) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + return nil; + } + + NSMutableArray *returnArray = [NSMutableArray new]; + for (id json in value) { + if (![json isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + return nil; + } + id element = [arrayElementType decodedObjectFromJSON:json error:error]; + if (element) { + [returnArray addObject:element]; + } + } + + return returnArray; +} + +#pragma mark - NSURL + +- (nullable NSURL *)_stds_urlForKey:(NSString *)key required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing *)error { + NSString *urlRawString = [self _stds_stringForKey:key validator:^BOOL (NSString *value) { + return [NSURL URLWithString:value] != nil; + } required:isRequired error:error]; + + if (urlRawString) { + return [NSURL URLWithString:urlRawString]; + } else { + return nil; + } +} + +#pragma mark - NSDictionary + +- (nullable NSDictionary *)_stds_dictionaryForKey:(NSString *)key required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing *)error { + id value = self[key]; + + // Missing? + if (value == nil) { + if (error && isRequired) { + *error = [NSError _stds_missingJSONFieldError:key]; + } + return nil; + } + + // Invalid type? + if (![value isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + return nil; + } + + return value; +} + +#pragma mark - NSString + +- (nullable NSString *)_stds_stringForKey:(NSString *)key required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing * _Nullable)error { + return [self _stds_stringForKey:key validator:nil required:isRequired error:error]; +} + +- (nullable NSString *)_stds_stringForKey:(NSString *)key validator:(nullable BOOL (^)(NSString * _Nonnull))validatorBlock required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing * _Nullable)error { + id value = self[key]; + + // Missing? + if (value == nil || ([value isKindOfClass:[NSString class]] && ((NSString *)value).length == 0)) { + if (error) { + if (isRequired) { + *error = [NSError _stds_missingJSONFieldError:key]; + } else if (value != nil) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + } + return nil; + } + + // Invalid type or value? + if (![value isKindOfClass:[NSString class]] || (validatorBlock && !validatorBlock(value))) { + if (error) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + return nil; + } + + return value; +} + +#pragma mark - NSURL + +- (NSNumber *)_stds_boolForKey:(NSString *)key required:(BOOL)isRequired error:(NSError * _Nullable __autoreleasing *)error { + id value = self[key]; + + // Missing? + if (value == nil) { + if (error && isRequired) { + *error = [NSError _stds_missingJSONFieldError:key]; + } + return nil; + } + + // Invalid type? + if (![value isKindOfClass:[NSNumber class]]) { + if (error) { + *error = [NSError _stds_invalidJSONFieldError:key]; + } + return nil; + } + + return value; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.h b/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.h new file mode 100644 index 00000000..6af09054 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.h @@ -0,0 +1,32 @@ +// +// NSError+Stripe3DS2.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSError (Stripe3DS2) + + +/// Represents an error where a JSON field value is not valid (e.g. expected 'Y' or 'N' but received something else). ++ (instancetype)_stds_invalidJSONFieldError:(NSString *)fieldName; + +/// Represents an error where a JSON field was either required or conditionally required but missing, empty, or null. ++ (instancetype)_stds_missingJSONFieldError:(NSString *)fieldName; + +/// Represents an error where a network request timed out. ++ (instancetype)_stds_timedOutError; + +// We explicitly do not provide any more info here based on security recommendations +// "the recipient MUST NOT distinguish between format, padding, and length errors of encrypted keys" +// https://tools.ietf.org/html/rfc7516#section-11.5 ++ (instancetype)_stds_jweError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.m b/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.m new file mode 100644 index 00000000..cd60c4cc --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSError+Stripe3DS2.m @@ -0,0 +1,38 @@ +// +// NSError+Stripe3DS2.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSError+Stripe3DS2.h" +#import "STDSLocalizedString.h" + +#import "STDSStripe3DS2Error.h" + +@implementation NSError (Stripe3DS2) + ++ (instancetype)_stds_invalidJSONFieldError:(NSString *)fieldName { + return [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeJSONFieldInvalid + userInfo:@{STDSStripe3DS2ErrorFieldKey: fieldName}]; +} + ++ (instancetype)_stds_missingJSONFieldError:(NSString *)fieldName { + return [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeJSONFieldMissing + userInfo:@{STDSStripe3DS2ErrorFieldKey: fieldName}]; +} + ++ (instancetype)_stds_timedOutError { + return [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeTimeout + userInfo:@{NSLocalizedDescriptionKey : STDSLocalizedString(@"Timeout", @"Error description for when a network request times out. English value is as required by UL certification.")}]; +} + ++ (instancetype)_stds_jweError { + return [[NSError alloc] initWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeDecryptionVerification userInfo:nil]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.h b/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.h new file mode 100644 index 00000000..66596c81 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.h @@ -0,0 +1,53 @@ +// +// NSLayoutConstraint+LayoutSupport.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSLayoutConstraint (LayoutSupport) + +/** + Provides an NSLayoutConstraint where the `NSLayoutAttributeTop` is equal for both views, with a multiplier of 1, and a constant of 0. + + @param view1 The view to constrain. + @param view2 The view to constraint to. + @return An NSLayoutConstraint that is constraining the first view to the second at the top. + */ ++ (NSLayoutConstraint *)_stds_topConstraintWithItem:(id)view1 toItem:(id)view2; + +/** + Provides an NSLayoutConstraint where the `NSLayoutAttributeLeft` is equal for both views, with a multiplier of 1, and a constant of 0. + + @param view1 The view to constrain. + @param view2 The view to constraint to. + @return An NSLayoutConstraint that is constraining the first view to the second on the left. + */ ++ (NSLayoutConstraint *)_stds_leftConstraintWithItem:(id)view1 toItem:(id)view2; + +/** + Provides an NSLayoutConstraint where the `NSLayoutAttributeRight` is equal for both views, with a multiplier of 1, and a constant of 0. + + @param view1 The view to constrain. + @param view2 The view to constraint to. + @return An NSLayoutConstraint that is constraining the first view to the second on the right. + */ ++ (NSLayoutConstraint *)_stds_rightConstraintWithItem:(id)view1 toItem:(id)view2; + +/** + Provides an NSLayoutConstraint where the `NSLayoutAttributeBottom` is equal for both views, with a multiplier of 1, and a constant of 0. + + @param view1 The view to constrain. + @param view2 The view to constraint to. + @return An NSLayoutConstraint that is constraining the first view to the second at the bottom. + */ ++ (NSLayoutConstraint *)_stds_bottomConstraintWithItem:(id)view1 toItem:(id)view2; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.m b/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.m new file mode 100644 index 00000000..8b73b5fe --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSLayoutConstraint+LayoutSupport.m @@ -0,0 +1,30 @@ +// +// NSLayoutConstraint+LayoutSupport.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSLayoutConstraint+LayoutSupport.h" + +@implementation NSLayoutConstraint (LayoutSupport) + + ++ (NSLayoutConstraint *)_stds_topConstraintWithItem:(id)view1 toItem:(id)view2 { + return [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeTop multiplier:1 constant:0]; +} + ++ (NSLayoutConstraint *)_stds_leftConstraintWithItem:(id)view1 toItem:(id)view2 { + return [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeLeft multiplier:1 constant:0]; +} + ++ (NSLayoutConstraint *)_stds_rightConstraintWithItem:(id)view1 toItem:(id)view2 { + return [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeRight multiplier:1 constant:0]; +} + ++ (NSLayoutConstraint *)_stds_bottomConstraintWithItem:(id)view1 toItem:(id)view2 { + return [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.h b/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.h new file mode 100644 index 00000000..aa019bf0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.h @@ -0,0 +1,19 @@ +// +// NSString+EmptyChecking.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (EmptyChecking) + ++ (BOOL)_stds_isStringEmpty:(NSString *)string; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.m b/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.m new file mode 100644 index 00000000..bf749718 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSString+EmptyChecking.m @@ -0,0 +1,25 @@ +// +// NSString+EmptyChecking.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSString+EmptyChecking.h" + +@implementation NSString (EmptyChecking) + ++ (BOOL)_stds_isStringEmpty:(NSString *)string { + if (string.length == 0) { + return YES; + } + + if(![string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length) { + return YES; + } + + return NO; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.h b/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.h new file mode 100644 index 00000000..acb9afde --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.h @@ -0,0 +1,21 @@ +// +// NSString+JWEHelpers.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (JWEHelpers) + +- (nullable NSString *)_stds_base64URLEncodedString; +- (nullable NSString *)_stds_base64URLDecodedString; +- (nullable NSData *)_stds_base64URLDecodedData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.m b/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.m new file mode 100644 index 00000000..2c8d5ad8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/NSString+JWEHelpers.m @@ -0,0 +1,54 @@ +// +// NSString+JWEHelpers.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "NSString+JWEHelpers.h" + +#import "NSData+JWEHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSString (JWEHelpers) + +- (nullable NSString *)_stds_base64URLEncodedString { + return [[self dataUsingEncoding:NSUTF8StringEncoding] _stds_base64URLEncodedString]; +} + +- (nullable NSString *)_stds_base64URLDecodedString { + NSData *data = [self _stds_base64URLDecodedData]; + return data != nil ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil; +} + +// ref. https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C +- (nullable NSData *)_stds_base64URLDecodedData { + NSCharacterSet *illegalBase64Chars = [NSCharacterSet characterSetWithCharactersInString:@"+/ \n"]; // TC_SDK_10556_001 & TC_SDK_10557_001 & TC_SDK_10558_001 & TC_SDK_10559_001 + if ([self hasSuffix:@"="] || [self rangeOfCharacterFromSet:illegalBase64Chars].location != NSNotFound) { + return nil; // invalid base64url string TC_SDK_10554_001 & TC_SDK_10555_001 + } + NSMutableString *decodedString = [[[self stringByReplacingOccurrencesOfString:@"-" withString:@"+"] // replace "-" character w/ "+" + stringByReplacingOccurrencesOfString:@"_" withString:@"/"] mutableCopy]; // replace "_" character w/ "/"]; + + switch (decodedString.length % 4) { + case 0: + break; // no padding needed + case 2: + [decodedString appendString:@"=="]; // pad with 2 + break; + case 3: + [decodedString appendString:@"="]; // pad with 1 + break; + default: + return nil; // invalid base64url string + + } + + return [[NSData alloc] initWithBase64EncodedString:decodedString options:0]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/PrivacyInfo.xcprivacy b/Stripe3DS2/Stripe3DS2/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..1f0bb214 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/amex.der b/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/amex.der new file mode 100644 index 0000000000000000000000000000000000000000..61a1cfd1f0490b32bdbe4e29d7a11602cbc2458f GIT binary patch literal 1237 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 literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/mastercard.der b/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/mastercard.der new file mode 100644 index 0000000000000000000000000000000000000000..6e136e13312275677887b3949a1212807ae4ba48 GIT binary patch literal 1465 zcmXqLV%=)c#5{KaGZP~dlYrWii5b6V{a>z`xic-q@LGTYFB_*;n@8JsUPeZ4RtAGA zLv903Hs(+kHesgFU_)U8K@f*an9Db@xFog6Ik70kP{TkCB*-l+j}TPwOi9fv$tXZu*;J)OU7b)WK_?YT4z^28wtXOp7|kb@*#lQNM%a z;)6j47n~QY`r+^(%=Uey=?Ol2APQWV%D0P zZcMYXZLe|pE}48K*gE~f-3Y@}*CoM5TUWR%MI}isIlR61>w~yAA`ITg4YT>uwOape zyQ_Tlo{vDs)-bE@8?4uMY;9$2s8Ov>&w9U1No%YA6}|TiCQEK`?r2)0|Moi5oba9- zjJH_=qOMK|bga_ZSz7RMy1>eBHnTf=nV1Al4ug@0BpDtUY7Aa||$*HDhsm2Dzh9+rg zrm3l^CKgE+CZ^ zah+t-Y^loyjHUw)|I zknspz7Cz-e?UcpS_;OcA|5?i3bgS&V_mO(Vg{g86*X^^}>!Mj3*vKHw6z;Yv$obuzNtN;cw}*&s`0a*%3s@?y_Gz=-Ru>oeco|d=X|UconHG6N+hu_drWnSvCL)ir0GS39XO{*pxD)5$6dHLp7f9lgE)1HXN>gwd#+Ppt0ymsB>p4=-xR`sc9 z{Mo;7`)99?39R?73$x2yv69rQm7Ofd@=ZlMXJ=bRy=s(2uK209ZQJt#f>=M)FosTe zk~KBTGe35=fX=<6^54%W+J5?^Xc`?KlHH>?P5Sw}{#%L3OXT*t-IiJSLGbaLy6wyv Z$pt^d{!ZZZv~EmwSjr^LyGZbx8UUfUE+GH_ literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/ul-test.der b/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/ul-test.der new file mode 100644 index 0000000000000000000000000000000000000000..11e5f29b3be5d00b2cf10c91929e8b9b913730d7 GIT binary patch literal 1578 zcmXqLVpB6{V&PlB%*4pV#KxFWq`|h!fR~L^tIebBJ1-+6H!FidjUl%ICmVAp3!5;L zpO2xYfjWr8#iJ1FqYzS*m{**bT#}ierx2W)Tw0V_QmK%VS(aH06fsmVkOL{@<`D-e z4OTFA(FH3?RS5F;4^eP-gsEW0slq@`oY&COz|hFh$iT$J&?rir*T?{gYanSTZXgP> zg_(!1xTGkvAXT?CN3SHcxWu4|NeS66jI0dIO-%d@KyfanCMHIPRXabQo`34Xu0-+D zExlUva#l>eeBx{2v=>o#Ma``}n-83P#I@?oN_jq&sJ8Iwkxtbvmu5O0b(ffSf8IPz z$?7Cd-O2ajE|iH(EL1aR57||ySu3wve~Pg{V{+QVptWBCNWKJS_dQR{BrT z#W_lAj#_2jd>(tF+oSKP-;ouc=PZxhD7?JaM8jWVXz(sQn_7V@MT8@$CSf7Z*prn z&ZKFv6-9M;=JShYn-`>3n2$w_MP%03nmRb&ddmBF(GFfU;zNk zG>i;;qLg-sy~7h6wI7uI9y_+P*6ki*?e}x zT7Iz@t$p*v~I@fof=2IJmntF5#Pc)MSV^5_RTl%FD&ae+f^QExo!HBSGrHH z+=}abAK;unxBl$q|CcX6k4(|N6*|>uS>21S3CAzk)xY}Nt1c^%r6Az-^!2wLnS!>j z)5_|l7M``Y)oeNPY2}{z>kF;^)bndK?#p7^_j>Vr0qoATi{ET1pEc{|U&|W- z3->tYaDVhjHc#;j`PwHiuVcdPv*opuzDpJCUUHx}NcG+2=-%s17hVPJ6TeqwaC7aV z(1Z;J?{_=9FwM*2<0e_`6DKPHgq7dR-|6s|Qhgy0TxI z$jm>zu5Yo_hT4BwsduVn(*(aRp2~N`Cvi^stjeN#&W`lkN>!I3|8>Gz}y4ehraX88qOJ##l)UF^&6#Jfp*q9m5xNM6gT`kiUv dZH?xLYoB|y_O7}APsU??|HLivPSg2*006^ahiCu* literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/visa.der b/Stripe3DS2/Stripe3DS2/Resources/CertificateFiles/visa.der new file mode 100644 index 0000000000000000000000000000000000000000..a77735dfa6e224cfa40334bb41a402771358f9a8 GIT binary patch literal 1458 zcmXqLVqIs@#4>FGGZP~dlK^|x$yo=l9^BkpUN_DD3jbOIUN%mxHjlRNyo`+8tPBQ) zhTI06Y|No7Y{E>T!G^pB+#n8@FiV(cu%n^Afi8&4Evy!nS)8cgnOBlpl$ThNnV*-K zqY#{0RF;{Xs^C~$oS&Qt7By5dPylIQ7M28SNOjK7%}p%=D)KBYF3rqKS8#STkQ3)M zG&eLfFaiQ&LxU)BULyk_*Vw=S%r$6SXeePI1~P_SnBOBaJtHSEFQr%^C^0WN!%)zG zA0)#e%;B7$lV6mWl22U^s2V6kJRoD7Qf#DGRGg@nnwMNuSpf31UKubH^pf*)4VoC0 zkVBG@m4Ug5k)Hu5&c)Qk$jGqie!Jx7^BZelz9>B6e9d^#tEW!3Rc6Q4X2%{{zFo4q zJ(4|3Y|gBi?>g$2c(E*aV93r}_~2qf%#u?j1#-Jf9Zh3wyqSed8urDS#X`cw2BP@4w4i)ZT3Vu&!ZCg4d#1`PT&5&tGOWH+s4!`Ney7{$<^_ z4Z91D*_LQY#|x||*{Ew|6dSUi_m^7shTa9+nV1#%5$>i8Kf|P=)ag7~3Q>N=gc>^!4+Tiwod^4(1u?!Id$!DM6K` z<`w1VgX2d(6&OjmddaCp2J#@gm05r(szK!bVXaU-eJ86bhq;p$e0Q(B-rwG3 zzynes%*gnkg~@=ypz#!lE6>t+$e?k*fyM&01uAfBTrsTyrEk6Dq8x+9NedeL7c};? zHFoBtB!b+Iq1wvW!cspu&lYFS&`Gup4c5sog{UBy*>v)4L7CJZ=*yDKw9MqhlGLEo zvV0&7OsYPa#U%!OAiwZ~yvD-J#Ja#h9mE%8Q818e!k@flK??a;#8^cB%+7M&>K$>t z{QsQ`eKX&a7!;k(+F<*$D`#5PLr zd%IkNOE{<5&bj5&t`;>;@o*#a% zW~9xpy22j1iglla+2ilKPoLWJHH=sD)AjjBt{FOamYQgo{NIsN&NN5*TBJM2jh#h8 z9i}$|E=iwAurJp<{Fd=X*_`SN0g`ez8;v*_m|bM&RX8NiC>L`nFxjc#<&_p(qj1~a zWKj{9SO4X!4i7Hxk0@&sVSU@w^sFzyR&! VxmlLhu5Qh(dR3q=eoivq001R~|EB-| literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@1x.png b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e6aa5e46f5cad15a947690d418e55c6697e3ee7b GIT binary patch literal 311 zcmV-70m%M|P)oDx@67HEB_f8UxHO4s{k3g9@z|#p&T*6?j~TRATyS%*|qjR!~Y;g zM#d9te2gApuC5w`|?+ zng9O(PXU^Eo0*y284)}XH5ep8D_B5|eDd}e*pck~i~{J61OP)wcwK1nMx+1$002ov JPDHLkV1nwkgggKM literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@2x.png b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/Chevron.imageset/Chevron@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..821c9bea5a7074a13c3289434ebec127efac147c GIT binary patch literal 798 zcmV+(1L6FMP)7cD6rP!-YBjBiRw80!HKHEEi7G}IeJq%G{iGQPKE#svy?NZ9wj_q}gs z-n{Ak7H6~>U)1~7!WB`qLGV{<0M)k}mHw$OJ64i}E zH%MZ7W-FF7S_o1Ri6t}3obWlvUtcetmop^Nh0D^xaF+uvr|1W$7O_$ zUPFuX_I6y)d3#CDA`=^5Cd`9c8G{N@X69AznjkuzGBaP&`fBHaCw|NQElIAD#((!{BK~R~g`nO|)%Ngx%KSx^d?;0uEoZd1mdX|uj} z0vISD2zbe?y;1_|6BHPJ@xp~?eQ*9NfMn=W&wjZXmyZjAFyIcp8Y+P84T&rG4jiYz zp5Z-!-vYKeVkZiuQlH0at)q`nNO_b%v|;~GVA+d8tWN;1rgd1$;1eZCkZ3WYJaS)C z5t9P=bP&?ONARW$QgQ@|!Enfny)kge*HbX?6@`sXYa!oPfergQHR!BhN%63S`Jngv cu!P>&U&ePNY{-UxdB|;d~7jz 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 sYnX@?3N&+ak>uU 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 literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/Contents.json b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/Contents.json new file mode 100644 index 00000000..428f8cf0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "discover@1x.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/discover@1x.png b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/discover-logo.imageset/discover@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..09ca3812d06ea40c08f804a9a987fb801cf1bb5f GIT binary patch literal 3220 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 V8r^3s1n>X=002ovPDHLkV1mf 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 literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/Contents.json b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/Contents.json new file mode 100644 index 00000000..102a36c8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "mastercard@1x.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/mastercard@1x.pdf b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/mastercard-logo.imageset/mastercard@1x.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cc999c3f476d248baefdbd5cf4784c19496bbd5a GIT binary patch literal 4307 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 literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/Contents.json b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/Contents.json new file mode 100644 index 00000000..e91aeeb5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "visa-white@1x.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/visa-white@1x.pdf b/Stripe3DS2/Stripe3DS2/Resources/Stripe3DS2.xcassets/visa-white-logo.imageset/visa-white@1x.pdf new file mode 100644 index 0000000000000000000000000000000000000000..437593c4040601379aa4ef29f4816f94f71149c7 GIT binary patch literal 41739 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 literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2/Resources/bg-BG.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/bg-BG.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/bg-BG.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ca-ES.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ca-ES.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ca-ES.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/cs-CZ.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/cs-CZ.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/cs-CZ.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/da.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/da.lproj/Localizable.strings new file mode 100644 index 00000000..2388efaa --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/da.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Et fejlfindingsprogram er vedhæftet appen."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "En emulator bruges til at køre appen."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Annullér"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Indlæser"; + +/* The no answer to a yes or no question. */ +"No" = "Nej"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Sikker betaling"; + +/* Indicates that a button is selected. */ +"Selected" = "Valgt"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Enheden er jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Integriteten af SDK'en er blevet ændret."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Operativsystemet eller versionen af operativsystemet understøttes ikke."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Udløb"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Fravalgt"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/de.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/de.lproj/Localizable.strings new file mode 100644 index 00000000..55a9ea44 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/de.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Ein Debugger ist an die App angeschlossen."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Ein Emulator wird zum Ausführen dieser App verwendet."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Abbrechen"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Wird geladen"; + +/* The no answer to a yes or no question. */ +"No" = "Nein"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Sicherer Bezahlvogang"; + +/* Indicates that a button is selected. */ +"Selected" = "Ausgewählt"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Das Gerät ist beschädigt."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Die Integrität des SDK wurde manipuliert."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Das Betriebssystem oder die Betriebssystemversion wird nicht unterstützt."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Zeitüberschreitung"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Nicht ausgewählt"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/el-GR.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/el-GR.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/el-GR.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/en-GB.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/en-GB.lproj/Localizable.strings new file mode 100644 index 00000000..ddfe3b2a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/en-GB.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device has been jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered with."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/en.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/en.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/en.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/es-419.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/es-419.lproj/Localizable.strings new file mode 100644 index 00000000..759db059 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/es-419.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Se ha asociado un depurador a la aplicación."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Se está usando un emulador para ejecutar la aplicación."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancelar"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Cargando"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Proceso de compra seguro"; + +/* Indicates that a button is selected. */ +"Selected" = "Seleccionado"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "El dispositivo ha sido liberado."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Se ha alterado la integridad del SDK."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "El OS o la versión del OS no son compatibles."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Tiempo de espera agotado"; + +/* Indicates that a button is not selected. */ +"Unselected" = "No seleccionado"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Sí"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/es.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/es.lproj/Localizable.strings new file mode 100644 index 00000000..5a6f2732 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/es.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Se ha asociado un depurador a la aplicación."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Se está usando un emulador para ejecutar la aplicación."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancelar"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Cargando"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Proceso de compra seguro"; + +/* Indicates that a button is selected. */ +"Selected" = "Seleccionado"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "El dispositivo ha sido liberado."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Se ha alterado la integridad del SDK."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "El SO o la versión del SO no son compatibles."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Tiempo de espera agotado"; + +/* Indicates that a button is not selected. */ +"Unselected" = "No seleccionado"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Sí"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/et-EE.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/et-EE.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/et-EE.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/fi.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/fi.lproj/Localizable.strings new file mode 100644 index 00000000..faeaf2d6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/fi.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Sovellukseen on liitetty virheiden tarkastusohjelma."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Sovelluksen suorittamiseen käytetään emulaattoria."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Peruuta"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Ladataan"; + +/* The no answer to a yes or no question. */ +"No" = "Ei"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Turvallinen maksaminen"; + +/* Indicates that a button is selected. */ +"Selected" = "Valittu"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Laitteen käyttöjärjestelmän rajoitukset on poistettu."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK:n eheyttä on peukaloitu."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Käyttöjärjestelmää tai käyttöjärjestelmäversiota ei tueta."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Aikakatkaisu"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Ei valittu"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Kyllä"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/fil.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/fil.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/fil.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/fr-CA.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/fr-CA.lproj/Localizable.strings new file mode 100644 index 00000000..19956a78 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/fr-CA.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Un débogueur est attaché à l'application."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Un émulateur est utilisé pour exécuter l'application."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Annuler"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Chargement"; + +/* The no answer to a yes or no question. */ +"No" = "Non"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Paiement sécurisé"; + +/* Indicates that a button is selected. */ +"Selected" = "Sélectionné"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "L'appareil est débridé."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "L'intégrité de SDK a été modifiée."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Le système d'exploitation ou la version du système d'exploitation n'est pas pris(e) en charge."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Expiration"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Non sélectionné"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Oui"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/fr.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/fr.lproj/Localizable.strings new file mode 100644 index 00000000..9d595679 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/fr.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Un débogueur est attaché à l'application."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Un émulateur est utilisé pour accéder à l'application."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Annuler"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Chargement"; + +/* The no answer to a yes or no question. */ +"No" = "Non"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Paiement sécurisé"; + +/* Indicates that a button is selected. */ +"Selected" = "Sélectionné"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "L'appareil est débridé."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "L'intégrité du SDK a été altérée."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Le système d'exploitation ou la version du système d'exploitation ne sont pas pris en charge."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Non sélectionné"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Oui"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/hr.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/hr.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/hr.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/hu.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/hu.lproj/Localizable.strings new file mode 100644 index 00000000..889cda1b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/hu.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Az alkalmazáshoz hibaelhárítót csatoltak."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Az alkalmazás futtatása emulátorral történik."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Mégse"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Betöltés folyamatban"; + +/* The no answer to a yes or no question. */ +"No" = "Nem"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Biztosnágos kifizetés"; + +/* Indicates that a button is selected. */ +"Selected" = "Kijelölve"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Az eszközt feltörték."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Az SDK integritása sérült."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Az OS vagy OS-verzió nem támogatott."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "időtúllépés"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Nincs kijelölve"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Igen"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/id.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/id.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/id.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/it.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/it.lproj/Localizable.strings new file mode 100644 index 00000000..419bf6b1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/it.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "All'app è connesso un debugger."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Per eseguire l'app viene utilizzato un emulatore."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Annulla"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Caricamento in corso"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Completamento del pagamento sicuro"; + +/* Indicates that a button is selected. */ +"Selected" = "Selezionato"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Il dispositivo è jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "L'integrità dell'SDK è stata manomessa."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Il sistema operativo o la versione del sistema operativo non sono supportati."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Non selezionato"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Sì"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ja.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ja.lproj/Localizable.strings new file mode 100644 index 00000000..9cec4421 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ja.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "アプリにデバッガが添付されています。"; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "アプリの実行にエミュレータが使用されています。"; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "キャンセル"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "読み込み中"; + +/* The no answer to a yes or no question. */ +"No" = "いいえ"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "セキュアなチェックアウト"; + +/* Indicates that a button is selected. */ +"Selected" = "選択済み"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "このデバイスは脱獄状態です。"; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK の完全性が改ざんされています。"; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "OS または OS のバージョンがサポートされていません。"; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "タイムアウト"; + +/* Indicates that a button is not selected. */ +"Unselected" = "選択解除済み"; + +/* The yes answer to a yes or no question. */ +"Yes" = "はい"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ko.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ko.lproj/Localizable.strings new file mode 100644 index 00000000..4ae98c37 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ko.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "디버거가 앱에 연결되었습니다."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "앱을 실행하는 데 에뮬레이터가 사용되고 있습니다."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "취소"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "로드 중"; + +/* The no answer to a yes or no question. */ +"No" = "아니요"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "보안 체크 아웃"; + +/* Indicates that a button is selected. */ +"Selected" = "선택됨"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "장치가 무단 해제되었습니다."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK의 무결성이 변경되었습니다."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = " OS 또는 OS 버전이 지원되지 않습니다."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "시간 초과"; + +/* Indicates that a button is not selected. */ +"Unselected" = "선택 해제됨"; + +/* The yes answer to a yes or no question. */ +"Yes" = "예"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/lt-LT.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/lt-LT.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/lt-LT.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/lv-LV.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/lv-LV.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/lv-LV.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ms-MY.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ms-MY.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ms-MY.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/mt.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/mt.lproj/Localizable.strings new file mode 100644 index 00000000..55b091cf --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/mt.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Debugger huwa mqabbad mal-Applikazzjoni."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Qed jintuża emulatur biex iħaddem l-Applikazzjoni."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Ikkanċella"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Qed jillowdja"; + +/* The no answer to a yes or no question. */ +"No" = "Le"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Checkout sigur"; + +/* Indicates that a button is selected. */ +"Selected" = "Attivata"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "L-apparat huwa mmodifikat (jailbroken)."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "L-integrità tal-SDK ġiet imbagħbsa."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Is-Sistema Operattiva (OS) jew il-Verżjoni tas-Sistema Operattiva (OS) mhijiex appoġġjata."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Waqfa qasira"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Mhijiex attivata"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Iva"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/nb.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/nb.lproj/Localizable.strings new file mode 100644 index 00000000..78613b3d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/nb.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "En debugger er knyttet til App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "En emulator brukes til å kjøre App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Avbryt"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Laster"; + +/* The no answer to a yes or no question. */ +"No" = "Nei"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Sikker utsjekk"; + +/* Indicates that a button is selected. */ +"Selected" = "Valgt"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Enheten er jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Integriteten til SDK har blitt manipulert."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "OS- eller OS-versjonen støttes ikke."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Avbrudd"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Ikke valgt"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/nl.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/nl.lproj/Localizable.strings new file mode 100644 index 00000000..335a340a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/nl.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Er is een foutopsporingsprogramma gekoppeld aan de app."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Er wordt een emulator gebruikt om de app uit te voeren."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Annuleren"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Bezig met laden"; + +/* The no answer to a yes or no question. */ +"No" = "Nee"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Beveiligde Checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Geselecteerd"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Het apparaat is opengebroken via een jailbreak."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "De integriteit van de SDK is niet meer intact."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Het besturingssysteem of de versie wordt niet ondersteund."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Time-out"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Niet geselecteerd"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/nn-NO.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/nn-NO.lproj/Localizable.strings new file mode 100644 index 00000000..65443693 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/nn-NO.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Ein avlusar er knytta til appen."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Ein emulator blir nytta til å køyre appen."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Avbryt"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Lastar inn"; + +/* The no answer to a yes or no question. */ +"No" = "Nei"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Trygg utsjekk"; + +/* Indicates that a button is selected. */ +"Selected" = "Vald"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Eininga er jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Integriteten til SDK er manipulert."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "OS- eller OS-versjonen blir ikkje stydd."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Tidsavbrot"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Uvald"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/pl-PL.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/pl-PL.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/pl-PL.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/pt-BR.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/pt-BR.lproj/Localizable.strings new file mode 100644 index 00000000..2e3e7b5b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/pt-BR.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Um depurador está anexado ao aplicativo."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Está sendo usado um emulador para executar o aplicativo."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancelar"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Carregando"; + +/* The no answer to a yes or no question. */ +"No" = "Não"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Finalização de compra segura"; + +/* Indicates that a button is selected. */ +"Selected" = "Selecionado"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "O dispositivo está desbloqueado."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "A integridade do SDK foi adulterada."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "O SO ou a versão do SO não é compatível."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Tempo esgotado"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Não selecionado"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Sim"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/pt-PT.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/pt-PT.lproj/Localizable.strings new file mode 100644 index 00000000..3be97a51 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/pt-PT.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Um depurador está anexado à aplicação."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Um emulador está a ser utilizado para executar a aplicação."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancelar"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "A carregar"; + +/* The no answer to a yes or no question. */ +"No" = "Não"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Finalização de compra segura"; + +/* Indicates that a button is selected. */ +"Selected" = "Selecionado"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "O dispositivo está desbloqueado."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "A integridade do SDK foi adulterada."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Sistema operativo ou versão do sistema operativo não suportado(a)."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Tempo limite"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Não selecionado"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Sim"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ro-RO.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ro-RO.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ro-RO.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/ru.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/ru.lproj/Localizable.strings new file mode 100644 index 00000000..2c7cd69b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/ru.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "К приложению подключен отладчик."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Приложение запускается и работает в симуляторе."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Отмена"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Идет загрузка"; + +/* The no answer to a yes or no question. */ +"No" = "Нет"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Безопасное оформление и оплата заказа"; + +/* Indicates that a button is selected. */ +"Selected" = "Выбранные"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Устройство разблокировано."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "Целостность пакета средств разработки программного обеспечения нарушена."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "Данная операционная система или данная версия операционной системы не поддерживается."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Истечение лимита времени"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Не выбранные"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Да"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/sk-SK.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/sk-SK.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/sk-SK.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/sl-SI.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/sl-SI.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/sl-SI.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/sv.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/sv.lproj/Localizable.strings new file mode 100644 index 00000000..e92a466a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/sv.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "En felsökare är ansluten till appen."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "En emulator används för att köra appen."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Avbryt"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Laddar"; + +/* The no answer to a yes or no question. */ +"No" = "Nej"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Säker utcheckning"; + +/* Indicates that a button is selected. */ +"Selected" = "Markerad"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Enheten är jailbreakad."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK:ns integritet har manipulerats."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "OS eller OS-versionen stöds inte."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Avbrott"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Avmarkerad"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Ja"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/tr.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/tr.lproj/Localizable.strings new file mode 100644 index 00000000..bf9064f1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/tr.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "Uygulamaya hata ayıklayıcı eklendi."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "Uygulamayı çalıştırmak için bir emülatör kullanılıyor."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "İptal"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "Yüklüyor"; + +/* The no answer to a yes or no question. */ +"No" = "Hayır"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Güvenli ödeme"; + +/* Indicates that a button is selected. */ +"Selected" = "Seçildi"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "Cihazın üretici yazılım kilidi kaldırılmış."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK bütünlüğü ihlal edilmiş."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "İS veya İS Sürümü desteklenmiyor."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Zaman aşımı"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Seçimi kaldırıldı"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Evet"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/vi.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/vi.lproj/Localizable.strings new file mode 100644 index 00000000..7e168827 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/vi.lproj/Localizable.strings @@ -0,0 +1,45 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "A debugger is attached to the App."; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "An emulator is being used to run the App."; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "Cancel"; + +/* Accessibility label for expandandable text control to indicate text is hidden. */ +"Collapsed" = "Collapsed"; + +/* Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available. */ +"Expanded" = "Expanded"; + +/* Spoken by VoiceOver when the challenge is loading. */ +"Loading" = "Loading"; + +/* The no answer to a yes or no question. */ +"No" = "No"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "Secure checkout"; + +/* Indicates that a button is selected. */ +"Selected" = "Selected"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "The device is jailbroken."; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "The integrity of the SDK has been tampered."; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "The OS or the OS Version is not supported."; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "Timeout"; + +/* Indicates that a button is not selected. */ +"Unselected" = "Unselected"; + +/* The yes answer to a yes or no question. */ +"Yes" = "Yes"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/zh-HK.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/zh-HK.lproj/Localizable.strings new file mode 100644 index 00000000..16328109 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/zh-HK.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "調試器已附在應用程式內。"; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "正在用模擬器執行應用程式。"; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "取消"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "正在載入"; + +/* The no answer to a yes or no question. */ +"No" = "否"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "安全結帳"; + +/* Indicates that a button is selected. */ +"Selected" = "已選"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "該設備已越獄。"; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK 的完整性已受損。"; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "不支援此 OS 或 OS 版本。"; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "超時"; + +/* Indicates that a button is not selected. */ +"Unselected" = "已去選"; + +/* The yes answer to a yes or no question. */ +"Yes" = "是"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/zh-Hans.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..b7729c45 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "该应用附带有调试程序。"; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "正在使用模拟器运行此应用。"; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "取消"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "正在加载"; + +/* The no answer to a yes or no question. */ +"No" = "否"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "安全的结账流程"; + +/* Indicates that a button is selected. */ +"Selected" = "已选择"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "该设备已越狱。"; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "该 SDK 的完整性已被破坏。"; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "不支持 OS 或 OS 版本。"; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "超时"; + +/* Indicates that a button is not selected. */ +"Unselected" = "未选择"; + +/* The yes answer to a yes or no question. */ +"Yes" = "是"; + diff --git a/Stripe3DS2/Stripe3DS2/Resources/zh-Hant.lproj/Localizable.strings b/Stripe3DS2/Stripe3DS2/Resources/zh-Hant.lproj/Localizable.strings new file mode 100644 index 00000000..4ea12100 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Resources/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,39 @@ +/* The text for warning when a debugger is currently attached to the process. */ +"A debugger is attached to the App." = "除錯工具附加在應用程式內。"; + +/* The text for warning when an emulator is being used to run the application. */ +"An emulator is being used to run the App." = "正在用模擬器執行應用程式。"; + +/* The text for the button that cancels the current challenge process. */ +"Cancel" = "取消"; + +/* Spoken by VoiceOver when the screen is loading. */ +"Loading" = "正在載入"; + +/* The no answer to a yes or no question. */ +"No" = "否"; + +/* The title for the challenge response step of an authenticated checkout. */ +"Secure checkout" = "安全結帳"; + +/* Indicates that a button is selected. */ +"Selected" = "已選"; + +/* The text for warning when a device is jailbroken */ +"The device is jailbroken." = "該裝置已越獄。"; + +/* The text for warning when the integrity of the SDK has been tampered with */ +"The integrity of the SDK has been tampered." = "SDK 的完整性已受損。"; + +/* The text for warning when the SDK is running on an unsupported OS or OS version. */ +"The OS or the OS Version is not supported." = "不支援此 OS 或 OS 版本。"; + +/* Error description for when a network request times out. English value is as required by UL certification. */ +"Timeout" = "逾時"; + +/* Indicates that a button is not selected. */ +"Unselected" = "已去選"; + +/* The yes answer to a yes or no question. */ +"Yes" = "是"; + diff --git a/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.h b/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.h new file mode 100644 index 00000000..cc907e88 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.h @@ -0,0 +1,30 @@ +// +// STDSACSNetworkingManager.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/3/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSChallengeRequestParameters; +@class STDSErrorMessage; +@protocol STDSChallengeResponse; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSACSNetworkingManager : NSObject + +- (instancetype)initWithURL:(NSURL *)acsURL + sdkContentEncryptionKey:(NSData *)sdkCEK + acsContentEncryptionKey:(NSData *)acsCEK + acsTransactionIdentifier:(NSString *)acsTransactionID; + +- (void)submitChallengeRequest:(STDSChallengeRequestParameters *)request withCompletion:(void (^)(id _Nullable, NSError * _Nullable))completion; + +- (void)sendErrorMessage:(STDSErrorMessage *)errorMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.m b/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.m new file mode 100644 index 00000000..f7c8de55 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSACSNetworkingManager.m @@ -0,0 +1,169 @@ +// +// STDSACSNetworkingManager..m +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/3/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSACSNetworkingManager.h" + +#import "STDSChallengeRequestParameters.h" +#import "STDSChallengeResponseObject.h" +#import "STDSJSONEncoder.h" +#import "STDSJSONWebEncryption.h" +#import "STDSStripe3DS2Error.h" +#import "STDSErrorMessage+Internal.h" +#import "NSError+Stripe3DS2.h" + +NS_ASSUME_NONNULL_BEGIN + +/// [Req 239] requires us to abort if the ACS does not respond with the CRes message within 10 seconds. +static const NSTimeInterval kTimeoutInterval = 10; + +@implementation STDSACSNetworkingManager { + NSURL *_acsURL; + NSData *_sdkContentEncryptionKey; + NSData *_acsContentEncryptionKey; + NSString *_acsTransactionIdentifier; + + NSURLSession *_urlSession; + NSURLSessionTask * _Nullable _currentTask; +} + +- (instancetype)initWithURL:(NSURL *)acsURL + sdkContentEncryptionKey:(NSData *)sdkCEK + acsContentEncryptionKey:(NSData *)acsCEK + acsTransactionIdentifier:(nonnull NSString *)acsTransactionID { + self = [super init]; + if (self) { + _acsURL = acsURL; + _sdkContentEncryptionKey = sdkCEK; + _acsContentEncryptionKey = acsCEK; + _acsTransactionIdentifier = [acsTransactionID copy]; + _urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] + delegate:nil + delegateQueue:[NSOperationQueue mainQueue]]; + } + + return self; +} + +- (void)dealloc { + [_urlSession finishTasksAndInvalidate]; +} + +- (void)submitChallengeRequest:(STDSChallengeRequestParameters *)request withCompletion:(void (^)(id _Nullable, NSError * _Nullable))completion { + if (_currentTask != nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeAssertionFailed + userInfo:@{@"assertion": [NSString stringWithFormat:@"%@ is not intended to handle multiple concurrent tasks.", NSStringFromClass([self class])]}]); + }); + return; + } + + NSDictionary *requestJSON = [STDSJSONEncoder dictionaryForObject:request]; + NSError *encryptionError = nil; + NSString *encryptedRequest = [STDSJSONWebEncryption directEncryptJSON:requestJSON + withContentEncryptionKey:_sdkContentEncryptionKey + forACSTransactionID:_acsTransactionIdentifier + error:&encryptionError]; + + if (encryptedRequest != nil) { + NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:_acsURL]; + urlRequest.HTTPMethod = @"POST"; + urlRequest.timeoutInterval = kTimeoutInterval; + [urlRequest setValue:@"application/jose;charset=UTF-8" forHTTPHeaderField:@"Content-Type"]; + __weak __typeof(self) weakSelf = self; + NSURLSessionUploadTask *requestTask = [_urlSession uploadTaskWithRequest:[urlRequest copy] fromData:[encryptedRequest dataUsingEncoding:NSUTF8StringEncoding] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + + __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + strongSelf->_currentTask = nil; + + if (data != nil) { + NSError *decryptionError = nil; + NSDictionary *decrypted = [STDSJSONWebEncryption decryptData:data + withContentEncryptionKey:strongSelf->_acsContentEncryptionKey + error:&decryptionError]; + if (decrypted != nil) { + NSError *challengeResponseError = nil; + id response = [strongSelf decodeJSON:decrypted error:&challengeResponseError]; + completion(response, challengeResponseError); + } else { + completion(nil, decryptionError); + } + } else { + if (error.code == NSURLErrorTimedOut) { + // We convert timeout errors for convenience, since the SDK must treat them differently from generic network errors. + error = [NSError _stds_timedOutError]; + } + completion(nil, error); + } + + }]; + _currentTask = requestTask; + [requestTask resume]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, encryptionError); + }); + } +} + +- (void)sendErrorMessage:(STDSErrorMessage *)errorMessage { + NSDictionary *requestJSON = [STDSJSONEncoder dictionaryForObject:errorMessage]; + NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:_acsURL]; + urlRequest.HTTPMethod = @"POST"; + [urlRequest setValue:@"application/JSON; charset = UTF-8" forHTTPHeaderField:@"Content-Type"]; + NSURLSessionUploadTask *requestTask = [_urlSession uploadTaskWithRequest:[urlRequest copy] + fromData:[NSJSONSerialization dataWithJSONObject:requestJSON options:0 error:nil] + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + // no-op + }]; + [requestTask resume]; +} + +#pragma mark - Helpers + +/** + Returns an STDSChallengeResponseObject instance decoded from the given dict, or populates the error argument. + */ +- (nullable id)decodeJSON:(NSDictionary *)dict error:(NSError * _Nullable *)outError { + NSString *kErrorMessageType = @"Erro"; + NSString *kChallengeResponseType = @"CRes"; + NSString *messageType = dict[@"messageType"]; + NSError *error; + id decodedObject; + + if ([messageType isEqualToString:kErrorMessageType]) { + // Error message type + STDSErrorMessage *errorMessage = [STDSErrorMessage decodedObjectFromJSON:dict error:&error]; + if (errorMessage) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeReceivedErrorMessage + userInfo:@{STDSStripe3DS2ErrorMessageErrorKey: errorMessage}]; + } + } else if ([messageType isEqualToString:kChallengeResponseType]) { + // CRes message type + decodedObject = [STDSChallengeResponseObject decodedObjectFromJSON:dict error:&error]; + } else { + // Unknown message type + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeUnknownMessageType + userInfo:nil]; + } + + if (error && outError) { + *outError = error; + } + return decodedObject; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.h b/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.h new file mode 100644 index 00000000..78b2c491 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.h @@ -0,0 +1,20 @@ +// +// STDSAuthenticationResponseObject.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 5/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSAuthenticationResponse.h" +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSAuthenticationResponseObject : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.m b/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.m new file mode 100644 index 00000000..4a9bd175 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSAuthenticationResponseObject.m @@ -0,0 +1,100 @@ +// +// STDSAuthenticationResponseObject.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 5/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSAuthenticationResponseObject.h" + +#import "NSDictionary+DecodingHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSAuthenticationResponseObject + +@synthesize acsOperatorID = _acsOperatorID; +@synthesize acsReferenceNumber = _acsReferenceNumber; +@synthesize acsSignedContent = _acsSignedContent; +@synthesize acsTransactionID = _acsTransactionID; +@synthesize acsURL = _acsURL; +@synthesize cardholderInfo = _cardholderInfo; +@synthesize status = _status; +@synthesize challengeRequired = _challengeRequired; +@synthesize directoryServerReferenceNumber = _directoryServerReferenceNumber; +@synthesize directoryServerTransactionID = _directoryServerTransactionID; +@synthesize protocolVersion = _protocolVersion; +@synthesize sdkTransactionID = _sdkTransactionID; +@synthesize threeDSServerTransactionID = _threeDSServerTransactionID; +@synthesize willUseDecoupledAuthentication = _willUseDecoupledAuthentication; + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError **)outError { + if (json == nil) { + return nil; + } + STDSAuthenticationResponseObject *response = [[self alloc] init]; + NSError *error = nil; + + response->_threeDSServerTransactionID = [[json _stds_stringForKey:@"threeDSServerTransID" required:YES error:&error] copy]; + NSString *transStatusString = [json _stds_stringForKey:@"transStatus" required:NO error:&error]; + response->_status = [self statusTypeForString:transStatusString]; + response->_challengeRequired = (response->_status == STDSACSStatusTypeChallengeRequired); + response->_willUseDecoupledAuthentication = [[json _stds_boolForKey:@"acsDecConInd" required:NO error:&error] boolValue]; + response->_acsOperatorID = [[json _stds_stringForKey:@"acsOperatorID" required:NO error:&error] copy]; + response->_acsReferenceNumber = [[json _stds_stringForKey:@"acsReferenceNumber" required:NO error:&error] copy]; + response->_acsSignedContent = [[json _stds_stringForKey:@"acsSignedContent" required:NO error:&error] copy]; + response->_acsTransactionID = [[json _stds_stringForKey:@"acsTransID" required:YES error:&error] copy]; + response->_acsURL = [json _stds_urlForKey:@"acsURL" required:NO error:&error]; + response->_cardholderInfo = [[json _stds_stringForKey:@"cardholderInfo" required:NO error:&error] copy]; + response->_directoryServerReferenceNumber = [[json _stds_stringForKey:@"dsReferenceNumber" required:NO error:&error] copy]; + response->_directoryServerTransactionID = [[json _stds_stringForKey:@"dsTransID" required:NO error:&error] copy]; + response->_protocolVersion = [[json _stds_stringForKey:@"messageVersion" required:YES error:&error] copy]; + response->_sdkTransactionID = [[json _stds_stringForKey:@"sdkTransID" required:YES error:&error] copy]; + + if (error != nil) { + if (outError != nil) { + *outError = error; + } + + return nil; + } + + return response; +} + ++ (STDSACSStatusType)statusTypeForString:(NSString *)statusString { + if ([statusString isEqualToString:@"Y"]) { + return STDSACSStatusTypeAuthenticated; + } + if ([statusString isEqualToString:@"C"]) { + return STDSACSStatusTypeChallengeRequired; + } + if ([statusString isEqualToString:@"D"]) { + return STDSACSStatusTypeDecoupledAuthentication; + } + if ([statusString isEqualToString:@"N"]) { + return STDSACSStatusTypeNotAuthenticated; + } + if ([statusString isEqualToString:@"A"]) { + return STDSACSStatusTypeProofGenerated; + } + if ([statusString isEqualToString:@"U"]) { + return STDSACSStatusTypeError; + } + if ([statusString isEqualToString:@"R"]) { + return STDSACSStatusTypeRejected; + } + if ([statusString isEqualToString:@"I"]) { + return STDSACSStatusTypeInformationalOnly; + } + return STDSACSStatusTypeUnknown; +} + +@end + +id _Nullable STDSAuthenticationResponseFromJSON(NSDictionary *json) { + return [STDSAuthenticationResponseObject decodedObjectFromJSON:json error:NULL]; +} + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSBrandingView.h b/Stripe3DS2/Stripe3DS2/STDSBrandingView.h new file mode 100644 index 00000000..310e18a6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSBrandingView.h @@ -0,0 +1,23 @@ +// +// STDSBrandingView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSBrandingView: UIView + +/// The issuer image to present in the branding view. +@property (nonatomic, strong) UIImage *issuerImage; + +/// The payment system image to present in the branding view. +@property (nonatomic, strong) UIImage *paymentSystemImage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSBrandingView.m b/Stripe3DS2/Stripe3DS2/STDSBrandingView.m new file mode 100644 index 00000000..3f528ec1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSBrandingView.m @@ -0,0 +1,133 @@ +// +// STDSBrandingView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSBrandingView.h" +#import "STDSStackView.h" +#import "UIView+LayoutSupport.h" +#import "STDSVisionSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSBrandingView() + +@property (nonatomic, strong) STDSStackView *stackView; + +@property (nonatomic, strong) UIImageView *issuerImageView; +@property (nonatomic, strong) UIImageView *paymentSystemImageView; + +@property (nonatomic, strong) UIView *issuerView; +@property (nonatomic, strong) UIView *paymentSystemView; + +@end + +@implementation STDSBrandingView + +static const CGFloat kBrandingViewBottomPadding = 24; +static const CGFloat kBrandingViewSpacing = 16; +#if !STP_TARGET_VISION +static const CGFloat kImageViewBorderWidth = 1; +#endif +static const CGFloat kImageViewHorizontalInset = 7; +static const CGFloat kImageViewVerticalInset = 19; +static const CGFloat kImageViewCornerRadius = 6; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)setPaymentSystemImage:(UIImage *)paymentSystemImage { + _paymentSystemImage = paymentSystemImage; + + self.paymentSystemImageView.image = paymentSystemImage; +} + +- (void)setIssuerImage:(UIImage *)issuerImage { + _issuerImage = issuerImage; + + self.issuerImageView.image = issuerImage; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + +#if !STP_TARGET_VISION + if (self.window.screen.nativeScale > 0) { + self.issuerView.layer.borderWidth = kImageViewBorderWidth / self.window.screen.nativeScale; + self.paymentSystemView.layer.borderWidth = kImageViewBorderWidth / self.window.screen.nativeScale; + } +#endif +} + +- (void)_setupViewHierarchy { + self.layoutMargins = UIEdgeInsetsMake(0, 0, kBrandingViewBottomPadding, 0); + + self.stackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisHorizontal]; + [self addSubview:self.stackView]; + + [self.stackView _stds_pinToSuperviewBounds]; + + self.issuerImageView = [self _newBrandingImageView]; + self.issuerView = [self _newInsetViewWithImageView:self.issuerImageView]; + [self.stackView addArrangedSubview:self.issuerView]; + + [self.stackView addSpacer:kBrandingViewSpacing]; + + self.paymentSystemImageView = [self _newBrandingImageView]; + self.paymentSystemView = [self _newInsetViewWithImageView:self.paymentSystemImageView]; + [self.stackView addArrangedSubview:self.paymentSystemView]; + + NSLayoutConstraint *imageViewWidthConstraint = [NSLayoutConstraint constraintWithItem:self.issuerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:0.5 constant:0]; + // Setting the priority of the width constraint, so that the priority of the equal widths constraint below takes precedence, allowing both image views to take half of the remaining space equally. + imageViewWidthConstraint.priority = UILayoutPriorityDefaultHigh; + NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:self.paymentSystemView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.issuerView attribute:NSLayoutAttributeWidth multiplier:1 constant:0]; + + [NSLayoutConstraint activateConstraints:@[imageViewWidthConstraint, width]]; +} + +- (UIView *)_newInsetViewWithImageView:(UIImageView *)imageView { + UIView *insetView = [UIView new]; + insetView.layoutMargins = UIEdgeInsetsMake(kImageViewHorizontalInset, kImageViewVerticalInset, kImageViewHorizontalInset, kImageViewVerticalInset); + insetView.layer.cornerRadius = kImageViewCornerRadius; + insetView.backgroundColor = [UIColor whiteColor]; // Issuer images always expect a white background. + insetView.layer.masksToBounds = YES; + insetView.layer.borderColor = (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) ? + [UIColor colorWithRed:(CGFloat)0.0 green:(CGFloat)57.0/(CGFloat)255.0 blue:(CGFloat)69.0/(CGFloat)255.0 alpha:(CGFloat)0.25].CGColor : + [UIColor colorWithRed:(CGFloat)195.0/(CGFloat)255.0 green:(CGFloat)214.0/(CGFloat)255.0 blue:(CGFloat)218.0/(CGFloat)255.0 alpha:(CGFloat)0.25].CGColor; + + [insetView addSubview:imageView]; + [imageView _stds_pinToSuperviewBounds]; + + return insetView; +} + +- (UIImageView *)_newBrandingImageView { + UIImageView *imageView = [[UIImageView alloc] init]; + imageView.contentMode = UIViewContentModeScaleAspectFit; + + return imageView; +} + +#if !STP_TARGET_VISION +- (void)traitCollectionDidChange:(UITraitCollection * _Nullable)previousTraitCollection { + CGColorRef borderColor = (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) ? + [UIColor colorWithRed:(CGFloat)0.0 green:(CGFloat)57.0/(CGFloat)255.0 blue:(CGFloat)69.0/(CGFloat)255.0 alpha:(CGFloat)0.25].CGColor : + [UIColor colorWithRed:(CGFloat)195.0/(CGFloat)255.0 green:(CGFloat)214.0/(CGFloat)255.0 blue:(CGFloat)218.0/(CGFloat)255.0 alpha:(CGFloat)0.25].CGColor; + self.issuerView.layer.borderColor = borderColor; + self.paymentSystemView.layer.borderColor = borderColor; +} +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSBundleLocator.h b/Stripe3DS2/Stripe3DS2/STDSBundleLocator.h new file mode 100644 index 00000000..ed919fa5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSBundleLocator.h @@ -0,0 +1,15 @@ +// +// STDSBundleLocator.h +// Stripe3DS2 +// +// Created by David Estes on 7/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@interface STDSBundleLocator : NSObject + ++ (NSBundle *)stdsResourcesBundle; + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSBundleLocator.m b/Stripe3DS2/Stripe3DS2/STDSBundleLocator.m new file mode 100644 index 00000000..55384068 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSBundleLocator.m @@ -0,0 +1,109 @@ +// +// STDSBundleLocator.m +// Stripe3DS2 +// +// Created by David Estes on 7/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// Based on STPBundleLocator.m in Stripe.framework +// + +#import "STDSBundleLocator.h" + +/** + Using a private class to ensure that it can't be subclassed, which may + change the result of `bundleForClass` + */ +@interface STDSBundleLocatorInternal : NSObject +@end +@implementation STDSBundleLocatorInternal +@end + +@implementation STDSBundleLocator + +// This is copied from SPM's resource_bundle_accessor.m ++ (NSBundle *)stdsSPMBundle { + NSString *bundleName = @"Stripe_Stripe"; + + NSArray *candidates = @[ + NSBundle.mainBundle.resourceURL, + [NSBundle bundleForClass:[self class]].resourceURL, + NSBundle.mainBundle.bundleURL + ]; + + for (NSURL* candiate in candidates) { + NSURL *bundlePath = [candiate URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.bundle", bundleName]]; + + NSBundle *bundle = [NSBundle bundleWithURL:bundlePath]; + if (bundle != nil) { + return bundle; + } + } + + return nil; +} + ++ (NSBundle *)stdsResourcesBundle { + /** + First, find Stripe.framework. + Places to check: + 1. Stripe_Stripe3DS2.bundle (for SwiftPM) + 1. Stripe_Stripe.bundle (for SwiftPM) + 2. Stripe.bundle (for manual static installations, Fabric, and framework-less Cocoapods) + 3. Stripe.framework/Stripe.bundle (for framework-based Cocoapods) + 4. Stripe.framework (for Carthage, manual dynamic installations) + 5. main bundle (for people dragging all our files into their project) + **/ + + static NSBundle *ourBundle; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ +#ifdef SWIFTPM_MODULE_BUNDLE + ourBundle = SWIFTPM_MODULE_BUNDLE; +#endif + + if (ourBundle == nil) { + ourBundle = [STDSBundleLocator stdsSPMBundle]; + } + + if (ourBundle == nil) { + ourBundle = [NSBundle bundleWithPath:@"Stripe.bundle"]; + } + + if (ourBundle == nil) { + // This might be the same as the previous check if not using a dynamic framework + NSString *path = [[NSBundle bundleForClass:[STDSBundleLocatorInternal class]] pathForResource:@"Stripe" ofType:@"bundle"]; + ourBundle = [NSBundle bundleWithPath:path]; + } + + if (ourBundle == nil) { + // This will be the same as mainBundle if not using a dynamic framework + ourBundle = [NSBundle bundleForClass:[STDSBundleLocatorInternal class]]; + } + + if (ourBundle == nil) { + ourBundle = [NSBundle mainBundle]; + } + + // Once we've found Stripe.framework, seek around to find Stripe3DS2.bundle. + // Try to find Stripe3DS2 bundle within our current bundle + NSString *stdsBundlePath = [[ourBundle bundlePath] stringByAppendingPathComponent:@"Stripe3DS2.bundle"]; + NSBundle *stdsBundle = [NSBundle bundleWithPath:stdsBundlePath]; + if (stdsBundle != nil) { + ourBundle = stdsBundle; + } + // If it's not there, it might be a level up from us? + // (CocoaPods arranges us this way, as an example.) + if (stdsBundle == nil) { + NSString *stdsBundlePath = [[[ourBundle bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"Stripe3DS2.bundle"]; + stdsBundle = [NSBundle bundleWithPath:stdsBundlePath]; + if (stdsBundle != nil) { + ourBundle = stdsBundle; + } + } + }); + + return ourBundle; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.h b/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.h new file mode 100644 index 00000000..2111ea3f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.h @@ -0,0 +1,25 @@ +// +// STDSChallengeInformationView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSLabelCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeInformationView: UIView + +@property (nonatomic, strong, nullable) NSString *headerText; +@property (nonatomic, strong, nullable) UIImage *textIndicatorImage; +@property (nonatomic, strong, nullable) NSString *challengeInformationText; +@property (nonatomic, strong, nullable) NSString *challengeInformationLabel; + +@property (nonatomic, strong, nullable) STDSLabelCustomization *labelCustomization; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.m b/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.m new file mode 100644 index 00000000..6796dfe3 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeInformationView.m @@ -0,0 +1,137 @@ +// +// STDSChallengeInformationView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeInformationView.h" +#import "STDSStackView.h" +#import "STDSSpacerView.h" +#import "UIView+LayoutSupport.h" +#import "NSString+EmptyChecking.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeInformationView () + +@property (nonatomic, strong) STDSStackView *informationStackView; +@property (nonatomic, strong) STDSStackView *indicatorStackView; + +@property (nonatomic, strong) UILabel *headerLabel; +@property (nonatomic, strong) UIImageView *textIndicatorImageView; +@property (nonatomic, strong) UILabel *textLabel; +@property (nonatomic, strong) UILabel *informationLabel; +@property (nonatomic, strong) UIView *indicatorStackViewSpacerView; +@property (nonatomic, strong) UIView *indicatorImageTextSpacerView; + +@end + +@implementation STDSChallengeInformationView + +static const CGFloat kHeaderTextBottomPadding = 8; +static const CGFloat kInformationTextBottomPadding = 20; +static const CGFloat kChallengeInformationViewBottomPadding = 6; +static const CGFloat kTextIndicatorHorizontalPadding = 8; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)setHeaderText:(NSString * _Nullable)headerText { + _headerText = headerText; + + self.headerLabel.text = headerText; + self.headerLabel.hidden = [NSString _stds_isStringEmpty:headerText]; +} + +- (void)setTextIndicatorImage:(UIImage * _Nullable)textIndicatorImage { + _textIndicatorImage = textIndicatorImage; + + self.textIndicatorImageView.image = textIndicatorImage; + self.textIndicatorImageView.hidden = textIndicatorImage == nil; + self.indicatorImageTextSpacerView.hidden = textIndicatorImage == nil; +} + +- (void)setChallengeInformationText:(NSString * _Nullable)challengeInformationText { + _challengeInformationText = challengeInformationText; + + self.textLabel.text = challengeInformationText; + self.textLabel.hidden = [NSString _stds_isStringEmpty:challengeInformationText]; +} + +- (void)setChallengeInformationLabel:(NSString * _Nullable)challengeInformationLabel { + _challengeInformationLabel = challengeInformationLabel; + + self.informationLabel.text = challengeInformationLabel; + self.informationLabel.hidden = [NSString _stds_isStringEmpty:challengeInformationLabel]; + self.indicatorStackViewSpacerView.hidden = self.informationLabel.hidden; +} + +- (void)_setupViewHierarchy { + self.layoutMargins = UIEdgeInsetsMake(0, 0, kChallengeInformationViewBottomPadding, 0); + + self.headerLabel = [self _newInformationLabel]; + + self.textIndicatorImageView = [[UIImageView alloc] init]; + self.textIndicatorImageView.contentMode = UIViewContentModeTop; + self.textIndicatorImageView.hidden = YES; + + self.textLabel = [self _newInformationLabel]; + self.informationLabel = [self _newInformationLabel]; + + self.indicatorStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisHorizontal]; + + [self.indicatorStackView addArrangedSubview:self.textIndicatorImageView]; + self.indicatorImageTextSpacerView = [[STDSSpacerView alloc] initWithLayoutAxis:STDSStackViewLayoutAxisHorizontal dimension:kTextIndicatorHorizontalPadding]; + self.indicatorImageTextSpacerView.hidden = YES; + [self.indicatorStackView addArrangedSubview:self.indicatorImageTextSpacerView]; + [self.indicatorStackView addArrangedSubview:self.textLabel]; + + self.informationStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [self.informationStackView addArrangedSubview:self.headerLabel]; + [self.informationStackView addSpacer:kHeaderTextBottomPadding]; + [self.informationStackView addArrangedSubview:self.indicatorStackView]; + self.indicatorStackViewSpacerView = [[STDSSpacerView alloc] initWithLayoutAxis:STDSStackViewLayoutAxisVertical dimension:kInformationTextBottomPadding]; + [self.informationStackView addArrangedSubview:self.indicatorStackViewSpacerView]; + [self.informationStackView addArrangedSubview:self.informationLabel]; + + [self addSubview:self.informationStackView]; + + [self.informationStackView _stds_pinToSuperviewBounds]; + + NSLayoutConstraint *imageViewWidthConstraint = [NSLayoutConstraint constraintWithItem:self.textIndicatorImageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:35]; + [NSLayoutConstraint activateConstraints:@[imageViewWidthConstraint]]; +} + +- (void)setLabelCustomization:(STDSLabelCustomization * _Nullable)labelCustomization { + _labelCustomization = labelCustomization; + + self.headerLabel.font = labelCustomization.headingFont; + self.headerLabel.textColor = labelCustomization.headingTextColor; + + self.textLabel.font = labelCustomization.font; + self.textLabel.textColor = labelCustomization.textColor; + + self.informationLabel.font = labelCustomization.font; + self.informationLabel.textColor = labelCustomization.textColor; +} + +- (UILabel *)_newInformationLabel { + UILabel *label = [[UILabel alloc] init]; + label.numberOfLines = 0; + label.hidden = YES; + + return label; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.h b/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.h new file mode 100644 index 00000000..d0809963 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.h @@ -0,0 +1,138 @@ +// +// STDSChallengeRequestParameters.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 4/1/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONEncodable.h" + +@class STDSChallengeParameters; + +typedef NS_ENUM(NSInteger, STDSChallengeCancelType) { + /// The cardholder selected "Cancel" from the UI + STDSChallengeCancelTypeCardholderSelectedCancel, + + /// The transaction timed out + STDSChallengeCancelTypeTransactionTimedOut, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeRequestParameters : NSObject + +/** + Convenience initializer to create parameters for the first Challenge Request for a transaction. + */ +- (instancetype)initWithChallengeParameters:(STDSChallengeParameters *)challengeParams + transactionIdentifier:(NSString *)transactionIdentifier + messageVersion:(NSString *)messageVersion; + +/** + Designated initializer for `STDSChallengeRequestParameters` + */ +- (instancetype)initWithThreeDSServerTransactionIdentifier:(NSString *)threeDSServerTransactionIdentifier + acsTransactionIdentifier:(NSString *)acsTransactionIdentifier + messageVersion:(NSString *)messageVersion + sdkTransactionIdentifier:(NSString *)sdkTransactionIdentifier + requestorAppUrl:(NSString *)requestorAppUrl + sdkCounterStoA:(NSInteger)sdkCounterStoA NS_DESIGNATED_INITIALIZER; + +/** + Returns a new instance of STDSChallengeRequestParameters using the receiver, copying over the properties that are invariant across all CReqs for a given transaction and incrementing sdkCounterStoA. + */ +- (instancetype)nextChallengeRequestParametersByIncrementCounter; + +- (instancetype)init NS_UNAVAILABLE; + +#pragma mark - Required Properties + +/** + Universally unique transaction identifier assigned by the 3DS SDK to identify a single transaction. + */ +@property (nonatomic, readonly) NSString *sdkTransactionIdentifier; + +/** + Transaction identifier assigned by the 3DS Server to uniquely identify + a transaction. + */ +@property (nonatomic, readonly) NSString *threeDSServerTransactionIdentifier; + +/** + Transaction identifier assigned by the Access Control Server (ACS) + to uniquely identify a transaction. + */ +@property (nonatomic, readonly) NSString *acsTransactionIdentifier; + +/** + Identifies the type of message - always "CReq" + */ +@property (nonatomic, readonly) NSString *messageType; + +/** + The protocol version that is supported by the SDK and used for the transaction. + */ +@property (nonatomic, readonly) NSString *messageVersion; + +/** + Counter used as a security measure in the 3DS SDK to ACS secure channel. + */ +@property (nonatomic, readonly) NSString *sdkCounterStoA; + +#pragma mark - Optional/Conditional Properties + +/** + The URL for the application that is requesting 3DS2 verification. + This property can be optionally set and will be included with the + messages sent to the Directory Server during the challenge flow. + */ +@property (nonatomic, copy, nullable) NSString *threeDSRequestorAppURL; + +/** + A STDSChallengeCancelType wrapped in NSNumber, indicating that the authentication has been canceled. + */ +@property (nonatomic, copy, nullable) NSNumber *challengeCancel; + +/** + Contains the data that the Cardholder entered into the Native UI text field. + + @note The setter converts empty strings to nil. + */ +@property (nonatomic, copy, nullable) NSString *challengeDataEntry; + +/** + Data that the Cardholder entered into the HTML UI. + */ +@property (nonatomic, copy, nullable) NSString *challengeHTMLDataEntry; + +/** + Data necessary to support requirements not otherwise defined in the 3- D Secure message. + */ +@property (nonatomic, copy, nullable) NSArray *messageExtension; + +/** + A BOOL indiciating that Cardholder has completed the authentication as requested by selecting the Continue button in an Out- of-Band (OOB) authentication method. + */ +@property (nonatomic, nullable) NSNumber *oobContinue; + +/** + Indicator to resend the challenge information code to the Cardholder. + */ +@property (nonatomic, copy, nullable) NSString *resendChallenge; + +/** + Indicator confirming whether whitelisting was opted by the cardholder. + */ +@property (nonatomic, copy, nullable) NSString *whitelistingDataEntry; + +/** + Indicator informing that the Cardholder submits an empty response (no data entered in the UI). + */ +@property (nonatomic, copy, nullable) NSString *challengeNoEntry; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.m b/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.m new file mode 100644 index 00000000..18fcb8db --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeRequestParameters.m @@ -0,0 +1,105 @@ +// +// STDSChallengeRequestParameters.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 4/1/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeRequestParameters.h" + +#import "STDSChallengeParameters.h" + +@implementation STDSChallengeRequestParameters + +- (instancetype)initWithChallengeParameters:(STDSChallengeParameters *)challengeParams + transactionIdentifier:(NSString *)transactionIdentifier + messageVersion:(NSString *)messageVersion { + return [self initWithThreeDSServerTransactionIdentifier:challengeParams.threeDSServerTransactionID + acsTransactionIdentifier:challengeParams.acsTransactionID + messageVersion:messageVersion + sdkTransactionIdentifier:transactionIdentifier + requestorAppUrl:challengeParams.threeDSRequestorAppURL + sdkCounterStoA:0]; +} + +- (instancetype)initWithThreeDSServerTransactionIdentifier:(NSString *)threeDSServerTransactionIdentifier + acsTransactionIdentifier:(NSString *)acsTransactionIdentifier + messageVersion:(NSString *)messageVersion + sdkTransactionIdentifier:(NSString *)sdkTransactionIdentifier + requestorAppUrl:(NSString *)requestorAppUrl + sdkCounterStoA:(NSInteger)sdkCounterStoA { + self = [super init]; + if (self) { + _messageType = @"CReq"; + _threeDSServerTransactionIdentifier = [threeDSServerTransactionIdentifier copy]; + _acsTransactionIdentifier = [acsTransactionIdentifier copy]; + _messageVersion = [messageVersion copy]; + _sdkTransactionIdentifier = [sdkTransactionIdentifier copy]; + _threeDSRequestorAppURL = [requestorAppUrl copy]; + _sdkCounterStoA = [NSString stringWithFormat:@"%03ld", (long)sdkCounterStoA]; + } + return self; +} + +- (instancetype)nextChallengeRequestParametersByIncrementCounter { + NSInteger incrementedCounter = [self.sdkCounterStoA intValue] + 1; + return [[STDSChallengeRequestParameters alloc] initWithThreeDSServerTransactionIdentifier:self.threeDSServerTransactionIdentifier + acsTransactionIdentifier:self.acsTransactionIdentifier + messageVersion:self.messageVersion + sdkTransactionIdentifier:self.sdkTransactionIdentifier + requestorAppUrl:self.threeDSRequestorAppURL // TC_SDK_10209_001 + sdkCounterStoA:incrementedCounter]; +} + +- (void)setChallengeDataEntry:(NSString *)challengeDataEntry { + // [Req 40] ...if the cardholder has submitted the response without entering any data in the UI, the Challenge Data Entry field shall not be present in the CReq message. + if (challengeDataEntry.length == 0) { + _challengeDataEntry = nil; + _challengeNoEntry = @"Y"; + } else { + _challengeDataEntry = [challengeDataEntry copy]; + _challengeNoEntry = nil; + } +} + +#pragma mark - Helpers + +- (nullable NSString *)challengeCancelString { + if (self.challengeCancel == nil) { + return nil; + } + + STDSChallengeCancelType challengeCancelType = (STDSChallengeCancelType)[self.challengeCancel integerValue]; + switch (challengeCancelType) { + case STDSChallengeCancelTypeCardholderSelectedCancel: + return @"01"; + case STDSChallengeCancelTypeTransactionTimedOut: + return @"08"; + } + return @"07"; // Unknown +} + +#pragma mark - STDSJSONEncodable + ++ (NSDictionary *)propertyNamesToJSONKeysMapping { + return @{ + NSStringFromSelector(@selector(threeDSServerTransactionIdentifier)): @"threeDSServerTransID", + NSStringFromSelector(@selector(acsTransactionIdentifier)): @"acsTransID", + NSStringFromSelector(@selector(threeDSRequestorAppURL)): @"threeDSRequestorAppURL", + NSStringFromSelector(@selector(challengeCancelString)): @"challengeCancel", + NSStringFromSelector(@selector(challengeDataEntry)): @"challengeDataEntry", + NSStringFromSelector(@selector(challengeHTMLDataEntry)): @"challengeHTMLDataEntry", + NSStringFromSelector(@selector(challengeNoEntry)): @"challengeNoEntry", + NSStringFromSelector(@selector(messageExtension)): @"messageExtension", + NSStringFromSelector(@selector(messageVersion)): @"messageVersion", + NSStringFromSelector(@selector(messageType)): @"messageType", + NSStringFromSelector(@selector(oobContinue)): @"oobContinue", + NSStringFromSelector(@selector(resendChallenge)): @"resendChallenge", + NSStringFromSelector(@selector(sdkTransactionIdentifier)): @"sdkTransID", + NSStringFromSelector(@selector(sdkCounterStoA)): @"sdkCounterStoA", + NSStringFromSelector(@selector(whitelistingDataEntry)): @"whitelistingDataEntry", + }; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponse.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponse.h new file mode 100644 index 00000000..58606d25 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponse.h @@ -0,0 +1,145 @@ +// +// STDSChallengeResponse.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseSelectionInfo.h" +#import "STDSChallengeResponseMessageExtension.h" +#import "STDSChallengeResponseImage.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The `STDSACSUIType` enum defines the type of UI to be presented. + */ +typedef NS_ENUM(NSInteger, STDSACSUIType) { + + /// No UI associated with the response. + STDSACSUITypeNone = 0, + + /// Text challenge response UI. + STDSACSUITypeText = 1, + + /// Single-select challenge response UI. + STDSACSUITypeSingleSelect = 2, + + /// Multi-select challenge response UI. + STDSACSUITypeMultiSelect = 3, + + /// Out Of Band challenge response UI. + STDSACSUITypeOOB = 4, + + /// HTML challenge response UI. + STDSACSUITypeHTML = 5, +}; + +/// A protocol that represents the information contained within a challenge response. +@protocol STDSChallengeResponse + +/// Universally unique transaction identifier assigned by the 3DS Server to identify a single transaction. +@property (nonatomic, readonly) NSString *threeDSServerTransactionID; + +/// Counter used as a security measure in the ACS to 3DS SDK secure channel. +@property (nonatomic, readonly) NSString *acsCounterACStoSDK; + +/// Universally unique transaction identifier assigned by the ACS to identify a single transaction. +@property (nonatomic, readonly) NSString *acsTransactionID; + +/// HTML provided by the ACS in the Challenge Response message. Utilised when HTML is specified in the ACS UI Type during the Cardholder challenge. +@property (nonatomic, readonly, nullable) NSString *acsHTML; + +/// Optional HTML provided by the ACS in the CRes message to be utilised in the Out of Band flow when the HTML is specified in the ACS UI Type during the Cardholder challenge, displayed when the app is moved to the foreground. +@property (nonatomic, readonly, nullable) NSString *acsHTMLRefresh; + +/// User interface type that the 3DS SDK will render, which includes the specific data mapping and requirements. +@property (nonatomic, readonly) STDSACSUIType acsUIType; + +/** + Indicator of the state of the ACS challenge cycle and whether the challenge has completed or will require additional messages. Shall be populated in all Challenge Response messages to convey the current state of the transaction. + + - Note: + If set to YES, the ACS will populate the Transaction Status in the Challenge Response message. + */ +@property (nonatomic, readonly) BOOL challengeCompletionIndicator; + +/// Header text that for the challenge information screen that is being presented. +@property (nonatomic, readonly, nullable) NSString *challengeInfoHeader; + +/// Label to modify the Challenge Data Entry field provided by the Issuer. +@property (nonatomic, readonly, nullable) NSString *challengeInfoLabel; + +/// Text provided by the ACS/Issuer to Cardholder during the Challenge Message exchange. +@property (nonatomic, readonly, nullable) NSString *challengeInfoText; + +/// Text provided by the ACS/Issuer to Cardholder during OOB authentication to replace Challenge Information Text and Challenge Information Text Indicator +@property (nonatomic, readonly, nullable) NSString *challengeAdditionalInfoText; + +/// Indicates when the Issuer/ACS would like a warning icon or similar visual indicator to draw attention to the “Challenge Information Text” that is being displayed. +@property (nonatomic, readonly) BOOL showChallengeInfoTextIndicator; + +/// Selection information that will be presented to the Cardholder if the option is single or multi-select. The variables will be sent in a JSON Array and parsed by the SDK for display in the user interface. +@property (nonatomic, readonly, nullable) NSArray> *challengeSelectInfo; + +/// Label displayed to the Cardholder for the content in Expandable Information Text. +@property (nonatomic, readonly, nullable) NSString *expandInfoLabel; + +/// Text provided by the Issuer from the ACS to be displayed to the Cardholder for additional information and the format will be an expandable text field. +@property (nonatomic, readonly, nullable) NSString *expandInfoText; + +/// Sent in the initial Challenge Response message from the ACS to the 3DS SDK to provide the URL(s) of the Issuer logo or image to be used in the Native UI. +@property (nonatomic, readonly, nullable) id issuerImage; + +/// Data necessary to support requirements not otherwise defined in the 3-D Secure message are carried in a Message Extension. +@property (nonatomic, readonly, nullable) NSArray> *messageExtensions; + +/// Identifies the type of message that is passed. +@property (nonatomic, readonly) NSString *messageType; + +/// Protocol version identifier. This shall be the Protocol Version Number of the specification utilised by the system creating this message. The Message Version Number is set by the 3DS Server which originates the protocol with the AReq message. The Message Version Number does not change during a 3DS transaction. +@property (nonatomic, readonly) NSString *messageVersion; + +/// Mobile Deep link to an authentication app used in the out-of-band authentication. The App URL will open the appropriate location within the authentication app. +@property (nonatomic, readonly, nullable) NSURL *oobAppURL; + +/// Label to be displayed for the link to the OOB App URL. For example: “oobAppLabel”: “Click here to open Your Bank App” +@property (nonatomic, readonly, nullable) NSString *oobAppLabel; + +/// Label to be used in the UI for the button that the user selects when they have completed the OOB authentication. +@property (nonatomic, readonly, nullable) NSString *oobContinueLabel; + +/// Sent in the initial Challenge Response message from the ACS to the 3DS SDK to provide the URL(s) of the DS or Payment System logo or image to be used in the Native UI. +@property (nonatomic, readonly, nullable) id paymentSystemImage; + +/// Label to be used in the UI for the button that the user selects when they would like to have the authentication information present. +@property (nonatomic, readonly, nullable) NSString *resendInformationLabel; + +/// Universally unique transaction identifier assigned by the 3DS SDK to identify a single transaction. +@property (nonatomic, readonly) NSString *sdkTransactionID; + +/** + Label to be used in the UI for the button that the user selects when they have completed the authentication. + + - Note: + This is not used for OOB authentication. + */ +@property (nonatomic, readonly, nullable) NSString *submitAuthenticationLabel; + +/// Text provided by the ACS/Issuer to Cardholder during a Whitelisting transaction. For example, “Would you like to add this Merchant to your whitelist?” +@property (nonatomic, readonly, nullable) NSString *whitelistingInfoText; + +/// Label to be displayed to the Cardholder for the "why" information section. +@property (nonatomic, readonly, nullable) NSString *whyInfoLabel; + +/// Text provided by the Issuer to be displayed to the Cardholder to explain why the Cardholder is being asked to perform the authentication task. +@property (nonatomic, readonly, nullable) NSString *whyInfoText; + +/// Indicates the state of the associated Transaction. +@property (nonatomic, readonly, nullable) NSString *transactionStatus; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImage.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImage.h new file mode 100644 index 00000000..9128d77d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImage.h @@ -0,0 +1,27 @@ +// +// STDSChallengeResponseImage.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol used to represent information about an individual image resource inside of a challenge response. +@protocol STDSChallengeResponseImage + +/// A medium density image to display as the issuer image. +@property (nonatomic, readonly, nullable) NSURL *mediumDensityURL; + +/// A high density image to display as the issuer image. +@property (nonatomic, readonly, nullable) NSURL *highDensityURL; + +/// An extra-high density image to display as the issuer image. +@property (nonatomic, readonly, nullable) NSURL *extraHighDensityURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.h new file mode 100644 index 00000000..59e21208 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.h @@ -0,0 +1,23 @@ +// +// STDSChallengeResponseImageObject.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseImage.h" + +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An object used to represent information about an individual image resource inside of a challenge response. +@interface STDSChallengeResponseImageObject: NSObject + +- (instancetype)initWithMediumDensityURL:(NSURL * _Nullable)mediumDensityURL highDensityURL:(NSURL * _Nullable)highDensityURL extraHighDensityURL:(NSURL * _Nullable)extraHighDensityURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.m b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.m new file mode 100644 index 00000000..a3ea6840 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseImageObject.m @@ -0,0 +1,53 @@ +// +// STDSChallengeResponseImageObject.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseImageObject.h" + +#import "NSDictionary+DecodingHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeResponseImageObject() + +@property (nonatomic, nullable) NSURL *mediumDensityURL; +@property (nonatomic, nullable) NSURL *highDensityURL; +@property (nonatomic, nullable) NSURL *extraHighDensityURL; + +@end + +@implementation STDSChallengeResponseImageObject + +- (instancetype)initWithMediumDensityURL:(NSURL * _Nullable)mediumDensityURL highDensityURL:(NSURL * _Nullable)highDensityURL extraHighDensityURL:(NSURL * _Nullable)extraHighDensityURL { + self = [super init]; + + if (self) { + _mediumDensityURL = mediumDensityURL; + _highDensityURL = highDensityURL; + _extraHighDensityURL = extraHighDensityURL; + } + + return self; +} + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError * _Nullable __autoreleasing * _Nullable)outError { + if (json == nil) { + return nil; + } + + NSURL *mediumDensityURL = [json _stds_urlForKey:@"medium" required:NO error:nil]; + NSURL *highDensityURL = [json _stds_urlForKey:@"high" required:NO error:nil]; + NSURL *extraHighDensityURL = [json _stds_urlForKey:@"extraHigh" required:NO error:nil]; + + return [[STDSChallengeResponseImageObject alloc] initWithMediumDensityURL:mediumDensityURL + highDensityURL:highDensityURL + extraHighDensityURL:extraHighDensityURL]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtension.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtension.h new file mode 100644 index 00000000..c26425f9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtension.h @@ -0,0 +1,30 @@ +// +// STDSChallengeResponseMessageExtension.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol that encapsulates an individual message extension inside of a challenge response. +@protocol STDSChallengeResponseMessageExtension + +/// The name of the extension data set as defined by the extension owner. +@property (nonatomic, readonly) NSString *name; + +/// A unique identifier for the extension. +@property (nonatomic, readonly) NSString *identifier; + +/// A Boolean value indicating whether the recipient must understand the contents of the extension to interpret the entire message. +@property (nonatomic, readonly, getter = isCriticalityIndicator) BOOL criticalityIndicator; + +/// The data carried in the extension. +@property (nonatomic, readonly) NSDictionary *data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.h new file mode 100644 index 00000000..5ece844b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.h @@ -0,0 +1,21 @@ +// +// STDSChallengeResponseMessageExtensionObject.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseMessageExtension.h" + +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An object used to represent an individual message extension inside of a challenge response. +@interface STDSChallengeResponseMessageExtensionObject: NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.m b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.m new file mode 100644 index 00000000..54e58d42 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseMessageExtensionObject.m @@ -0,0 +1,72 @@ +// +// STDSChallengeResponseMessageExtensionObject.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseMessageExtensionObject.h" + +#import "NSDictionary+DecodingHelpers.h" +#import "NSError+Stripe3DS2.h" + +NS_ASSUME_NONNULL_BEGIN + +static const NSInteger kMaximumStringFieldLength = 64; +static const NSInteger kMaximumDataFieldLength = 8059; + +@implementation STDSChallengeResponseMessageExtensionObject + +@synthesize name = _name; +@synthesize identifier = _identifier; +@synthesize criticalityIndicator = _criticalityIndicator; +@synthesize data = _data; + +- (instancetype)initWithName:(NSString *)name identifier:(NSString *)identifier criticalityIndicator:(BOOL)criticalityIndicator data:(NSDictionary *)data { + self = [super init]; + if (self) { + _name = [name copy]; + _identifier = [identifier copy]; + _criticalityIndicator = criticalityIndicator; + _data = data; + } + return self; +} + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError * _Nullable __autoreleasing * _Nullable)outError { + if (json == nil) { + return nil; + } + NSError *error; + + NSString *name = [json _stds_stringForKey:@"name" validator:^BOOL (NSString *value) { + return value.length <= kMaximumStringFieldLength; + }required:YES error:&error]; + NSString *identifier = [json _stds_stringForKey:@"id" validator:^BOOL (NSString *value) { + return value.length <= kMaximumStringFieldLength; + } required:YES error:&error]; + BOOL criticalityIndicator= [json _stds_boolForKey:@"criticalityIndicator" required:YES error:&error].boolValue; + NSDictionary *data = [json _stds_dictionaryForKey:@"data" required:YES error:&error]; + // The spec requires data to be "Maximum 8059 characters" + if (data && [NSJSONSerialization dataWithJSONObject:data options:0 error:nil].length > kMaximumDataFieldLength) { + error = [NSError _stds_invalidJSONFieldError:@"data"]; + } + + if (error) { + if (outError) { + *outError = error; + } + return nil; + } + + if (data != nil) { + return [[STDSChallengeResponseMessageExtensionObject alloc] initWithName:name identifier:identifier criticalityIndicator:criticalityIndicator data:data]; + } else { + return nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.h new file mode 100644 index 00000000..1f245286 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.h @@ -0,0 +1,49 @@ +// +// STDSChallengeResponseObject.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponse.h" +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An object used to represent a challenge response from the ACS. +@interface STDSChallengeResponseObject: NSObject + +- (instancetype)initWithThreeDSServerTransactionID:(NSString *)threeDSServerTransactionID + acsCounterACStoSDK:(NSString *)acsCounterACStoSDK + acsTransactionID:(NSString *)acsTransactionID + acsHTML:(NSString * _Nullable)acsHTML + acsHTMLRefresh:(NSString * _Nullable)acsHTMLRefresh + acsUIType:(STDSACSUIType)acsUIType + challengeCompletionIndicator:(BOOL)challengeCompletionIndicator + challengeInfoHeader:(NSString * _Nullable)challengeInfoHeader + challengeInfoLabel:(NSString * _Nullable)challengeInfoLabel + challengeInfoText:(NSString * _Nullable)challengeInfoText + challengeAdditionalInfoText:(NSString * _Nullable)challengeAdditionalInfoText + showChallengeInfoTextIndicator:(BOOL)showChallengeInfoTextIndicator + challengeSelectInfo:(NSArray> * _Nullable)challengeSelectInfo + expandInfoLabel:(NSString * _Nullable)expandInfoLabel + expandInfoText:(NSString * _Nullable)expandInfoText + issuerImage:(id _Nullable)issuerImage + messageExtensions:(NSArray> * _Nullable)messageExtensions + messageVersion:(NSString *)messageVersion + oobAppURL:(NSURL * _Nullable)oobAppURL + oobAppLabel:(NSString * _Nullable)oobAppLabel + oobContinueLabel:(NSString * _Nullable)oobContinueLabel + paymentSystemImage:(id _Nullable)paymentSystemImage + resendInformationLabel:(NSString * _Nullable)resendInformationLabel + sdkTransactionID:(NSString *)sdkTransactionID + submitAuthenticationLabel:(NSString * _Nullable)submitAuthenticationLabel + whitelistingInfoText:(NSString * _Nullable)whitelistingInfoText + whyInfoLabel:(NSString * _Nullable)whyInfoLabel + whyInfoText:(NSString * _Nullable)whyInfoText + transactionStatus:(NSString * _Nullable)transactionStatus; +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.m b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.m new file mode 100644 index 00000000..7d952bb9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseObject.m @@ -0,0 +1,321 @@ +// +// STDSChallengeResponseObject.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseObject.h" + +#import "NSDictionary+DecodingHelpers.h" +#import "NSError+Stripe3DS2.h" +#import "STDSChallengeResponseSelectionInfoObject.h" +#import "STDSChallengeResponseImageObject.h" +#import "STDSChallengeResponseMessageExtensionObject.h" +#import "NSString+JWEHelpers.h" +#import "STDSStripe3DS2Error.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSChallengeResponseObject + +@synthesize threeDSServerTransactionID = _threeDSServerTransactionID; +@synthesize acsCounterACStoSDK = _acsCounterACStoSDK; +@synthesize acsTransactionID = _acsTransactionID; +@synthesize acsHTML = _acsHTML; +@synthesize acsHTMLRefresh = _acsHTMLRefresh; +@synthesize acsUIType = _acsUIType; +@synthesize challengeCompletionIndicator = _challengeCompletionIndicator; +@synthesize challengeInfoHeader = _challengeInfoHeader; +@synthesize challengeInfoLabel = _challengeInfoLabel; +@synthesize challengeInfoText = _challengeInfoText; +@synthesize challengeAdditionalInfoText = _challengeAdditionalInfoText; +@synthesize showChallengeInfoTextIndicator = _showChallengeInfoTextIndicator; +@synthesize challengeSelectInfo = _challengeSelectInfo; +@synthesize expandInfoLabel = _expandInfoLabel; +@synthesize expandInfoText = _expandInfoText; +@synthesize issuerImage = _issuerImage; +@synthesize messageExtensions = _messageExtensions; +@synthesize messageType = _messageType; +@synthesize messageVersion = _messageVersion; +@synthesize oobAppURL = _oobAppURL; +@synthesize oobAppLabel = _oobAppLabel; +@synthesize oobContinueLabel = _oobContinueLabel; +@synthesize paymentSystemImage = _paymentSystemImage; +@synthesize resendInformationLabel = _resendInformationLabel; +@synthesize sdkTransactionID = _sdkTransactionID; +@synthesize submitAuthenticationLabel = _submitAuthenticationLabel; +@synthesize whitelistingInfoText = _whitelistingInfoText; +@synthesize whyInfoLabel = _whyInfoLabel; +@synthesize whyInfoText = _whyInfoText; +@synthesize transactionStatus = _transactionStatus; + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ -- completion: %@, count: %@", [super description], @(self.challengeCompletionIndicator), self.acsCounterACStoSDK]; +} + +- (instancetype)initWithThreeDSServerTransactionID:(NSString *)threeDSServerTransactionID + acsCounterACStoSDK:(NSString *)acsCounterACStoSDK + acsTransactionID:(NSString *)acsTransactionID + acsHTML:(NSString * _Nullable)acsHTML + acsHTMLRefresh:(NSString * _Nullable)acsHTMLRefresh + acsUIType:(STDSACSUIType)acsUIType + challengeCompletionIndicator:(BOOL)challengeCompletionIndicator + challengeInfoHeader:(NSString * _Nullable)challengeInfoHeader + challengeInfoLabel:(NSString * _Nullable)challengeInfoLabel + challengeInfoText:(NSString * _Nullable)challengeInfoText + challengeAdditionalInfoText:(NSString * _Nullable)challengeAdditionalInfoText + showChallengeInfoTextIndicator:(BOOL)showChallengeInfoTextIndicator + challengeSelectInfo:(NSArray> * _Nullable)challengeSelectInfo + expandInfoLabel:(NSString * _Nullable)expandInfoLabel + expandInfoText:(NSString * _Nullable)expandInfoText + issuerImage:(id _Nullable)issuerImage + messageExtensions:(NSArray> * _Nullable)messageExtensions + messageVersion:(NSString *)messageVersion + oobAppURL:(NSURL * _Nullable)oobAppURL + oobAppLabel:(NSString * _Nullable)oobAppLabel + oobContinueLabel:(NSString * _Nullable)oobContinueLabel + paymentSystemImage:(id _Nullable)paymentSystemImage + resendInformationLabel:(NSString * _Nullable)resendInformationLabel + sdkTransactionID:(NSString *)sdkTransactionID + submitAuthenticationLabel:(NSString * _Nullable)submitAuthenticationLabel + whitelistingInfoText:(NSString * _Nullable)whitelistingInfoText + whyInfoLabel:(NSString * _Nullable)whyInfoLabel + whyInfoText:(NSString * _Nullable)whyInfoText + transactionStatus:(NSString * _Nullable)transactionStatus { + self = [super init]; + + if (self) { + _threeDSServerTransactionID = [threeDSServerTransactionID copy]; + _acsCounterACStoSDK = [acsCounterACStoSDK copy]; + _acsTransactionID = [acsTransactionID copy]; + _acsHTML = [acsHTML copy]; + _acsHTMLRefresh = [acsHTMLRefresh copy]; + _acsUIType = acsUIType; + _challengeCompletionIndicator = challengeCompletionIndicator; + _challengeInfoHeader = [challengeInfoHeader copy]; + _challengeInfoLabel = [challengeInfoLabel copy]; + _challengeInfoText = [challengeInfoText copy]; + _challengeAdditionalInfoText = [challengeAdditionalInfoText copy]; + _showChallengeInfoTextIndicator = showChallengeInfoTextIndicator; + _challengeSelectInfo = [challengeSelectInfo copy]; + _expandInfoLabel = [expandInfoLabel copy]; + _expandInfoText = [expandInfoText copy]; + _issuerImage = issuerImage; + _messageExtensions = [messageExtensions copy]; + _messageType = @"CRes"; + _messageVersion = [messageVersion copy]; + _oobAppURL = oobAppURL; + _oobAppLabel = [oobAppLabel copy]; + _oobContinueLabel = [oobContinueLabel copy]; + _paymentSystemImage = paymentSystemImage; + _resendInformationLabel = [resendInformationLabel copy]; + _sdkTransactionID = [sdkTransactionID copy]; + _submitAuthenticationLabel = [submitAuthenticationLabel copy]; + _whitelistingInfoText = [whitelistingInfoText copy]; + _whyInfoLabel = [whyInfoLabel copy]; + _whyInfoText = [whyInfoText copy]; + _transactionStatus = [transactionStatus copy]; + } + + return self; +} + +#pragma mark Private Helpers + ++ (NSDictionary *)acsUITypeStringMapping { + return @{ + @"01": @(STDSACSUITypeText), + @"02": @(STDSACSUITypeSingleSelect), + @"03": @(STDSACSUITypeMultiSelect), + @"04": @(STDSACSUITypeOOB), + @"05": @(STDSACSUITypeHTML), + }; +} + +/// The message extension identifiers that we support. ++ (NSSet *)supportedMessageExtensions { + return [NSSet new]; +} + +#pragma mark STDSJSONDecodable + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError **)outError { + if (json == nil) { + return nil; + } + NSError *error; + +#pragma mark Required + NSString *threeDSServerTransactionID = [json _stds_stringForKey:@"threeDSServerTransID" validator:^BOOL (NSString *value) { + return [[NSUUID alloc] initWithUUIDString:value] != nil; + } required:YES error:&error]; + NSString *acsCounterACStoSDK = [json _stds_stringForKey:@"acsCounterAtoS" required:YES error:&error]; + NSString *acsTransactionID = [json _stds_stringForKey:@"acsTransID" required:YES error:&error]; + NSString *challengeCompletionIndicatorRawString = [json _stds_stringForKey:@"challengeCompletionInd" validator:^BOOL (NSString *value) { + return [value isEqualToString:@"N"] || [value isEqualToString:@"Y"]; + } required:YES error:&error]; + // There is only one valid messageType value for this object (@"CRes"), so we don't store it. + [json _stds_stringForKey:@"messageType" validator:^BOOL (NSString *value) { + return [value isEqualToString:@"CRes"]; + } required:YES error:&error]; + NSString *messageVersion = [json _stds_stringForKey:@"messageVersion" required:YES error:&error]; + NSString *sdkTransactionID = [json _stds_stringForKey:@"sdkTransID" required:YES error:&error]; + + BOOL challengeCompletionIndicator = challengeCompletionIndicatorRawString.boolValue; + + STDSACSUIType acsUIType = STDSACSUITypeNone; + if (!challengeCompletionIndicator) { + NSString *acsUITypeRawString = [json _stds_stringForKey:@"acsUiType" validator:^BOOL (NSString *value) { + return [self acsUITypeStringMapping][value] != nil; + } required:YES error:&error]; + + acsUIType = [self acsUITypeStringMapping][acsUITypeRawString].integerValue; + } + + if (error) { + // We failed to populate a required field + if (outError) { + *outError = error; + } + return nil; + } + + // At this point all the above values are valid: e.g. raw string representations of a BOOL or enum will map to a valid value. + +#pragma mark Conditional + NSString *encodedAcsHTML = [json _stds_stringForKey:@"acsHTML" required:(acsUIType == STDSACSUITypeHTML) error: &error]; + NSString *acsHTML = [encodedAcsHTML _stds_base64URLDecodedString]; + if (encodedAcsHTML && !acsHTML) { + // html was not valid base64url + error = [NSError _stds_invalidJSONFieldError:@"acsHTML"]; + } + + NSArray> *challengeSelectInfo = [json _stds_arrayForKey:@"challengeSelectInfo" + arrayElementType:[STDSChallengeResponseSelectionInfoObject class] + required:(acsUIType == STDSACSUITypeSingleSelect || acsUIType == STDSACSUITypeMultiSelect) + error:&error]; + NSString *oobContinueLabel = [json _stds_stringForKey:@"oobContinueLabel" required:(acsUIType == STDSACSUITypeOOB) error:&error]; + NSString *submitAuthenticationLabel = [json _stds_stringForKey:@"submitAuthenticationLabel" required:(acsUIType == STDSACSUITypeText || acsUIType == STDSACSUITypeSingleSelect || acsUIType == STDSACSUITypeMultiSelect || acsUIType == STDSACSUITypeText) error:&error]; + +#pragma mark Optional + NSArray> *messageExtensions = [json _stds_arrayForKey:@"messageExtension" + arrayElementType:[STDSChallengeResponseMessageExtensionObject class] + required:NO + error:&error]; + NSMutableArray *unrecognizedMessageExtensionIdentifiers = [NSMutableArray new]; + for (id messageExtension in messageExtensions) { + if (messageExtension.criticalityIndicator && ![[self supportedMessageExtensions] containsObject:messageExtension.identifier]) { + [unrecognizedMessageExtensionIdentifiers addObject:messageExtension.identifier]; + } + } + if (unrecognizedMessageExtensionIdentifiers.count > 0) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeUnrecognizedCriticalMessageExtension userInfo:@{STDSStripe3DS2UnrecognizedCriticalMessageExtensionsKey: unrecognizedMessageExtensionIdentifiers}]; + } + if (messageExtensions.count > 10) { + error = [NSError _stds_invalidJSONFieldError:@"messageExtension"]; + } + + NSString *encodedAcsHTMLRefresh = [json _stds_stringForKey:@"acsHTMLRefresh" required:NO error: &error]; + NSString *acsHTMLRefresh = [encodedAcsHTMLRefresh _stds_base64URLDecodedString]; + if (encodedAcsHTMLRefresh && !acsHTMLRefresh) { + // html was not valid base64url + error = [NSError _stds_invalidJSONFieldError:@"acsHTMLRefresh"]; + } + + BOOL infoLabelRequired = NO; + BOOL headerRequired = NO; + BOOL infoTextRequired = NO; + switch (acsUIType) { + case STDSACSUITypeNone: + break; // no-op + case STDSACSUITypeText: + case STDSACSUITypeSingleSelect: + case STDSACSUITypeMultiSelect: + infoLabelRequired = YES; // TC_SDK_10270_001 & TC_SDK_10276_001 & TC_SDK_10284_001 + headerRequired = YES; // TC_SDK_10268_001 & TC_SDK_10273_001 & TC_SDK_10282_001 + infoTextRequired = YES; // TC_SDK_10272_001 & TC_SDK_10278_001 & TC_SDK_10286_001 + break; + case STDSACSUITypeOOB: + + break; + case STDSACSUITypeHTML: + break; // no-op + } + + + NSString *challengeInfoLabel = [json _stds_stringForKey:@"challengeInfoLabel" validator:nil required:infoLabelRequired error:&error]; + NSString *challengeInfoHeader = [json _stds_stringForKey:@"challengeInfoHeader" required: (oobContinueLabel != nil) || headerRequired error:&error]; // TC_SDK_10292_001 + NSString *challengeInfoText = [json _stds_stringForKey:@"challengeInfoText" required:(oobContinueLabel != nil) || infoTextRequired error:&error]; // TC_SDK_10292_001 + NSString *challengeAdditionalInfoText = [json _stds_stringForKey:@"challengeAddInfo" required:NO error:&error]; + if (!error && submitAuthenticationLabel && (!challengeInfoLabel || !challengeInfoHeader || !challengeInfoText)) { + error = [NSError _stds_missingJSONFieldError:@"challengeInfoLabel or challengeInfoText"]; + } + + NSString *showChallengeInfoTextIndicatorRawString; + if (json[@"challengeInfoTextIndicator"]) { + showChallengeInfoTextIndicatorRawString = [json _stds_stringForKey:@"challengeInfoTextIndicator" validator:^BOOL (NSString *value) { + return [value isEqualToString:@"N"] || [value isEqualToString:@"Y"]; + } required:NO error:&error]; + } + BOOL showChallengeInfoTextIndicator = showChallengeInfoTextIndicatorRawString ? showChallengeInfoTextIndicatorRawString.boolValue : NO; // If the field is missing, we shouldn't show the indicator + NSString *expandInfoLabel = [json _stds_stringForKey:@"expandInfoLabel" required:NO error:&error]; + NSString *expandInfoText = [json _stds_stringForKey:@"expandInfoText" required:NO error:&error]; + NSURL *oobAppURL = [json _stds_urlForKey:@"oobAppURL" required:NO error:&error]; + NSString *oobAppLabel = [json _stds_stringForKey:@"oobAppURL" required:NO error:&error]; + NSDictionary *issuerImageJSON = [json _stds_dictionaryForKey:@"issuerImage" required:NO error:&error]; + STDSChallengeResponseImageObject *issuerImage = [STDSChallengeResponseImageObject decodedObjectFromJSON:issuerImageJSON error:&error]; + NSDictionary *paymentSystemImageJSON = [json _stds_dictionaryForKey:@"psImage" required:NO error:&error]; + STDSChallengeResponseImageObject *paymentSystemImage = [STDSChallengeResponseImageObject decodedObjectFromJSON:paymentSystemImageJSON error:&error]; + NSString *resendInformationLabel = [json _stds_stringForKey:@"resendInformationLabel" required:NO error:&error]; + NSString *whitelistingInfoText = [json _stds_stringForKey:@"whitelistingInfoText" required:NO error:&error]; + if (whitelistingInfoText.length > 64) { + // TC_SDK_10199_001 + error = [NSError _stds_invalidJSONFieldError:@"whitelisting text is greater than 64 characters"]; + } + NSString *whyInfoLabel = [json _stds_stringForKey:@"whyInfoLabel" required:NO error:&error]; + NSString *whyInfoText = [json _stds_stringForKey:@"whyInfoText" required:NO error:&error]; + NSString *transactionStatus = [json _stds_stringForKey:@"transStatus" required:challengeCompletionIndicator error:&error]; + + if (error) { + if (outError) { + *outError = error; + } + return nil; + } + + return [[self alloc] initWithThreeDSServerTransactionID:threeDSServerTransactionID + acsCounterACStoSDK:acsCounterACStoSDK + acsTransactionID:acsTransactionID + acsHTML:acsHTML + acsHTMLRefresh:acsHTMLRefresh + acsUIType:acsUIType + challengeCompletionIndicator:challengeCompletionIndicator + challengeInfoHeader:challengeInfoHeader + challengeInfoLabel:challengeInfoLabel + challengeInfoText:challengeInfoText + challengeAdditionalInfoText:challengeAdditionalInfoText + showChallengeInfoTextIndicator:showChallengeInfoTextIndicator + challengeSelectInfo:challengeSelectInfo + expandInfoLabel:expandInfoLabel + expandInfoText:expandInfoText + issuerImage:issuerImage + messageExtensions:messageExtensions + messageVersion:messageVersion + oobAppURL:oobAppURL + oobAppLabel:oobAppLabel + oobContinueLabel:oobContinueLabel + paymentSystemImage:paymentSystemImage + resendInformationLabel:resendInformationLabel + sdkTransactionID:sdkTransactionID + submitAuthenticationLabel:submitAuthenticationLabel + whitelistingInfoText:whitelistingInfoText + whyInfoLabel:whyInfoLabel + whyInfoText:whyInfoText + transactionStatus:transactionStatus]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfo.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfo.h new file mode 100644 index 00000000..a482c8c1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfo.h @@ -0,0 +1,24 @@ +// +// STDSChallengeResponseSelectionInfo.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol that encapsulates information about an individual selection inside of a challenge response. +@protocol STDSChallengeResponseSelectionInfo + +/// The name of the selection option. +@property (nonatomic, readonly) NSString *name; + +/// The value of the selection option. +@property (nonatomic, readonly) NSString *value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.h new file mode 100644 index 00000000..6e34b6c6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.h @@ -0,0 +1,21 @@ +// +// STDSChallengeResponseSelectionInfoObject.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseSelectionInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An object used to represent information about an individual selection inside of a challenge response. +@interface STDSChallengeResponseSelectionInfoObject: NSObject + +- (instancetype)initWithName:(NSString *)name value:(NSString *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.m b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.m new file mode 100644 index 00000000..8aa22ee0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseSelectionInfoObject.m @@ -0,0 +1,46 @@ +// +// STDSChallengeResponseSelectionInfoObject.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseSelectionInfoObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeResponseSelectionInfoObject() + +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *value; + +@end + +@implementation STDSChallengeResponseSelectionInfoObject + +- (instancetype)initWithName:(NSString *)name value:(NSString *)value { + self = [super init]; + + if (self) { + _name = name; + _value = value; + } + + return self; +} + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError * _Nullable __autoreleasing * _Nullable)outError { + if (json == nil) { + return nil; + } + + NSString *name = [json allKeys].firstObject; + NSString *value = [json objectForKey:name]; + + return [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:name value:value]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.h b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.h new file mode 100644 index 00000000..e1ace0cc --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.h @@ -0,0 +1,80 @@ +// +// STDSChallengeResponseViewController.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponse.h" +#import "STDSUICustomization.h" +#import "STDSImageLoader.h" +#import "STDSDirectoryServer.h" + +@class STDSChallengeResponseViewController; + +NS_ASSUME_NONNULL_BEGIN + +@protocol STDSChallengeResponseViewControllerDelegate + +/** + Called when the user taps the Submit button after entering text in the Text flow (STDSACSUITypeText) + */ +- (void)challengeResponseViewController:(STDSChallengeResponseViewController *)viewController didSubmitInput:(NSString *)userInput + whitelistSelection: (id) whitelistSelection; + +/** + Called when the user taps the Submit button after selecting one or more options in the Single-Select (STDSACSUITypeSingleSelect) or Multi-Select (STDSACSUITypeMultiSelect) flow. + */ +- (void)challengeResponseViewController:(STDSChallengeResponseViewController *)viewController didSubmitSelection:(NSArray> *)selection whitelistSelection: (id) whitelistSelection; + +/** + Called when the user submits an HTML form. + */ +- (void)challengeResponseViewController:(STDSChallengeResponseViewController *)viewController didSubmitHTMLForm:(NSString *)form; + +/** + Called when the user taps the Continue button from an Out-of-Band flow (STDSACSUITypeOOB). + */ +- (void)challengeResponseViewControllerDidOOBContinue:(STDSChallengeResponseViewController *)viewController whitelistSelection: (id) whitelistSelection; + +/** + Called when the user taps the Cancel button. + */ +- (void)challengeResponseViewControllerDidCancel:(STDSChallengeResponseViewController *)viewController; + +/** + Called when the user taps the Resend button. + */ +- (void)challengeResponseViewControllerDidRequestResend:(STDSChallengeResponseViewController *)viewController; + +@end + +@protocol STDSChallengeResponseViewControllerPresentationDelegate + +- (void)dismissChallengeResponseViewController:(STDSChallengeResponseViewController *)viewController; + +@end + +@interface STDSChallengeResponseViewController : UIViewController + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, nullable, weak) id presentationDelegate; + +/// Use setChallengeResponser:animated: to update this value +@property (nonatomic, strong, readonly) id response; + +- (instancetype)initWithUICustomization:(STDSUICustomization * _Nullable)uiCustomization imageLoader:(STDSImageLoader *)imageLoader directoryServer:(STDSDirectoryServer)directoryServer; + +/// If `setLoading` was called beforehand, this waits until the loading spinner has been shown for at least 1 second before displaying the challenge responseself.processingView.isHidden. +- (void)setChallengeResponse:(id)response animated:(BOOL)animated; + +- (void)setLoading; + +- (void)dismiss; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.m b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.m new file mode 100644 index 00000000..0a112b3a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeResponseViewController.m @@ -0,0 +1,595 @@ +// +// STDSChallengeResponseViewController.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +@import WebKit; + +#import "STDSBundleLocator.h" +#import "STDSLocalizedString.h" +#import "STDSChallengeResponseViewController.h" +#import "STDSImageLoader.h" +#import "STDSStackView.h" +#import "STDSBrandingView.h" +#import "STDSChallengeInformationView.h" +#import "STDSChallengeSelectionView.h" +#import "STDSTextChallengeView.h" +#import "STDSVisionSupport.h" +#import "STDSWhitelistView.h" +#import "STDSExpandableInformationView.h" +#import "STDSWebView.h" +#import "STDSProcessingView.h" +#import "UIView+LayoutSupport.h" +#import "NSString+EmptyChecking.h" +#import "UIColor+DefaultColors.h" +#import "UIButton+CustomInitialization.h" +#import "UIFont+DefaultFonts.h" +#import "UIViewController+Stripe3DS2.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeResponseViewController() + +@property (nonatomic, strong, nullable) id response; +@property (nonatomic) STDSDirectoryServer directoryServer; +/// Used to track how long we've been showing a loading spinner. Nil if we are not showing a spinner. +@property (nonatomic, strong, nullable) NSDate *loadingStartDate; +@property (nonatomic, strong, nullable) STDSUICustomization *uiCustomization; +@property (nonatomic, strong) STDSImageLoader *imageLoader; +@property (nonatomic, strong) NSTimer *processingTimer; +@property (nonatomic, getter=isLoading) BOOL loading; +@property (nonatomic, strong) STDSProcessingView *processingView; +@property (nonatomic, strong, nullable) UIScrollView *scrollView; +@property (nonatomic, strong, nullable) STDSWebView *webView; +@property (nonatomic, strong, nullable) STDSChallengeInformationView *challengeInformationView; +@property (nonatomic, strong) UITapGestureRecognizer *tapOutsideKeyboardGestureRecognizer; + +// User input views +@property (nonatomic, strong) STDSChallengeSelectionView *challengeSelectionView; +@property (nonatomic, strong) STDSTextChallengeView *textChallengeView; +@property (nonatomic, strong) STDSWhitelistView *whitelistView; +@property (nonatomic, strong) UIStackView *buttonStackView; +@end + +@implementation STDSChallengeResponseViewController + +static const NSTimeInterval kInterstepProcessingTime = 1.0; +static const NSTimeInterval kDefaultTransitionAnimationDuration = 0.3; +static const CGFloat kBrandingViewHeight = 107; +static const CGFloat kContentHorizontalInset = 16; +static const CGFloat kExpandableContentHorizontalInset = 27; +static const CGFloat kContentViewTopPadding = 16; +static const CGFloat kContentViewBottomPadding = 26; +static const CGFloat kExpandableContentViewTopPadding = 28; + +static NSString * const kHTMLStringLoadingURL = @"about:blank"; + +- (instancetype)initWithUICustomization:(STDSUICustomization * _Nullable)uiCustomization imageLoader:(STDSImageLoader *)imageLoader directoryServer:(STDSDirectoryServer)directoryServer { + self = [super initWithNibName:nil bundle:nil]; + + if (self) { + _uiCustomization = uiCustomization; + _imageLoader = imageLoader; + _tapOutsideKeyboardGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_didTapOutsideKeyboard:)]; + _directoryServer = directoryServer; + } + + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + [self _stds_setupNavigationBarElementsWithCustomization:_uiCustomization cancelButtonSelector:@selector(_cancelButtonTapped:)]; + self.view.backgroundColor = self.uiCustomization.backgroundColor; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + + NSString *imageName = STDSDirectoryServerImageName(self.directoryServer); + UIImage *dsImage = imageName ? [UIImage imageNamed:imageName inBundle:[STDSBundleLocator stdsResourcesBundle] compatibleWithTraitCollection:nil] : nil; + self.processingView = [[STDSProcessingView alloc] initWithCustomization:self.uiCustomization directoryServerLogo:dsImage]; + self.processingView.hidden = !self.isLoading; + + [self.view addSubview:self.processingView]; + [self.processingView _stds_pinToSuperviewBoundsWithoutMargin]; + + [self.view addGestureRecognizer:self.tapOutsideKeyboardGestureRecognizer]; +} + +#if !STP_TARGET_VISION +- (UIStatusBarStyle)preferredStatusBarStyle { + return self.uiCustomization.preferredStatusBarStyle; +} +#endif + +#pragma mark - Public APIs + +- (void)setLoading { + [self _setLoading:YES]; +} + +- (void)setChallengeResponse:(id)response animated:(BOOL)animated { + BOOL isFirstChallengeResponse = _response == nil; + _response = response; + + [self.processingTimer invalidate]; + + if (isFirstChallengeResponse || !self.isLoading || !self.loadingStartDate) { + [self _displayChallengeResponseAnimated:animated]; + } else { + // Show the loading spinner for at least kDefaultProcessingTime seconds before displaying + NSTimeInterval timeSpentLoading = [[NSDate date] timeIntervalSinceDate:self.loadingStartDate]; + if (timeSpentLoading >= kInterstepProcessingTime) { + // loadingStartDate is nil if we called this method in between viewDidLoad and viewDidAppear. + // There is no time requirement for the initial CRes. + [self _displayChallengeResponseAnimated:animated]; + } else { + self.processingTimer = [NSTimer timerWithTimeInterval:(kInterstepProcessingTime - timeSpentLoading) target:self selector:@selector(_timerDidFire:) userInfo:@(animated) repeats:NO]; + [[NSRunLoop currentRunLoop] addTimer:self.processingTimer forMode:NSDefaultRunLoopMode]; + } + } +} + +- (void)dismiss { + if (self.presentationDelegate) { + [self.presentationDelegate dismissChallengeResponseViewController:self]; + } else { + [self dismissViewControllerAnimated:YES completion:nil]; + } +} + +#pragma mark - Private Helpers + +- (void)_setLoading:(BOOL)isLoading { + self.loading = isLoading; + if (!self.viewLoaded || isLoading == !self.processingView.isHidden) { + return; + } + + /* According to the specs [0], this should be set to NO during AReq/Ares and YES during CReq/CRes. + However, according to UL test feedback [1], the AReq/ARes and initial CReq/CRes processing views should be identical. + + [0]: EMV 3-D Secure Protocol and Core Functions Specification v2.1.0 4.2.1.1 + - "The 3DS SDK shall for the CReq/CRes message exchange...[Req 148] Not include the DS logo or any other design element in the Processing screen." + - "The 3DS SDK shall for the AReq/ARes message exchange...[Req 143] If requested, integrate the DS logo into the Processing screen." + + [1]: UL_PreCompTestReport_ID846_201906_1.0 + - "Visual test case TC_SDK_10022_001 - The test case is FAILED because the processing screen for step 1 and step 2 are not identical. Step 1 displays a 'DS logo' while step 2 does not. + + To pass certification, we'll show the DS logo during the initial CReq/CRes (when self.response == nil). + */ + self.processingView.shouldDisplayDSLogo = self.response == nil; + // If there's no response, the blur view has nothing to blur and looks better visually if it's just the background color + // EDIT Jan 2021: The challenge contents is hidden so this never looks good https://jira.corp.stripe.com/browse/MOBILESDK-153 + self.processingView.shouldDisplayBlurView = NO; // self.response != nil; + + if (isLoading) { + [self.view bringSubviewToFront:self.processingView]; + self.processingView.hidden = NO; + + self.loadingStartDate = [NSDate date]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, STDSLocalizedString(@"Loading", @"Spoken by VoiceOver when the challenge is loading.")); + } else { + self.processingView.hidden = YES; + self.loadingStartDate = nil; + } +} + +- (void)_timerDidFire:(NSTimer *)timer { + BOOL animated = ((NSNumber *)timer.userInfo).boolValue; + [self.processingTimer invalidate]; + [self _displayChallengeResponseAnimated:animated]; +} + +- (void)_setupViewHierarchy { + self.scrollView = [[UIScrollView alloc] init]; + self.scrollView.backgroundColor = self.uiCustomization.footerCustomization.backgroundColor; + self.scrollView.alwaysBounceVertical = YES; + [self.view addSubview:self.scrollView]; + [self.scrollView _stds_pinToSuperviewBoundsWithoutMargin]; + + STDSStackView *containerStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [self.scrollView addSubview:containerStackView]; + [containerStackView _stds_pinToSuperviewBoundsWithoutMargin]; + + UIView *contentView = [UIView new]; + contentView.layoutMargins = UIEdgeInsetsMake(kContentViewTopPadding, kContentHorizontalInset, kContentViewBottomPadding, kContentHorizontalInset); + contentView.backgroundColor = self.uiCustomization.backgroundColor; + [containerStackView addArrangedSubview:contentView]; + + STDSStackView *contentStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [contentView addSubview:contentStackView]; + [contentStackView _stds_pinToSuperviewBounds]; + + STDSBrandingView *brandingView = [self _newConfiguredBrandingView]; + STDSChallengeInformationView *challengeInformationView = [self _newConfiguredChallengeInformationView]; + self.challengeInformationView = challengeInformationView; + UIButton *actionButton = [self _newConfiguredActionButton]; + UIButton *resendButton = [self _newConfiguredResendButton]; + STDSTextChallengeView *textChallengeView = [self _newConfiguredTextChallengeView]; + self.textChallengeView = textChallengeView; + STDSChallengeSelectionView *challengeSelectionView = [self _newConfiguredChallengeSelectionView]; + self.challengeSelectionView = challengeSelectionView; + self.whitelistView = [self _newConfiguredWhitelistView]; + + UIView *expandableContentView = [UIView new]; + expandableContentView.layoutMargins = UIEdgeInsetsMake(kExpandableContentViewTopPadding, kExpandableContentHorizontalInset, 0, kExpandableContentHorizontalInset); + [containerStackView addArrangedSubview:expandableContentView]; + + STDSStackView *expandableContentStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [expandableContentView addSubview:expandableContentStackView]; + [expandableContentStackView _stds_pinToSuperviewBounds]; + + STDSExpandableInformationView *whyInformationView = [self _newConfiguredWhyInformationView]; + STDSExpandableInformationView *expandableInformationView = [self _newConfiguredExpandableInformationView]; + + [contentStackView addArrangedSubview:brandingView]; + [contentStackView addArrangedSubview:challengeInformationView]; + [contentStackView addArrangedSubview:textChallengeView]; + [contentStackView addArrangedSubview:challengeSelectionView]; + + self.buttonStackView = [self _newSubmitButtonStackView]; + + [self.buttonStackView addArrangedSubview:actionButton]; + + [contentStackView addArrangedSubview:self.buttonStackView]; + + if (_response.acsUIType != STDSACSUITypeOOB && _response.acsUIType != STDSACSUITypeMultiSelect && _response.acsUIType != STDSACSUITypeSingleSelect) { + [self.buttonStackView addArrangedSubview:resendButton]; + } + if (!self.whitelistView.isHidden) { + [contentStackView addSpacer:10]; + } + [contentStackView addArrangedSubview:self.whitelistView]; + [expandableContentStackView addArrangedSubview:whyInformationView]; + [expandableContentStackView addArrangedSubview:expandableInformationView]; + + NSLayoutConstraint *contentViewWidth = [NSLayoutConstraint constraintWithItem:containerStackView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeWidth multiplier:1 constant:0]; + NSLayoutConstraint *brandingViewHeightConstraint = [NSLayoutConstraint constraintWithItem:brandingView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:kBrandingViewHeight]; + [NSLayoutConstraint activateConstraints:@[brandingViewHeightConstraint, contentViewWidth]]; + + [self _loadBrandingViewImages:brandingView]; +} + +- (void)_setupWebView { + self.webView = [[STDSWebView alloc] init]; + self.webView.navigationDelegate = self; + [self.view addSubview:self.webView]; + [self.webView _stds_pinToSuperviewBounds]; + [self.webView loadExternalResourceBlockingHTMLString:self.response.acsHTML]; +} + +- (void)_loadBrandingViewImages:(STDSBrandingView *)brandingView { + NSURL *issuerImageURL = [self _highestFideltyURLFromChallengeResponseImage:self.response.issuerImage]; + + if (issuerImageURL != nil) { + [self.imageLoader loadImageFromURL:issuerImageURL completion:^(UIImage * _Nullable image) { + brandingView.issuerImage = image; + }]; + } + + NSURL *paymentSystemImageURL = [self _highestFideltyURLFromChallengeResponseImage:self.response.paymentSystemImage]; + + if (paymentSystemImageURL != nil) { + [self.imageLoader loadImageFromURL:paymentSystemImageURL completion:^(UIImage * _Nullable image) { + brandingView.paymentSystemImage = image; + }]; + } +} + +- (NSURL * _Nullable)_highestFideltyURLFromChallengeResponseImage:(id )image { + return image.extraHighDensityURL ?: image.highDensityURL ?: image.mediumDensityURL; +} + +- (void)_displayChallengeResponseAnimated:(BOOL)animated { + if (self.response != nil) { + [self _setLoading:NO]; + + UIScrollView *existingScrollView = self.scrollView; + STDSWebView *existingWebView = self.webView; + + void (^transitionBlock)(UIView *, BOOL) = ^void(UIView *viewToTransition, BOOL animated) { + NSTimeInterval transitionTime = animated ? kDefaultTransitionAnimationDuration : 0; + viewToTransition.alpha = 0; + [UIView animateWithDuration:transitionTime animations:^{ + viewToTransition.alpha = 1; + } completion:^(BOOL finished) { + [existingScrollView removeFromSuperview]; + [existingWebView removeFromSuperview]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"STDSChallengeResponseViewController.didDisplayChallengeResponse" object:self]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.navigationItem.titleView); + }]; + }; + + switch (self.response.acsUIType) { + case STDSACSUITypeNone: + break; + case STDSACSUITypeText: + case STDSACSUITypeSingleSelect: + case STDSACSUITypeMultiSelect: + case STDSACSUITypeOOB: + [self _setupViewHierarchy]; + + transitionBlock(self.scrollView, animated); + break; + case STDSACSUITypeHTML: + [self _setupWebView]; + + transitionBlock(self.webView, animated); + break; + } + } +} + +- (STDSBrandingView *)_newConfiguredBrandingView { + STDSBrandingView *brandingView = [[STDSBrandingView alloc] init]; + brandingView.hidden = self.response.issuerImage == nil && self.response.paymentSystemImage == nil; + + return brandingView; +} + +- (STDSChallengeInformationView *)_newConfiguredChallengeInformationView { + STDSChallengeInformationView *challengeInformationView = [[STDSChallengeInformationView alloc] init]; + challengeInformationView.headerText = self.response.challengeInfoHeader; + challengeInformationView.challengeInformationText = self.response.challengeInfoText; + challengeInformationView.challengeInformationLabel = self.response.challengeInfoLabel; + challengeInformationView.labelCustomization = self.uiCustomization.labelCustomization; + + if (self.response.showChallengeInfoTextIndicator) { + challengeInformationView.textIndicatorImage = [UIImage imageNamed:@"error" inBundle:[STDSBundleLocator stdsResourcesBundle] compatibleWithTraitCollection:nil]; + } + + return challengeInformationView; +} + +- (STDSTextChallengeView *)_newConfiguredTextChallengeView { + STDSTextChallengeView *textChallengeView = [[STDSTextChallengeView alloc] init]; + textChallengeView.hidden = self.response.acsUIType != STDSACSUITypeText; + textChallengeView.textFieldCustomization = self.uiCustomization.textFieldCustomization; + textChallengeView.textField.accessibilityLabel = self.response.challengeInfoLabel; + textChallengeView.backgroundColor = self.uiCustomization.backgroundColor; + + return textChallengeView; +} + +- (STDSChallengeSelectionView *)_newConfiguredChallengeSelectionView { + STDSChallengeSelectionStyle selectionStyle = self.response.acsUIType == STDSACSUITypeMultiSelect ? STDSChallengeSelectionStyleMulti : STDSChallengeSelectionStyleSingle; + STDSChallengeSelectionView *challengeSelectionView = [[STDSChallengeSelectionView alloc] initWithChallengeSelectInfo:self.response.challengeSelectInfo selectionStyle:selectionStyle]; + challengeSelectionView.hidden = self.response.acsUIType != STDSACSUITypeSingleSelect && self.response.acsUIType != STDSACSUITypeMultiSelect; + challengeSelectionView.labelCustomization = self.uiCustomization.labelCustomization; + challengeSelectionView.selectionCustomization = self.uiCustomization.selectionCustomization; + challengeSelectionView.backgroundColor = self.uiCustomization.backgroundColor; + + return challengeSelectionView; +} + +- (UIButton *)_newConfiguredActionButton { + STDSUICustomizationButtonType buttonType = STDSUICustomizationButtonTypeSubmit; + NSString *buttonTitle; + + switch (self.response.acsUIType) { + case STDSACSUITypeNone: + break; + case STDSACSUITypeText: + case STDSACSUITypeSingleSelect: + case STDSACSUITypeMultiSelect: { + buttonTitle = self.response.submitAuthenticationLabel; + + break; + } + case STDSACSUITypeOOB: { + buttonType = STDSUICustomizationButtonTypeContinue; + buttonTitle = self.response.oobContinueLabel; + + break; + } + case STDSACSUITypeHTML: + break; + } + + STDSButtonCustomization *buttonCustomization = [self.uiCustomization buttonCustomizationForButtonType:buttonType]; + UIButton *actionButton = [UIButton _stds_buttonWithTitle:buttonTitle customization:buttonCustomization]; + [actionButton addTarget:self action:@selector(_actionButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + actionButton.hidden = buttonTitle == nil || [NSString _stds_isStringEmpty:buttonTitle]; + actionButton.accessibilityIdentifier = @"Continue"; + + return actionButton; +} + +- (UIButton *)_newConfiguredResendButton { + STDSButtonCustomization *buttonCustomization = [self.uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeResend]; + + NSString *resendButtonTitle = self.response.resendInformationLabel; + UIButton *resendButton = [UIButton _stds_buttonWithTitle:resendButtonTitle customization:buttonCustomization]; + + resendButton.hidden = resendButtonTitle == nil || [NSString _stds_isStringEmpty:resendButtonTitle]; + [resendButton addTarget:self action:@selector(_resendButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + + return resendButton; +} + +- (STDSWhitelistView *)_newConfiguredWhitelistView { + STDSWhitelistView *whitelistView = [[STDSWhitelistView alloc] init]; + whitelistView.whitelistText = self.response.whitelistingInfoText; + whitelistView.labelCustomization = self.uiCustomization.labelCustomization; + whitelistView.selectionCustomization = self.uiCustomization.selectionCustomization; + whitelistView.hidden = whitelistView.whitelistText == nil; + whitelistView.accessibilityIdentifier = @"STDSWhitelistView"; + + return whitelistView; +} + +- (STDSExpandableInformationView *)_newConfiguredWhyInformationView { + STDSExpandableInformationView *whyInformationView = [[STDSExpandableInformationView alloc] init]; + whyInformationView.title = self.response.whyInfoLabel; + whyInformationView.text = self.response.whyInfoText; + whyInformationView.customization = self.uiCustomization.footerCustomization; + whyInformationView.hidden = whyInformationView.title == nil; + whyInformationView.backgroundColor = self.uiCustomization.footerCustomization.backgroundColor; + __weak typeof(self) weakSelf = self; + whyInformationView.didTap = ^{ + [weakSelf.textChallengeView endEditing:NO]; + }; + + return whyInformationView; +} + +- (STDSExpandableInformationView *)_newConfiguredExpandableInformationView { + + STDSExpandableInformationView *expandableInformationView = [[STDSExpandableInformationView alloc] init]; + expandableInformationView.title = self.response.expandInfoLabel; + expandableInformationView.text = self.response.expandInfoText; + expandableInformationView.customization = self.uiCustomization.footerCustomization; + expandableInformationView.hidden = expandableInformationView.title == nil; + expandableInformationView.backgroundColor = self.uiCustomization.footerCustomization.backgroundColor; + __weak typeof(self) weakSelf = self; + expandableInformationView.didTap = ^{ + [weakSelf.textChallengeView endEditing:NO]; + }; + + return expandableInformationView; +} + +- (UIStackView *)_newSubmitButtonStackView { + UIStackView *stackView = [[UIStackView alloc] init]; + stackView.axis = UILayoutConstraintAxisVertical; + stackView.distribution = UIStackViewDistributionFillEqually; + stackView.alignment = UIStackViewAlignmentFill; + stackView.spacing = 5; + stackView.translatesAutoresizingMaskIntoConstraints = NO; + +#if !STP_TARGET_VISION + CGSize size = [UIScreen mainScreen].bounds.size; + if (size.width > size.height) { + // hack to detect landscape + stackView.axis = UILayoutConstraintAxisHorizontal; + stackView.alignment = UIStackViewAlignmentCenter; + } +#endif + return stackView; +} + +- (void)_keyboardDidShow:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + + // Get the keyboard’s frame at the end of its animation. + CGRect keyboardFrameEnd = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + // Convert the keyboard's frame from the screen's coordinate space to your view's coordinate space. + keyboardFrameEnd = [self.view convertRect:keyboardFrameEnd fromView:nil]; + + // Get the intersection between the keyboard's frame and the view's bounds to work with the + // part of the keyboard that overlaps your view. + CGRect viewIntersection = CGRectIntersection(self.view.bounds, keyboardFrameEnd); + CGFloat bottomOffset = 0; + + // Check whether the keyboard intersects your view before adjusting your offset. + if (!CGRectIsEmpty(viewIntersection)) { + // Adjust the offset by the difference between the view's height and the height of the + // intersection rectangle. + bottomOffset = CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(viewIntersection); + } + + UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.scrollView.contentInset.top, 0.0, bottomOffset, 0.0); + self.scrollView.contentInset = contentInsets; + self.scrollView.scrollIndicatorInsets = contentInsets; +} + +- (void)_keyboardWillHide:(NSNotification *)notification { + UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.scrollView.contentInset.top, 0.0, 0.0, 0.0); + self.scrollView.contentInset = contentInsets; + self.scrollView.scrollIndicatorInsets = contentInsets; +} + +- (void)_applicationWillEnterForeground:(NSNotification *)notification { + if (self.response.acsUIType == STDSACSUITypeOOB && self.response.challengeAdditionalInfoText) { + // [Req 316] When Challenge Additional Information Text is present, the SDK would replace the Challenge Information Text and Challenge Information Text Indicator with the Challenge Additional Information Text when the 3DS Requestor App is moved to the foreground. + self.challengeInformationView.challengeInformationText = self.response.challengeAdditionalInfoText; + self.challengeInformationView.textIndicatorImage = nil; + } else if (self.response.acsUIType == STDSACSUITypeHTML && self.response.acsHTMLRefresh) { + // [Req 317] When the ACS HTML Refresh element is present, the SDK replaces the ACS HTML with the contents of ACS HTML Refresh when the 3DS Requestor App is moved to the foreground. + [self.webView loadExternalResourceBlockingHTMLString:self.response.acsHTMLRefresh]; + } +} + +- (void)_didTapOutsideKeyboard:(UIGestureRecognizer *)gestureRecognizer { + // Note this doesn't fire if a subview handles the touch (e.g. UIControls, STDSExpandableInformationView) + [self.textChallengeView endEditing:NO]; +} + +#pragma mark - Button callbacks + +- (void)_cancelButtonTapped:(UIButton *)sender { + [self.textChallengeView endEditing:NO]; + [self.delegate challengeResponseViewControllerDidCancel:self]; +} + +- (void)_resendButtonTapped:(UIButton *)sender { + [self.textChallengeView endEditing:NO]; + [self.delegate challengeResponseViewControllerDidRequestResend:self]; +} + +- (void)_actionButtonTapped:(UIButton *)sender { + [self.textChallengeView endEditing:NO]; + switch (self.response.acsUIType) { + case STDSACSUITypeNone: + break; + case STDSACSUITypeText: { + [self.delegate challengeResponseViewController:self + didSubmitInput:self.textChallengeView.inputText + whitelistSelection:self.whitelistView.selectedResponse]; + break; + } + case STDSACSUITypeSingleSelect: + case STDSACSUITypeMultiSelect: { + [self.delegate challengeResponseViewController:self + didSubmitSelection:self.challengeSelectionView.currentlySelectedChallengeInfo + whitelistSelection:self.whitelistView.selectedResponse]; + break; + } + case STDSACSUITypeOOB: + [self.delegate challengeResponseViewControllerDidOOBContinue:self + whitelistSelection:self.whitelistView.selectedResponse]; + break; + case STDSACSUITypeHTML: + // No action button in this case, see WKNavigationDelegate. + break; + } +} + +#pragma mark - WKNavigationDelegate + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + + NSURLRequest *request = navigationAction.request; + + if ([request.URL.absoluteString isEqualToString:kHTMLStringLoadingURL]) { + return decisionHandler(WKNavigationActionPolicyAllow); + } else { + if (navigationAction.navigationType == WKNavigationTypeFormSubmitted || navigationAction.navigationType == WKNavigationTypeOther) { + // When the Cardholder’s response is returned as a parameter string, the form data is passed to the web view instance by triggering a location change to a specified (HTTPS://EMV3DS/challenge) URL with the challenge responses appended to the location URL as query parameters (for example, HTTPS://EMV3DS/challenge?city=Pittsburgh). The web view instance, because it monitors URL changes, receives the Cardholder’s responses as query parameters. + [self.delegate challengeResponseViewController:self didSubmitHTMLForm:request.URL.query]; + } + + return decisionHandler(WKNavigationActionPolicyCancel); + } +} + +- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { + if (size.width > size.height) { + // hack to detect landscape + self.buttonStackView.axis = UILayoutConstraintAxisHorizontal; + self.buttonStackView.alignment = UIStackViewAlignmentCenter; + } else { + self.buttonStackView.axis = UILayoutConstraintAxisVertical; + self.buttonStackView.alignment = UIStackViewAlignmentFill; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.h b/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.h new file mode 100644 index 00000000..5ac5be0d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.h @@ -0,0 +1,35 @@ +// +// STDSChallengeSelectionView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseSelectionInfo.h" +#import "STDSLabelCustomization.h" +#import "STDSSelectionCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, STDSChallengeSelectionStyle) { + + /// A display style for selecting a single option. + STDSChallengeSelectionStyleSingle = 0, + + /// A display style for selection multiple options. + STDSChallengeSelectionStyleMulti = 1, +}; + +@interface STDSChallengeSelectionView : UIView + +@property (nonatomic, strong, readonly) NSArray> *currentlySelectedChallengeInfo; +@property (nonatomic, strong) STDSLabelCustomization *labelCustomization; +@property (nonatomic, strong) STDSSelectionCustomization *selectionCustomization; + +- (instancetype)initWithChallengeSelectInfo:(NSArray> *)challengeSelectInfo selectionStyle:(STDSChallengeSelectionStyle)selectionStyle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.m b/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.m new file mode 100644 index 00000000..0b4d15b3 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSChallengeSelectionView.m @@ -0,0 +1,255 @@ +// +// STDSChallengeSelectionView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSLocalizedString.h" +#import "STDSBundleLocator.h" +#import "STDSChallengeSelectionView.h" +#import "STDSStackView.h" +#import "UIView+LayoutSupport.h" +#import "STDSSelectionButton.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeResponseSelectionRow: STDSStackView + +typedef NS_ENUM(NSInteger, STDSChallengeResponseSelectionRowStyle) { + + /// A display style for showing a radio button. + STDSChallengeResponseSelectionRowStyleRadio = 0, + + /// A display style for shows a checkbox. + STDSChallengeResponseSelectionRowStyleCheckbox = 1, +}; + +typedef void (^STDSChallengeResponseRowSelectedBlock)(STDSChallengeResponseSelectionRow *); + +@property (nonatomic, strong, readonly) id challengeSelectInfo; +@property (nonatomic, getter=isSelected) BOOL selected; +@property (nonatomic, strong) STDSLabelCustomization *labelCustomization; +@property (nonatomic, strong) STDSSelectionCustomization *selectionCustomization; + +- (instancetype)initWithChallengeSelectInfo:(id)challengeSelectInfo rowStyle:(STDSChallengeResponseSelectionRowStyle)rowStyle rowSelectedBlock:(STDSChallengeResponseRowSelectedBlock)rowSelectedBlock; + +@end + +@interface STDSChallengeResponseSelectionRow() + +@property (nonatomic, strong) id challengeSelectInfo; +@property (nonatomic, strong) STDSChallengeResponseRowSelectedBlock rowSelectedBlock; +@property (nonatomic) STDSChallengeResponseSelectionRowStyle rowStyle; +@property (nonatomic, strong) STDSSelectionButton *selectionButton; +@property (nonatomic, strong) UILabel *valueLabel; +@property (nonatomic, strong) UITapGestureRecognizer *valueLabelTapRecognizer; + +@end + +@implementation STDSChallengeResponseSelectionRow + +- (instancetype)initWithChallengeSelectInfo:(id)challengeSelectInfo rowStyle:(STDSChallengeResponseSelectionRowStyle)rowStyle rowSelectedBlock:(STDSChallengeResponseRowSelectedBlock)rowSelectedBlock { + self = [super initWithAlignment:STDSStackViewLayoutAxisHorizontal]; + + if (self) { + _challengeSelectInfo = challengeSelectInfo; + _rowStyle = rowStyle; + _rowSelectedBlock = rowSelectedBlock; + self.isAccessibilityElement = YES; + self.accessibilityIdentifier = @"STDSChallengeResponseSelectionRow"; + + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)_setupViewHierarchy { + self.selectionButton = [[STDSSelectionButton alloc] initWithCustomization:self.selectionCustomization]; + self.selectionButton.customization = self.selectionCustomization; + [self.selectionButton addTarget:self action:@selector(_rowWasSelected) forControlEvents:UIControlEventTouchUpInside]; + + if (self.rowStyle == STDSChallengeResponseSelectionRowStyleCheckbox) { + self.selectionButton.isCheckbox = YES; + } + + self.valueLabel = [[UILabel alloc] init]; + self.valueLabel.text = self.challengeSelectInfo.value; + self.valueLabel.userInteractionEnabled = YES; + self.valueLabel.numberOfLines = 0; + self.valueLabelTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_rowWasSelected)]; + [self.valueLabel addGestureRecognizer:self.valueLabelTapRecognizer]; + + [self addArrangedSubview:self.selectionButton]; + [self addSpacer:15.0]; + [self addArrangedSubview:self.valueLabel]; + [self addArrangedSubview:[UIView new]]; +} + +- (void)_rowWasSelected { + self.rowSelectedBlock(self); +} + +- (BOOL)isSelected { + /// Placeholder until visual and interaction design is complete. + return self.selectionButton.isSelected; +} + +- (void)setSelected:(BOOL)selected { + /// Placeholder until visual and interaction design is complete. + self.selectionButton.selected = selected; +} + +- (void)setLabelCustomization:(STDSLabelCustomization *)labelCustomization { + _labelCustomization = labelCustomization; + + self.valueLabel.font = labelCustomization.font; + self.valueLabel.textColor = labelCustomization.textColor; +} + +- (void)setSelectionCustomization:(STDSSelectionCustomization *)selectionCustomization { + _selectionCustomization = selectionCustomization; + + self.selectionButton.customization = selectionCustomization; +} + +#pragma mark - UIAccessibility + +- (BOOL)accessibilityActivate { + self.rowSelectedBlock(self); + return YES; +} + +- (nullable NSString *)accessibilityLabel { + return self.valueLabel.text; +} + +- (nullable NSString *)accessibilityValue { + return self.selected ? STDSLocalizedString(@"Selected", @"Indicates that a button is selected.") : STDSLocalizedString(@"Unselected", @"Indicates that a button is not selected."); +} + +- (UIAccessibilityTraits)accessibilityTraits { + // remove the selected trait since we manually add that as an accessibilityValue above + return (self.selectionButton.accessibilityTraits & ~UIAccessibilityTraitSelected); +} + +@end + +@interface STDSChallengeSelectionView() + +@property (nonatomic, strong) STDSStackView *containerView; +@property (nonatomic, strong) NSArray *challengeSelectionRows; + +@property (nonatomic) STDSChallengeSelectionStyle selectionStyle; + +@end + +@implementation STDSChallengeSelectionView + +static const CGFloat kChallengeSelectionViewTopPadding = 5; +static const CGFloat kChallengeSelectionViewBottomPadding = 20; +static const CGFloat kChallengeSelectionViewInterRowVerticalPadding = 16; + +- (instancetype)initWithChallengeSelectInfo:(NSArray> *)challengeSelectInfo selectionStyle:(STDSChallengeSelectionStyle)selectionStyle { + self = [super init]; + + if (self) { + _selectionStyle = selectionStyle; + _challengeSelectionRows = [self _rowsForChallengeSelectInfo:challengeSelectInfo]; + + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)_setupViewHierarchy { + self.containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + + for (STDSChallengeResponseSelectionRow *selectionRow in self.challengeSelectionRows) { + [self.containerView addArrangedSubview:selectionRow]; + + if (selectionRow != self.challengeSelectionRows.lastObject) { + [self.containerView addSpacer:kChallengeSelectionViewInterRowVerticalPadding]; + } + } + + if (self.challengeSelectionRows.count > 0) { + self.layoutMargins = UIEdgeInsetsMake(kChallengeSelectionViewTopPadding, 0, kChallengeSelectionViewBottomPadding, 0); + } else { + self.layoutMargins = UIEdgeInsetsZero; + } + + [self addSubview:self.containerView]; + [self.containerView _stds_pinToSuperviewBounds]; +} + +- (NSArray *)_rowsForChallengeSelectInfo:(NSArray> *)challengeSelectInfo { + NSMutableArray *challengeRows = [NSMutableArray array]; + STDSChallengeResponseSelectionRowStyle rowStyle = self.selectionStyle == STDSChallengeSelectionStyleSingle ? STDSChallengeResponseSelectionRowStyleRadio : STDSChallengeResponseSelectionRowStyleCheckbox; + + for (id selectionInfo in challengeSelectInfo) { + __weak typeof(self) weakSelf = self; + STDSChallengeResponseSelectionRow *challengeRow = [[STDSChallengeResponseSelectionRow alloc] initWithChallengeSelectInfo:selectionInfo rowStyle:rowStyle rowSelectedBlock:^(STDSChallengeResponseSelectionRow * _Nonnull selectedRow) { + __strong typeof(self) strongSelf = weakSelf; + + [strongSelf _rowWasSelected:selectedRow]; + }]; + + if (selectionInfo == challengeSelectInfo.firstObject && self.selectionStyle == STDSChallengeSelectionStyleSingle) { + challengeRow.selected = YES; + } + + [challengeRows addObject:challengeRow]; + } + + return [challengeRows copy]; +} + +- (void)_rowWasSelected:(STDSChallengeResponseSelectionRow *)selectedRow { + switch (self.selectionStyle) { + case STDSChallengeSelectionStyleSingle: + for (STDSChallengeResponseSelectionRow *row in self.challengeSelectionRows) { + row.selected = row == selectedRow; + } + + break; + case STDSChallengeSelectionStyleMulti: + selectedRow.selected = !selectedRow.isSelected; + break; + } +} + +- (NSArray> *)currentlySelectedChallengeInfo { + NSMutableArray *selectedChallengeInfo = [NSMutableArray array]; + + for (STDSChallengeResponseSelectionRow *selectionRow in self.challengeSelectionRows) { + if (selectionRow.isSelected) { + [selectedChallengeInfo addObject:selectionRow.challengeSelectInfo]; + } + } + + return [selectedChallengeInfo copy]; +} + +- (void)setLabelCustomization:(STDSLabelCustomization *)labelCustomization { + _labelCustomization = labelCustomization; + + for (STDSChallengeResponseSelectionRow *row in self.challengeSelectionRows) { + row.labelCustomization = labelCustomization; + } +} + +- (void)setSelectionCustomization:(STDSSelectionCustomization *)selectionCustomization { + _selectionCustomization = selectionCustomization; + + for (STDSChallengeResponseSelectionRow *row in self.challengeSelectionRows) { + row.selectionCustomization = selectionCustomization; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.h b/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.h new file mode 100644 index 00000000..a50689f9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.h @@ -0,0 +1,19 @@ +// +// STDSDebuggerChecker.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDebuggerChecker : NSObject + ++ (BOOL)processIsCurrentlyAttachedToDebugger; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.m b/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.m new file mode 100644 index 00000000..cbe3a059 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDebuggerChecker.m @@ -0,0 +1,54 @@ +// +// STDSDebuggerChecker.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDebuggerChecker.h" + +#include +#include +#include +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSDebuggerChecker + +// This checking code has been lifted from the apple documentation on how to determine if you're attached to a debugger: https://developer.apple.com/library/archive/qa/qa1361/_index.html ++ (BOOL)processIsCurrentlyAttachedToDebugger { + int junk; + int mib[4]; + struct kinfo_proc info; + size_t size; + + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl. + + size = sizeof(info); + junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + assert(junk == 0); + + // We're being debugged if the P_TRACED flag is set. + + return ( (info.kp_proc.p_flag & P_TRACED) != 0 ); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.h b/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.h new file mode 100644 index 00000000..d73e1872 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.h @@ -0,0 +1,21 @@ +// +// STDSDeviceInformation.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDeviceInformation : NSObject + +- (instancetype)initWithDictionary:(NSDictionary *)deviceInformationDict; + +@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.m b/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.m new file mode 100644 index 00000000..92425435 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformation.m @@ -0,0 +1,30 @@ +// +// STDSDeviceInformation.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDeviceInformation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSDeviceInformation + +- (instancetype)initWithDictionary:(NSDictionary *)deviceInformationDict { + self = [super init]; + if (self) { + _dictionaryValue = [deviceInformationDict copy]; + } + + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ : %@", [super description], _dictionaryValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.h b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.h new file mode 100644 index 00000000..b23abfed --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.h @@ -0,0 +1,23 @@ +// +// STDSDeviceInformationManager.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSDeviceInformation; +@class STDSWarning; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDeviceInformationManager : NSObject + ++ (STDSDeviceInformation *)deviceInformationWithWarnings:(NSArray *)warnings + ignoringRestrictions:(BOOL)ignoreRestrictions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.m b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.m new file mode 100644 index 00000000..5bd65e90 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationManager.m @@ -0,0 +1,65 @@ +// +// STDSDeviceInformationManager.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDeviceInformationManager.h" + +#import "STDSDeviceInformation.h" +#import "STDSDeviceInformationParameter.h" +#import "STDSWarning.h" + +NS_ASSUME_NONNULL_BEGIN + +// TC_SDK_10089_001, Req 2 & 5 +static const NSString * const k3DSDataVersion = @"1.4"; + +static const NSString * const kDataVersionKey = @"DV"; +static const NSString * const kDeviceDataKey = @"DD"; +static const NSString * const kDeviceParameterNotAvailableKey = @"DPNA"; +static const NSString * const kDeviceWarningsKey = @"SW"; + +@implementation STDSDeviceInformationManager + ++ (STDSDeviceInformation *)deviceInformationWithWarnings:(NSArray *)warnings + ignoringRestrictions:(BOOL)ignoreRestrictions { + NSMutableDictionary *deviceInformation = [NSMutableDictionary dictionaryWithObject:k3DSDataVersion forKey:kDataVersionKey]; + + for (STDSDeviceInformationParameter *parameter in [STDSDeviceInformationParameter allParameters]) { + + [parameter collectIgnoringRestrictions:ignoreRestrictions withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + if (collected) { + NSMutableDictionary *deviceData = deviceInformation[kDeviceDataKey]; + if (deviceData == nil) { + deviceData = [NSMutableDictionary dictionary]; + deviceInformation[kDeviceDataKey] = deviceData; + } + deviceData[identifier] = value; + } else { + NSMutableDictionary *notAvailableData = deviceInformation[kDeviceParameterNotAvailableKey]; + if (notAvailableData == nil) { + notAvailableData = [NSMutableDictionary dictionary]; + deviceInformation[kDeviceParameterNotAvailableKey] = notAvailableData; + } + notAvailableData[identifier] = value; + } + }]; + } + + NSMutableArray *warningIDs = [NSMutableArray arrayWithCapacity:warnings.count]; + for (STDSWarning *warning in warnings) { + [warningIDs addObject:warning.identifier]; + } + if (warningIDs.count > 0) { + deviceInformation[kDeviceWarningsKey] = [warningIDs copy]; + } + + return [[STDSDeviceInformation alloc] initWithDictionary:deviceInformation]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter+Private.h b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter+Private.h new file mode 100644 index 00000000..22d9f1a1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter+Private.h @@ -0,0 +1,76 @@ +// +// STDSDeviceInformationParameter+Private.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDeviceInformationParameter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDeviceInformationParameter (Private) + +- (instancetype)initWithIdentifier:(NSString *)identifier + permissionCheck:(nullable BOOL (^)(void))permissionCheck + valueCheck:(id _Nullable (^)(void))valueCheck; + +/// Platform: Platform that the device is using ++ (instancetype)platform; +/// Device Model: Mobile device manufacturer and model ++ (instancetype)deviceModel; +/// OS Name: Operating system name ++ (instancetype)OSName; +/// OS Version: Operating system version ++ (instancetype)OSVersion; +/// Locale: Device locale set by the user ++ (instancetype)locale; +/// Time zone: Device time zone ++ (instancetype)timeZone; +/// Advertising ID: Unique ID available for adertising and fraud detection purposes ++ (instancetype)advertisingID; +/// Screen Resolution: Pixel width and pixel height ++ (instancetype)screenResolution; +/// Device Name: User-assigned device name ++ (instancetype)deviceName; +/// IP Address: IP address of device ++ (instancetype)IPAddress; +/// Latitude: Device physical location latitude ++ (instancetype)latitude; +/// Longitude: Device physical location longitude ++ (instancetype)longitude; + +/// Identifier for Vendor: Alphanumeric string that uniquely ideitifies a device to the app's vendor ++ (instancetype)identiferForVendor; +/// UserInterfaceIdiom: Style of interface to use on the current device ++ (instancetype)userInterfaceIdiom; + +/// familyNames: an array of font family names available on the system ++ (instancetype)familyNames; +/// fontNamesForFamilyName: an array of font names available in a particular font family using the system font family ++ (instancetype)fontNamesForFamilyName; +/// systemFont: System font ++ (instancetype)systemFont; +/// labelFontSize: standard font size used for labels ++ (instancetype)labelFontSize; +/// buttonFontSize: standard font size used for buttons ++ (instancetype)buttonFontSize; +/// smallSystemFontSize: size of the standard small system font ++ (instancetype)smallSystemFontSize; +/// systemFontSize: size of the standard system font ++ (instancetype)systemFontSize; + +/// systemLocale: the ID of the generic locale that contains fixed "backstop" settings that provide values for otherwise undefined keys ++ (instancetype)systemLocale; +/// availableLocaleIdentifiers: an array of NSString objecgts, each of which identifies a locale available on the system ++ (instancetype)availableLocaleIdentifiers; +/// preferredLanguages: the user's language preference order as an array of strings ++ (instancetype)preferredLanguages; + +/// defaultTimeZone: the default time zone for the current application ++ (instancetype)defaultTimeZone; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.h b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.h new file mode 100644 index 00000000..46a3f66b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.h @@ -0,0 +1,24 @@ +// +// STDSDeviceInformationParameter.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDeviceInformationParameter : NSObject + ++ (NSArray *)allParameters; + +/// Returns a UUID unique to the app version ++ (NSString *)sdkAppIdentifier; + +- (void)collectIgnoringRestrictions:(BOOL)ignoreRestrictions withHandler:(void (^)(BOOL, NSString *, id))handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.m b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.m new file mode 100644 index 00000000..3f29724e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDeviceInformationParameter.m @@ -0,0 +1,455 @@ +// +// STDSDeviceInformationParameter.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDeviceInformationParameter.h" + +#import +#import + +#import "STDSIPAddress.h" +#import "STDSSynchronousLocationManager.h" +#import "STDSVisionSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +// Code value to use if the parameter is restricted by the region or market +static const NSString * const kParameterRestrictedCode = @"RE01"; +// Code value to use if the platform version does not support the parameter or the parameter has been deprecated +static const NSString * const kParameterUnavailableCode = @"RE02"; +// Code value to use if parameter collection not possible without prompting the user for permission +static const NSString * const kParameterMissingPermissionsCode = @"RE03"; +// Code value to use if parameter value returned is null or blank +static const NSString * const kParameterNilCode = @"RE04"; + +@implementation STDSDeviceInformationParameter +{ + NSString *_identifier; + BOOL (^ _Nullable _permissionCheck)(void); + id (^_valueCheck)(void); +} + +- (instancetype)initWithIdentifier:(NSString *)identifier + permissionCheck:(nullable BOOL (^)(void))permissionCheck + valueCheck:(id (^)(void))valueCheck { + self = [super init]; + if (self) { + _identifier = [identifier copy]; + _permissionCheck = [permissionCheck copy]; + _valueCheck = [valueCheck copy]; + } + + return self; +} + +- (BOOL)_hasPermissions { + if (_permissionCheck == nil) { + return YES; + } + return _permissionCheck(); +} + +- (BOOL)_isRestricted { + static NSSet *sApprovedParameters = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sApprovedParameters = [NSSet setWithObjects: + // platform + @"C001", + // device model + @"C002", + // OS name + @"C003", + // OS version + @"C004", + // locale + @"C005", + // time zone + @"C006", + // advertising id (i.e. hardware id) + @"C007", + // screen solution + @"C008", + nil + ]; + }); + + return ![sApprovedParameters containsObject:_identifier]; +} + +- (void)collectIgnoringRestrictions:(BOOL)ignoreRestrictions withHandler:(void (^)(BOOL, NSString *, id))handler { + if (!ignoreRestrictions && [self _isRestricted]) { + handler(NO, _identifier, kParameterRestrictedCode); + return; + } else if (![self _hasPermissions]) { + handler(NO, _identifier, kParameterMissingPermissionsCode); + return; + } + + NSAssert(_valueCheck != nil, @"STDSDeviceInformationParameter should not have nil _valueCheck."); + id value = _valueCheck != nil ? _valueCheck() : nil; + + handler(value != nil, _identifier, value ?: kParameterUnavailableCode); +} + ++ (NSArray *)allParameters { + static NSArray *allParameters = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + allParameters = @[ + +#pragma mark - Common Parameters + + [STDSDeviceInformationParameter platform], + [STDSDeviceInformationParameter deviceModel], + [STDSDeviceInformationParameter OSName], + [STDSDeviceInformationParameter OSVersion], + [STDSDeviceInformationParameter locale], + [STDSDeviceInformationParameter timeZone], + [STDSDeviceInformationParameter advertisingID], + [STDSDeviceInformationParameter screenResolution], + [STDSDeviceInformationParameter deviceName], + [STDSDeviceInformationParameter IPAddress], + [STDSDeviceInformationParameter latitude], + [STDSDeviceInformationParameter longitude], + [STDSDeviceInformationParameter applicationPackageName], + [STDSDeviceInformationParameter sdkAppId], + [STDSDeviceInformationParameter sdkVersion], + + +#pragma mark - iOS-Specific Parameters + + [STDSDeviceInformationParameter identiferForVendor], + [STDSDeviceInformationParameter userInterfaceIdiom], + [STDSDeviceInformationParameter familyNames], + [STDSDeviceInformationParameter fontNamesForFamilyName], + [STDSDeviceInformationParameter systemFont], + [STDSDeviceInformationParameter labelFontSize], + [STDSDeviceInformationParameter buttonFontSize], + [STDSDeviceInformationParameter smallSystemFontSize], + [STDSDeviceInformationParameter systemFontSize], + [STDSDeviceInformationParameter systemLocale], + [STDSDeviceInformationParameter availableLocaleIdentifiers], + [STDSDeviceInformationParameter preferredLanguages], + [STDSDeviceInformationParameter defaultTimeZone], + [STDSDeviceInformationParameter appStoreReciptURL], + ]; + + + }); + + return allParameters; +} + ++ (instancetype)platform { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C001" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @"iOS"; + }]; +} + ++ (instancetype)deviceModel { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C002" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [[UIDevice currentDevice] model]; + }]; +} + ++ (instancetype)OSName { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C003" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [[UIDevice currentDevice] systemName]; + }]; +} + ++ (instancetype)OSVersion { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C004" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [[UIDevice currentDevice] systemVersion]; + }]; +} + ++ (instancetype)locale { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C005" + permissionCheck:nil + valueCheck:^id _Nullable{ + NSLocale *locale = [NSLocale currentLocale]; + NSString *language = locale.languageCode; + NSString *country = locale.countryCode; + if (language != nil && country != nil) { + return [@[language, country] componentsJoinedByString:@"-"]; + } else { + return nil; + } + }]; +} + ++ (instancetype)timeZone { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C006" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [NSTimeZone localTimeZone].name; + }]; +} + ++ (instancetype)advertisingID { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C007" + permissionCheck:nil + valueCheck:^id _Nullable{ + // Actually collecting advertisingIdentifier would require our users to tell Apple they're using it during app submission. + // advertisingIdentifier returns all zeros when the user has limited ad tracking. + return @"00000000-0000-0000-0000-000000000000"; + }]; +} + ++ (instancetype)screenResolution { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C008" + permissionCheck:nil + valueCheck:^id _Nullable{ +#if STP_TARGET_VISION + // Offer something reasonable + CGRect boundsInPixels = CGRectMake(0, 0, 512, 342); +#else + CGRect boundsInPixels = [UIScreen mainScreen].nativeBounds; +#endif + return [NSString stringWithFormat:@"%ldx%ld", (long)boundsInPixels.size.width, (long)boundsInPixels.size.height]; + + }]; +} + ++ (instancetype)deviceName +{ + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C009" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [UIDevice currentDevice].localizedModel; + }]; +} + ++ (instancetype)IPAddress { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C010" + permissionCheck:nil + valueCheck:^id _Nullable{ + return STDSCurrentDeviceIPAddress(); + }]; +} + ++ (instancetype)latitude { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C011" + permissionCheck:^BOOL{ + return [STDSSynchronousLocationManager hasPermissions]; + } + valueCheck:^id _Nullable{ + CLLocation *location = [[STDSSynchronousLocationManager sharedManager] deviceLocation]; + return location != nil ? @(location.coordinate.latitude).stringValue : nil; + }]; +} + ++ (instancetype)longitude { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C012" + permissionCheck:^BOOL{ + return [STDSSynchronousLocationManager hasPermissions]; + } + valueCheck:^id _Nullable{ + CLLocation *location = [[STDSSynchronousLocationManager sharedManager] deviceLocation]; + return location != nil ? @(location.coordinate.longitude).stringValue : nil; + }]; +} + ++ (instancetype)applicationPackageName { + /* + The unique package name/bundle identifier of the application in which the + 3DS SDK is embedded. + • iOS: obtained from the [NSBundle mainBundle] bundleIdentifier + property. + */ + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C013" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [[NSBundle mainBundle] bundleIdentifier]; + }]; +} + + ++ (instancetype)sdkAppId { + /* + Universally unique ID that is created for each installation of the 3DS + Requestor App on a Consumer Device. + Note: This should be the same ID that is passed to the Requestor App in + the AuthenticationRequestParameters object (Refer to Section + 4.12.1 in the EMV 3DS SDK Specification). + */ + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C014" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [STDSDeviceInformationParameter sdkAppIdentifier]; + }]; +} + + ++ (instancetype)sdkVersion { + /* + 3DS SDK version as applied by the implementer and stored securely in the + SDK (refer to Req 58 in the EMV 3DS SDK Specification). + */ + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"C015" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @"2.2.0"; + }]; +} + ++ (instancetype)identiferForVendor { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I001" + permissionCheck:nil + valueCheck:^id _Nullable{ + // N.B. This can return nil if the device is locked + // We've decided to mark this case and similar as parameter unavailable, + // even though we have permission and the device _can_ provide it when + // it's in a different state + return [UIDevice currentDevice].identifierForVendor.UUIDString; + }]; +} + ++ (instancetype)userInterfaceIdiom { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I002" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @([UIDevice currentDevice].userInterfaceIdiom).stringValue; + }]; +} + ++ (instancetype)familyNames { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I003" + permissionCheck:nil + valueCheck:^id _Nullable{ + return UIFont.familyNames; + }]; +} + ++ (instancetype)fontNamesForFamilyName { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I004" + permissionCheck:nil + valueCheck:^id _Nullable{ + NSArray *fontNames = [UIFont fontNamesForFamilyName:[UIFont systemFontOfSize:[UIFont systemFontSize]].familyName]; + if (fontNames.count == 0) { + return @[@""]; // Workaround for TC_SDK_10176_001 + } + return fontNames; + }]; +} + ++ (instancetype)systemFont { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I005" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [UIFont systemFontOfSize:[UIFont systemFontSize]].fontName; + }]; +} + ++ (instancetype)labelFontSize { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I006" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @([UIFont labelFontSize]).stringValue; + }]; +} + ++ (instancetype)buttonFontSize { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I007" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @([UIFont buttonFontSize]).stringValue; + }]; +} + ++ (instancetype)smallSystemFontSize { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I008" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @([UIFont smallSystemFontSize]).stringValue; + }]; +} + ++ (instancetype)systemFontSize { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I009" + permissionCheck:nil + valueCheck:^id _Nullable{ + return @([UIFont systemFontSize]).stringValue; + }]; +} + ++ (instancetype)systemLocale { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I010" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [NSLocale currentLocale].localeIdentifier; + }]; +} + ++ (instancetype)availableLocaleIdentifiers { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I011" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [NSLocale availableLocaleIdentifiers]; + }]; +} + ++ (instancetype)preferredLanguages { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I012" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [NSLocale preferredLanguages]; + }]; +} + ++ (instancetype)defaultTimeZone { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I013" + permissionCheck:nil + valueCheck:^id _Nullable{ + return [NSTimeZone defaultTimeZone].name; + }]; +} + ++ (instancetype)appStoreReciptURL { + return [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"I014" + permissionCheck:nil + valueCheck:^id _Nullable { + NSString *appStoreReceiptURL = [[NSBundle mainBundle] appStoreReceiptURL].absoluteString; + if (appStoreReceiptURL) { + return appStoreReceiptURL; + } + return kParameterNilCode; + }]; +} + ++ (NSString *)sdkAppIdentifier { + static NSString * const appIdentifierKeyPrefix = @"STDSStripe3DS2AppIdentifierKey"; + NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @""; + NSString *appIdentifierUserDefaultsKey = [appIdentifierKeyPrefix stringByAppendingString:appVersion]; + NSString *appIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:appIdentifierUserDefaultsKey]; + if (appIdentifier == nil) { + appIdentifier = [[NSUUID UUID] UUIDString].lowercaseString; + // Clean up any previous app identifiers + NSSet *previousKeys = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] keysOfEntriesPassingTest:^BOOL (NSString *key, id obj, BOOL *stop) { + return [key hasPrefix:appIdentifierKeyPrefix] && ![key isEqualToString:appIdentifierUserDefaultsKey]; + }]; + for (NSString *key in previousKeys) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; + } + } + [[NSUserDefaults standardUserDefaults] setObject:appIdentifier forKey:appIdentifierUserDefaultsKey]; + return appIdentifier; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDirectoryServer.h b/Stripe3DS2/Stripe3DS2/STDSDirectoryServer.h new file mode 100644 index 00000000..ce903303 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDirectoryServer.h @@ -0,0 +1,132 @@ +// +// Header.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSInteger, STDSDirectoryServer) { + STDSDirectoryServerULTestRSA, + STDSDirectoryServerULTestEC, + STDSDirectoryServerSTPTestRSA, + STDSDirectoryServerSTPTestEC, + STDSDirectoryServerAmex, + STDSDirectoryServerCartesBancaires, + STDSDirectoryServerDiscover, + STDSDirectoryServerMastercard, + STDSDirectoryServerVisa, + STDSDirectoryServerCustom, + STDSDirectoryServerUnknown, +}; + +static NSString * const kULTestRSADirectoryServerID = @"F055545342"; +static NSString * const kULTestECDirectoryServerID = @"F155545342"; + +static NSString * const kSTDSTestRSADirectoryServerID = @"ul_test"; +static NSString * const kSTDSTestECDirectoryServerID = @"ec_test"; + +static NSString * const kSTDSAmexDirectoryServerID = @"A000000025"; +static NSString * const kSTDSCartesBancairesServerID = @"A000000042"; +static NSString * const kSTDSDiscoverDirectoryServerID = @"A000000324"; +static NSString * const kSTDSDiscoverDirectoryServerID_2 = @"A000000152"; +static NSString * const kSTDSMastercardDirectoryServerID = @"A000000004"; +static NSString * const kSTDSVisaDirectoryServerID = @"A000000003"; + + +/// Returns the typed directory server enum or STDSDirectoryServerUnknown if the directoryServerID is not recognized +NS_INLINE STDSDirectoryServer STDSDirectoryServerForID(NSString *directoryServerID) { + if ([directoryServerID isEqualToString:kULTestRSADirectoryServerID]) { + return STDSDirectoryServerULTestRSA; + } else if ([directoryServerID isEqualToString:kULTestECDirectoryServerID]) { + return STDSDirectoryServerULTestEC; + } else if ([directoryServerID isEqualToString:kSTDSTestRSADirectoryServerID]) { + return STDSDirectoryServerSTPTestRSA; + } else if ([directoryServerID isEqualToString:kSTDSTestECDirectoryServerID]) { + return STDSDirectoryServerSTPTestEC; + } else if ([directoryServerID isEqualToString:kSTDSAmexDirectoryServerID]) { + return STDSDirectoryServerAmex; + } else if ([directoryServerID isEqualToString:kSTDSDiscoverDirectoryServerID] || [directoryServerID isEqualToString:kSTDSDiscoverDirectoryServerID_2]) { + return STDSDirectoryServerDiscover; + } else if ([directoryServerID isEqualToString:kSTDSMastercardDirectoryServerID]) { + return STDSDirectoryServerMastercard; + } else if ([directoryServerID isEqualToString:kSTDSVisaDirectoryServerID]) { + return STDSDirectoryServerVisa; + } else if ([directoryServerID isEqualToString:kSTDSCartesBancairesServerID]) { + return STDSDirectoryServerCartesBancaires; + } + + return STDSDirectoryServerUnknown; +} + +/// Returns the directory server ID or nil for STDSDirectoryServerUnknown +NS_INLINE NSString * _Nullable STDSDirectoryServerIdentifier(STDSDirectoryServer directoryServer) { + switch (directoryServer) { + case STDSDirectoryServerULTestRSA: + return kULTestRSADirectoryServerID; + + case STDSDirectoryServerULTestEC: + return kULTestECDirectoryServerID; + + case STDSDirectoryServerSTPTestRSA: + return kSTDSTestRSADirectoryServerID; + + case STDSDirectoryServerSTPTestEC: + return kSTDSTestECDirectoryServerID; + + case STDSDirectoryServerAmex: + return kSTDSAmexDirectoryServerID; + + case STDSDirectoryServerDiscover: + return kSTDSDiscoverDirectoryServerID; + + case STDSDirectoryServerMastercard: + return kSTDSMastercardDirectoryServerID; + + case STDSDirectoryServerVisa: + return kSTDSVisaDirectoryServerID; + + case STDSDirectoryServerCartesBancaires: + return kSTDSCartesBancairesServerID; + + case STDSDirectoryServerCustom: + return nil; + + case STDSDirectoryServerUnknown: + return nil; + } +} + +/// Returns the directory server image name if one exists +NS_INLINE NSString * _Nullable STDSDirectoryServerImageName(STDSDirectoryServer directoryServer) { + switch (directoryServer) { + case STDSDirectoryServerAmex: + return @"amex-logo"; + case STDSDirectoryServerDiscover: + return @"discover-logo"; + case STDSDirectoryServerMastercard: + return @"mastercard-logo"; + case STDSDirectoryServerCartesBancaires: + return @"cartes-bancaires-logo"; + // just default to an arbitrary logo for the test servers + case STDSDirectoryServerULTestEC: + case STDSDirectoryServerULTestRSA: + case STDSDirectoryServerSTPTestRSA: + case STDSDirectoryServerSTPTestEC: + case STDSDirectoryServerVisa: + if ([[UITraitCollection currentTraitCollection] userInterfaceStyle] == UIUserInterfaceStyleDark) { + return @"visa-white-logo"; + } + return @"visa-logo"; + case STDSDirectoryServerCustom: + case STDSDirectoryServerUnknown: + return nil; + + } +} + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate+Internal.h b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate+Internal.h new file mode 100644 index 00000000..8f45d98a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate+Internal.h @@ -0,0 +1,22 @@ +// +// STDSDirectoryServerCertificate+Internal.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/2/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServerCertificate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDirectoryServerCertificate (Internal) + +/// Verifies the certificate chain represented by certificates where each element is a base64 encoded (NOT base64url) certificate ++ (BOOL)_verifyCertificateChain:(NSArray *)certificates withRootCertificates:(NSArray *)rootCertificates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.h b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.h new file mode 100644 index 00000000..4ac91b09 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.h @@ -0,0 +1,43 @@ +// +// STDSDirectoryServerCertificate.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSJSONWebSignature; + +#import "STDSDirectoryServer.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, STDSDirectoryServerKeyType) { + STDSDirectoryServerKeyTypeRSA, + STDSDirectoryServerKeyTypeEC, + STDSDirectoryServerKeyTypeUnknown, +}; + +@interface STDSDirectoryServerCertificate : NSObject + ++ (nullable instancetype)certificateForDirectoryServer:(STDSDirectoryServer)directoryServer; + ++ (nullable instancetype)customCertificateWithData:(NSData *)certificateData; + ++ (nullable instancetype)customCertificateWithString:(NSString *)certificateString; + +@property (nonatomic, readonly) STDSDirectoryServerKeyType keyType; + +@property (nonatomic, readonly) SecKeyRef publicKey; + +@property (nonatomic, readonly, copy) NSString *certificateString; + +- (nullable NSData *)encryptDataUsingRSA_OAEP_SHA256:(NSData *)plaintext; + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws withRootCertificates:(NSArray *)rootCertificates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.m b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.m new file mode 100644 index 00000000..b5d180dd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSDirectoryServerCertificate.m @@ -0,0 +1,326 @@ +// +// STDSDirectoryServerCertificate.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDirectoryServerCertificate.h" +#import "STDSDirectoryServerCertificate+Internal.h" + +#import "NSData+JWEHelpers.h" +#import "NSString+JWEHelpers.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSJSONWebSignature.h" +#import "STDSSecTypeUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDirectoryServerCertificate () +{ + SecCertificateRef _certificate; + STDSDirectoryServer _directoryServer; +} + +- (instancetype)_initForDirectoryServer:(STDSDirectoryServer)directoryServer; + +@end + +@implementation STDSDirectoryServerCertificate + +- (instancetype)_initWithCertificate:(SecCertificateRef _Nullable)certificate forDirectorySever:(STDSDirectoryServer)directoryServer { + self = [super init]; + if (self) { + _certificate = certificate; + switch (directoryServer) { + + case STDSDirectoryServerULTestRSA: { + /** + UL provides the following, which is PKCS#8, but Security framework wants PKCS#1. Luckily all we have to do is remove the first 32 characters which are just a header to convert + @"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/O0BfXWngO9OJDBsqdR\n5U2h28jrX6Y+LlblTBaYeT2tW7+ca3YzTFXA8duVUwdlWxl3JZCOOeL1feVP6g0TNOHVCkCnirVDLkcozod4aSkNvx+929aDr1ithqhruf0skBc2sMZGBBCNpso6XGzyAf2uZ2+9DvXoKIUYgcr7PQmL2Y0awyQN7KCRcusaotYNz2mOPrL/hAv6hTexkNrQ\nKzFcPwCuc6kN6aNjD+p2CJ51/5p02SNS70nPOmwmg63j6f3n7xVykQ56kNc1l5B5xOpeHJmqk3+hyF1dF/47rQmMFicN41QSvZ5AZJKgWlIn2VQROMkEHkF9ZBRLx1nF\nTwIDAQAB\n-----END PUBLIC KEY-----\n" + */ + static NSString * const kULTestRSAPublicKey = @"MIIBCgKCAQEAr/O0BfXWngO9OJDBsqdR\n5U2h28jrX6Y+LlblTBaYeT2tW7+ca3YzTFXA8duVUwdlWxl3JZCOOeL1feVP6g0TNOHVCkCnirVDLkcozod4aSkNvx+929aDr1ithqhruf0skBc2sMZGBBCNpso6XGzyAf2uZ2+9DvXoKIUYgcr7PQmL2Y0awyQN7KCRcusaotYNz2mOPrL/hAv6hTexkNrQ\nKzFcPwCuc6kN6aNjD+p2CJ51/5p02SNS70nPOmwmg63j6f3n7xVykQ56kNc1l5B5xOpeHJmqk3+hyF1dF/47rQmMFicN41QSvZ5AZJKgWlIn2VQROMkEHkF9ZBRLx1nF\nTwIDAQAB"; + + NSString *cleanedString = [[[kULTestRSAPublicKey stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""]; + + NSData *base64Decoded = [[NSData alloc] initWithBase64EncodedString:cleanedString options:0]; + NSDictionary *attributes = @{ + (__bridge NSString *)kSecAttrKeyType: (__bridge NSString *)kSecAttrKeyTypeRSA, + (__bridge NSString *)kSecAttrKeyClass: (__bridge NSString *)kSecAttrKeyClassPublic, + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef)base64Decoded, (__bridge CFDictionaryRef)attributes, &error); + if (key == NULL) { + return nil; + } + _publicKey = key; + } + break; + + case STDSDirectoryServerULTestEC: { + static NSString * const kULTestECPublicKey = @"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYktbLuAv0v52erE5LPscomKaOmQs\nvevxzOyn9k4sF1hqpBc5kUygzxA9Jl0R/2dTuk8ka7UCujk36xeUsLVpWA=="; + NSString *cleanedString = [[[kULTestECPublicKey stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""]; + NSData *base64Decoded = [[NSData alloc] initWithBase64EncodedString:cleanedString options:0]; + // This data is PEM encoded, to get to ec standard we take the last 65 bytes + if (base64Decoded.length >= 65) { + base64Decoded = [base64Decoded subdataWithRange:NSMakeRange(base64Decoded.length - 65, 65)]; + } + NSDictionary *attributes = @{ + (__bridge NSString *)kSecAttrKeyType: (__bridge NSString *)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge NSString *)kSecAttrKeyClass: (__bridge NSString *)kSecAttrKeyClassPublic, + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef)base64Decoded, (__bridge CFDictionaryRef)attributes, &error); + if (key == NULL) { + return nil; + } + _publicKey = key; + } + break; + + case STDSDirectoryServerSTPTestRSA: + // fall-through + case STDSDirectoryServerSTPTestEC: + // fall-through + case STDSDirectoryServerAmex: + // fall-through + case STDSDirectoryServerCartesBancaires: + // fall-through + case STDSDirectoryServerDiscover: + // fall-through + case STDSDirectoryServerMastercard: + // fall-through + case STDSDirectoryServerVisa: + // fall-through + case STDSDirectoryServerCustom: + // fall-through + case STDSDirectoryServerUnknown: + NSAssert(certificate != NULL, @"Must provide a certificate"); + _publicKey = SecCertificateCopyKey(certificate); + } + _directoryServer = directoryServer; + + if (_publicKey == NULL) { + return nil; + } + } + + return self; +} + +- (instancetype)_initForDirectoryServer:(STDSDirectoryServer)directoryServer { + SecCertificateRef certificate = NULL; + + switch (directoryServer) { + case STDSDirectoryServerULTestRSA: + // fall-through + case STDSDirectoryServerULTestEC: + // The UL test servers don't actually have certificates, just hard-coded key values + break; + + case STDSDirectoryServerSTPTestRSA: + // fall-through + case STDSDirectoryServerSTPTestEC: + // fall-through + case STDSDirectoryServerAmex: + // fall-through + case STDSDirectoryServerCartesBancaires: + // fall-through + case STDSDirectoryServerDiscover: + // fall-through; + case STDSDirectoryServerMastercard: + // fall-through + case STDSDirectoryServerVisa: { + certificate = STDSCertificateForServer(directoryServer); + if (certificate == NULL) { + return nil; + } + } + break; + + case STDSDirectoryServerCustom: + return nil; + + case STDSDirectoryServerUnknown: + return nil; + } + return [self _initWithCertificate:certificate forDirectorySever:directoryServer]; +} + ++ (nullable instancetype)certificateForDirectoryServer:(STDSDirectoryServer)directoryServer { + return [[self alloc] _initForDirectoryServer:directoryServer]; +} + ++ (nullable instancetype)customCertificateWithData:(NSData *)certificateData { + SecCertificateRef certificate = STDSSecCertificateFromData(certificateData); + if (certificate == NULL) { + return nil; + } + return [[self alloc] _initWithCertificate:certificate forDirectorySever:STDSDirectoryServerCustom]; +} + ++ (nullable instancetype)customCertificateWithString:(NSString *)certificateString { + SecCertificateRef certificate = STDSSecCertificateFromString(certificateString); + if (certificate == NULL) { + return nil; + } + + return [[self alloc] _initWithCertificate:certificate forDirectorySever:STDSDirectoryServerCustom]; +} + +- (void)dealloc { + if (_certificate != NULL) { + CFRelease(_certificate); + } + if (_publicKey != NULL) { + CFRelease(_publicKey); + } +} + +- (NSString *)certificateString { + NSData *data = (NSData *)CFBridgingRelease(SecCertificateCopyData(_certificate)); + return [data base64EncodedStringWithOptions:0]; +} + +- (STDSDirectoryServerKeyType)keyType { + switch (_directoryServer) { + case STDSDirectoryServerULTestRSA: + return STDSDirectoryServerKeyTypeRSA; + + case STDSDirectoryServerULTestEC: + return STDSDirectoryServerKeyTypeEC; + + + case STDSDirectoryServerSTPTestRSA: + // fall-through + case STDSDirectoryServerSTPTestEC: + // fall-through + case STDSDirectoryServerAmex: + // fall-through + case STDSDirectoryServerCartesBancaires: + // fall-through + case STDSDirectoryServerDiscover: + // fall-through; + case STDSDirectoryServerMastercard: + // fall-through + case STDSDirectoryServerVisa: + // fall-through + case STDSDirectoryServerCustom: { + NSAssert(_certificate != NULL, @"Must have a valid certificate file"); + if (_certificate == NULL) { + return STDSDirectoryServerKeyTypeUnknown; + } + CFStringRef keyType = STDSSecCertificateCopyPublicKeyType(_certificate); + STDSDirectoryServerKeyType ret = STDSDirectoryServerKeyTypeUnknown; + if (keyType != NULL) { + if (CFStringCompare(keyType, kSecAttrKeyTypeRSA, 0) == kCFCompareEqualTo) { + ret = STDSDirectoryServerKeyTypeRSA; + } else if (CFStringCompare(keyType, kSecAttrKeyTypeECSECPrimeRandom, 0) == kCFCompareEqualTo) { + ret = STDSDirectoryServerKeyTypeEC; + } + + CFRelease(keyType); + } + return ret; + } + + case STDSDirectoryServerUnknown: + NSAssert(0, @"Should not have an STDSDirectoryServerCertificate instance withSTPDirectoryServerUnknown"); + return STDSDirectoryServerKeyTypeUnknown; + } +} + +- (nullable NSData *)encryptDataUsingRSA_OAEP_SHA256:(NSData *)plaintext { + NSAssert(_publicKey != NULL, @"STDSDirectoryServerCertificate should always have _publicKey"); + if (_publicKey == NULL) { + return nil; + } + + CFDataRef encryptedData = SecKeyCreateEncryptedData(_publicKey, + kSecKeyAlgorithmRSAEncryptionOAEPSHA256, + (CFDataRef)plaintext, + NULL); + return (NSData *)CFBridgingRelease(encryptedData); +} + ++ (BOOL)_verifyCertificateChain:(NSArray *)certificatesStrings withRootCertificates:(NSArray *)rootCertificateStrings { + if (certificatesStrings.count == 0 || rootCertificateStrings.count == 0) { + return NO; + } + + NSMutableArray *certificates = [[NSMutableArray alloc] initWithCapacity:certificatesStrings.count]; + for (NSString *certificateString in certificatesStrings) { + SecCertificateRef certificate = STDSSecCertificateFromString(certificateString); + if (certificate == NULL) { + return NO; + } + [certificates addObject:(id)CFBridgingRelease(certificate)]; + } + + NSMutableArray *rootCertificates = [[NSMutableArray alloc] initWithCapacity:rootCertificateStrings.count]; + for (NSString *certificateString in rootCertificateStrings) { + SecCertificateRef certificate = STDSSecCertificateFromString(certificateString); + if (certificate == NULL) { + return NO; + } + [rootCertificates addObject:(id)CFBridgingRelease(certificate)]; + } + + SecPolicyRef policy = SecPolicyCreateBasicX509(); + SecTrustRef trust; + OSStatus status = SecTrustCreateWithCertificates((__bridge CFTypeRef)certificates, + policy, + &trust); + if (policy) { + CFRelease(policy); + } + if (status != errSecSuccess) { + return NO; + } + if (rootCertificates.count > 0) { + status = SecTrustSetAnchorCertificates(trust, (__bridge CFTypeRef)rootCertificates); + if (status != errSecSuccess) { + return NO; + } + } + + CFErrorRef error = NULL; + + bool verified = SecTrustEvaluateWithError(trust, &error); + return (BOOL)verified; +} + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws withRootCertificates:(NSArray *)rootCertificates { + if (jws.certificateChain.count == 0 || ![self _verifyCertificateChain:jws.certificateChain withRootCertificates:rootCertificates]) { + return NO; + } + + switch (jws.algorithm) { + case STDSJSONWebSignatureAlgorithmES256: + return STDSVerifyEllipticCurveP256Signature(jws.ellipticCurvePoint.x, jws.ellipticCurvePoint.y, jws.digest, jws.signature); + + case STDSJSONWebSignatureAlgorithmPS256: { + if (jws.certificateChain.count == 0) { + return NO; + } + NSString *certificateString = [jws.certificateChain firstObject]; + SecCertificateRef certificate = STDSSecCertificateFromString(certificateString); + if (certificate == NULL) { + return NO; + } + + BOOL verified = STDSVerifyRSASignature(certificate, jws.digest, jws.signature); + CFRelease(certificate); + return verified; + } + + + case STDSJSONWebSignatureAlgorithmUnknown: + return NO; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.h b/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.h new file mode 100644 index 00000000..c856951d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.h @@ -0,0 +1,26 @@ +// +// STDSEllipticCurvePoint.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSEllipticCurvePoint : NSObject + +- (nullable instancetype)initWithX:(NSData *)x y:(NSData *)y; +- (nullable instancetype)initWithCertificateData:(NSData *)certificateData; +- (nullable instancetype)initWithKey:(SecKeyRef)key; +- (nullable instancetype)initWithJWK:(NSDictionary *)jwk; + +@property (nonatomic, readonly) NSData *x; +@property (nonatomic, readonly) NSData *y; + +@property (nonatomic, readonly) SecKeyRef publicKey; + +@end +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.m b/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.m new file mode 100644 index 00000000..49fdf3e5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSEllipticCurvePoint.m @@ -0,0 +1,90 @@ +// +// STDSEllipticCurvePoint.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSEllipticCurvePoint.h" + +#import "NSDictionary+DecodingHelpers.h" +#import "NSString+JWEHelpers.h" +#import "STDSSecTypeUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSEllipticCurvePoint + +- (nullable instancetype)initWithX:(NSData *)x y:(NSData *)y { + self = [super init]; + if (self) { + _x = x; + _y = y; + _publicKey = STDSSecKeyRefFromCoordinates(x, y); + if (_publicKey == NULL) { + return nil; + } + } + + return self; +} + +- (nullable instancetype)initWithKey:(SecKeyRef)key { + self = [super init]; + if (self) { + _publicKey = key; + CFErrorRef error = NULL; + NSData *keyData = (NSData *)CFBridgingRelease(SecKeyCopyExternalRepresentation(key, &error)); + if (keyData == nil) { + return nil; + } + + NSUInteger coordinateLength = (keyData.length - 1) / 2; // -1 because the first byte is formatting 0x04 + NSData *xData = [keyData subdataWithRange:NSMakeRange(1, coordinateLength)]; + NSData *yData = [keyData subdataWithRange:NSMakeRange(1 + coordinateLength, coordinateLength)]; + _x = xData; + _y = yData; + } + + return self; +} + +- (nullable instancetype)initWithCertificateData:(NSData *)certificateData { + SecCertificateRef certificate = STDSSecCertificateFromData(certificateData); + if (certificateData != NULL) { + SecKeyRef key = SecCertificateCopyKey(certificate); + CFRelease(certificate); + if (key != NULL) { + STDSEllipticCurvePoint *point = [self initWithKey:key]; + CFRelease(key); + return point; + } + } + return nil; +} + +- (nullable instancetype)initWithJWK:(NSDictionary *)jwk { + NSString *kty = [jwk _stds_stringForKey:@"kty" validator:^BOOL(NSString * _Nonnull val) { + return [val isEqualToString:@"EC"]; + } required:YES error:NULL]; + NSString *crv = [jwk _stds_stringForKey:@"crv" validator:^BOOL(NSString * _Nonnull val) { + return [val isEqualToString:@"P-256"]; + } required:YES error:NULL]; + + NSData *coordinateX = [[jwk _stds_stringForKey:@"x" required:YES error:NULL] _stds_base64URLDecodedData]; + NSData *coordinateY = [[jwk _stds_stringForKey:@"y" required:YES error:NULL] _stds_base64URLDecodedData]; + + if (kty == nil || + crv == nil || + coordinateX == nil || + coordinateY == nil + ) { + return nil; + } + return [self initWithX:coordinateX y:coordinateY]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair+Testing.h b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair+Testing.h new file mode 100644 index 00000000..d8efb11d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair+Testing.h @@ -0,0 +1,19 @@ +// +// STDSEphemeralKeyPair+Testing.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSEphemeralKeyPair.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSEphemeralKeyPair (Testing) + ++ (nullable instancetype)testKeyPair; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.h b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.h new file mode 100644 index 00000000..515fbb58 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.h @@ -0,0 +1,39 @@ +// +// STDSEphemeralKeyPair.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSDirectoryServerCertificate; +@class STDSEllipticCurvePoint; + +#import "STDSDirectoryServer.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSEphemeralKeyPair : NSObject + +/// Creates a returns a new elliptic curve key pair using curve P-256 ++ (nullable instancetype)ephemeralKeyPair; + +- (instancetype)init NS_UNAVAILABLE; + +@property (nonatomic, readonly) NSString *publicKeyJWK; +@property (nonatomic, readonly) STDSEllipticCurvePoint *publicKeyCurvePoint; + +/** + Creates and returns a new secret key derived using Elliptic Curve Diffie-Hellman + and the certificate's public key (return nil on failure). + Per OpenSSL documentation: Never use a derived secret directly. Typically it is passed through some + hash function to produce a key (e.g. pass the secret as the first argument to STDSCreateConcatKDFWithSHA256) + */ +- (nullable NSData *)createSharedSecretWithEllipticCurveKey:(STDSEllipticCurvePoint *)ecKey; +- (nullable NSData *)createSharedSecretWithCertificate:(STDSDirectoryServerCertificate *)certificate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.m b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.m new file mode 100644 index 00000000..426ebc5a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSEphemeralKeyPair.m @@ -0,0 +1,108 @@ +// +// STDSEphemeralKeyPair.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSEphemeralKeyPair.h" + +#import "NSData+JWEHelpers.h" +#import "NSDictionary+DecodingHelpers.h" +#import "NSString+JWEHelpers.h" +#import "STDSDirectoryServerCertificate.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSSecTypeUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSEphemeralKeyPair () +{ + SecKeyRef _privateKey; + SecKeyRef _publicKey; +} + +@end + + +@implementation STDSEphemeralKeyPair + +- (instancetype)_initWithPrivateKey:(SecKeyRef)privateKey publicKey:(SecKeyRef)publicKey { + self = [super init]; + if (self) { + _privateKey = privateKey; + _publicKey = publicKey; + } + + return self; +} + ++ (nullable instancetype)ephemeralKeyPair { + NSDictionary *parameters = @{ + (__bridge NSString *)kSecAttrKeyType: (__bridge NSString *)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge NSString *)kSecAttrKeySizeInBits: @(256), + }; + CFErrorRef error = NULL; + SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)parameters, &error); + + if (privateKey != NULL) { + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + return [[self alloc] _initWithPrivateKey:privateKey publicKey:publicKey]; + } + + return nil; +} + ++ (nullable instancetype)testKeyPair { + + // values from EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSData *d = [@"iyn--IbkBeNoPu8cN245L6pOQWt2lTH8V0Ds92jQmWA" _stds_base64URLDecodedData]; + NSData *x = [@"C1PL42i6kmNkM61aupEAgLJ4gF1ZRzcV7lqo1TG0mL4" _stds_base64URLDecodedData]; + NSData *y = [@"cNToWLSdcFQKG--PGVEUQrIHP8w6TcRyj0pyFx4-ZMc" _stds_base64URLDecodedData]; + + SecKeyRef privateKey = STDSPrivateSecKeyRefFromCoordinates(x, y, d); + if (privateKey == NULL) { + return nil; + } + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + if (publicKey == NULL) { + return nil; + } + + return [[STDSEphemeralKeyPair alloc] _initWithPrivateKey:privateKey publicKey:publicKey]; +} + +- (void)dealloc { + if (_privateKey != NULL) { + CFRelease(_privateKey); + } + if (_publicKey != NULL) { + CFRelease(_publicKey); + } +} + +- (NSString *)publicKeyJWK { + STDSEllipticCurvePoint *publicKeyCurvePoint = [[STDSEllipticCurvePoint alloc] initWithKey:_publicKey]; + return [NSString stringWithFormat:@"{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"%@\",\"y\":\"%@\"}", [publicKeyCurvePoint.x _stds_base64URLEncodedString], [publicKeyCurvePoint.y _stds_base64URLEncodedString]]; +} + +- (nullable NSData *)createSharedSecretWithEllipticCurveKey:(STDSEllipticCurvePoint *)ecKey { + return [self _createSharedSecretWithPrivateKey:_privateKey publicKey:ecKey.publicKey]; +} + +- (nullable NSData *)createSharedSecretWithCertificate:(STDSDirectoryServerCertificate *)certificate { + return [self _createSharedSecretWithPrivateKey:_privateKey publicKey:certificate.publicKey]; +} + +- (nullable NSData *)_createSharedSecretWithPrivateKey:(SecKeyRef)privateKey publicKey:(SecKeyRef)publicKey { + NSDictionary *params = @{(__bridge NSString *)kSecKeyKeyExchangeParameterRequestedSize: @(32)}; + CFErrorRef error = NULL; + CFDataRef secret = SecKeyCopyKeyExchangeResult(privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard, publicKey, (__bridge CFDictionaryRef)params, &error); + + return (NSData *)CFBridgingRelease(secret); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.h b/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.h new file mode 100644 index 00000000..3a0730c3 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.h @@ -0,0 +1,40 @@ +// +// STDSErrorMessage+Internal.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 4/9/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +// Constructors for the circumstances in which we are required to send an ErrorMessage to the ACS +@interface STDSErrorMessage (Internal) + +/// Received an invalid message type ++ (instancetype)errorForInvalidMessageWithACSTransactionID:(NSString *)acsTransactionID + messageVersion:(NSString *)messageVersion; + +/// Encountered an invalid field parsing a JSON response ++ (nullable instancetype)errorForJSONFieldInvalidWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error; + +/// Encountered a missing field parsing a JSON response ++ (nullable instancetype)errorForJSONFieldMissingWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error; + +/// Encountered an error decrypting a networking response ++ (instancetype)errorForDecryptionErrorWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion; + +/// Timed out ++ (instancetype)errorForTimeoutWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion; + ++ (instancetype)errorForUnrecognizedIDWithACSTransactionID:(NSString *)transactionID messageVersion:(NSString *)messageVersion; + +/// Encountered unrecognized critical message extension(s) ++ (instancetype)errorForUnrecognizedCriticalMessageExtensionsWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.m b/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.m new file mode 100644 index 00000000..02eed1d0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSErrorMessage+Internal.m @@ -0,0 +1,91 @@ +// +// STDSErrorMessage+Internal.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 4/9/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSErrorMessage+Internal.h" +#import "STDSStripe3DS2Error.h" + +@implementation STDSErrorMessage (Internal) + ++ (NSString *)_stringForErrorCode:(STDSErrorMessageCode)errorCode { + return [NSString stringWithFormat:@"%ld", (long)errorCode]; +} + ++ (instancetype)errorForInvalidMessageWithACSTransactionID:(nonnull NSString *)acsTransactionID messageVersion:(nonnull NSString *)messageVersion { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageCodeInvalidMessage] + errorComponent:@"C" + errorDescription:@"Message not recognized" + errorDetails:@"Unknown message type" + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; + +} + ++ (nullable instancetype)errorForJSONFieldMissingWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageCodeRequiredDataElementMissing] + errorComponent:@"C" + errorDescription:@"Missing Field" + errorDetails:error.userInfo[STDSStripe3DS2ErrorFieldKey] + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + ++ (nullable instancetype)errorForJSONFieldInvalidWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageErrorInvalidDataElement] + errorComponent:@"C" + errorDescription:@"Invalid Field" + errorDetails:error.userInfo[STDSStripe3DS2ErrorFieldKey] + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + ++ (instancetype)errorForDecryptionErrorWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageErrorDataDecryptionFailure] + errorComponent:@"C" + errorDescription:@"Response could not be decrypted." + errorDetails:@"Response could not be decrypted.s" + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + ++ (instancetype)errorForTimeoutWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageErrorTimeout] + errorComponent:@"C" + errorDescription:@"Transaction timed out." + errorDetails:@"Transaction timed out." + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + ++ (instancetype)errorForUnrecognizedIDWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion { + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageErrorTransactionIDNotRecognized] + errorComponent:@"C" + errorDescription:@"Unrecognized transaction ID" + errorDetails:@"Unrecognized transaction ID" + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + ++ (instancetype)errorForUnrecognizedCriticalMessageExtensionsWithACSTransactionID:(NSString *)acsTransactionID messageVersion:(NSString *)messageVersion error:(NSError *)error { + NSArray *unrecognizedIDs = error.userInfo[STDSStripe3DS2UnrecognizedCriticalMessageExtensionsKey]; + + return [[[self class] alloc] initWithErrorCode:[self _stringForErrorCode:STDSErrorMessageCodeUnrecognizedCriticalMessageExtension] + errorComponent:@"C" + errorDescription:@"Critical message extension not recognised." + errorDetails:[unrecognizedIDs componentsJoinedByString:@","] + messageVersion:messageVersion + acsTransactionIdentifier:acsTransactionID + errorMessageType:@"CRes"]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSException+Internal.h b/Stripe3DS2/Stripe3DS2/STDSException+Internal.h new file mode 100644 index 00000000..3429aba9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSException+Internal.h @@ -0,0 +1,19 @@ +// +// STDSException+Internal.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSException (Internal) + ++ (instancetype)exceptionWithMessage:(NSString *)format, ...; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.h b/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.h new file mode 100644 index 00000000..f6f3ee0e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.h @@ -0,0 +1,23 @@ +// +// STDSExpandableInformationView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSFooterCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSExpandableInformationView : UIView + +@property (nonatomic, strong, nullable) NSString *title; +@property (nonatomic, strong, nullable) NSString *text; +@property (nonatomic, strong, nullable) STDSFooterCustomization *customization; +@property (nonatomic, strong, nullable) void (^didTap)(void); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.m b/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.m new file mode 100644 index 00000000..9b1bbdfb --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSExpandableInformationView.m @@ -0,0 +1,150 @@ +// +// STDSExpandableInformationView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSLocalizedString.h" +#import "STDSBundleLocator.h" +#import "STDSExpandableInformationView.h" +#import "STDSStackView.h" +#import "UIView+LayoutSupport.h" +#import "NSString+EmptyChecking.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSExpandableInformationView() + +@property (nonatomic, strong) UIView *tappableView; +@property (nonatomic, strong) STDSStackView *textContainerView; +@property (nonatomic, strong) STDSStackView *imageViewStackView; +@property (nonatomic, strong) UIView *imageViewSpacerView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *textLabel; +@property (nonatomic, strong) UIImageView *titleImageView; + +@end + +@implementation STDSExpandableInformationView + +static const CGFloat kTitleContainerSpacing = 20; +static const CGFloat kTextContainerSpacing = 13; +static const CGFloat kExpandableInformationViewBottomMargin = 30; +static const CGFloat kTitleImageViewRotationAnimationDuration = (CGFloat)0.2; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self _setupViewHierarchy]; + self.accessibilityIdentifier = @"STDSExpandableInformationView"; + } + + return self; +} + +- (void)_setupViewHierarchy { + self.layoutMargins = UIEdgeInsetsMake(0, 0, kExpandableInformationViewBottomMargin, 0); + + self.titleLabel = [[UILabel alloc] init]; + // Set titleLabel as not an accessibility element because we make the + // container, which is the actual control, have the same accessibility label + // and accurately reflects that interactivity and state of the control + self.titleLabel.isAccessibilityElement = NO; + self.titleLabel.numberOfLines = 0; + + self.textLabel = [[UILabel alloc] init]; + self.textLabel.numberOfLines = 0; + + UIImage *chevronImage = [[UIImage imageNamed:@"Chevron" inBundle:[STDSBundleLocator stdsResourcesBundle] compatibleWithTraitCollection:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.titleImageView = [[UIImageView alloc] initWithImage:chevronImage]; + self.titleImageView.contentMode = UIViewContentModeScaleAspectFit; + + STDSStackView *containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisHorizontal]; + [self addSubview:containerView]; + [containerView _stds_pinToSuperviewBounds]; + + STDSStackView *titleContainerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [titleContainerView addArrangedSubview:self.titleLabel]; + + self.imageViewStackView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + self.imageViewSpacerView = [UIView new]; + [self.imageViewStackView addArrangedSubview:self.titleImageView]; + + [containerView addArrangedSubview:self.imageViewStackView]; + [containerView addSpacer:kTitleContainerSpacing]; + [containerView addArrangedSubview:titleContainerView]; + [containerView addArrangedSubview:[UIView new]]; + + self.textContainerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + self.textContainerView.hidden = YES; + [self.textContainerView addSpacer:kTextContainerSpacing]; + [self.textContainerView addArrangedSubview:self.textLabel]; + [titleContainerView addArrangedSubview:self.textContainerView]; + + UITapGestureRecognizer *expandTextTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_toggleTextExpansion)]; + [containerView addGestureRecognizer:expandTextTapRecognizer]; + containerView.accessibilityTraits |= UIAccessibilityTraitButton; + containerView.isAccessibilityElement = YES; + self.tappableView = containerView; + [self _updateTappableViewAccessibilityValue]; +} + +- (void)setTitle:(NSString * _Nullable)title { + _title = title; + + self.titleLabel.text = title; + self.tappableView.accessibilityLabel = title; +} + +- (void)setText:(NSString * _Nullable)text { + _text = text; + + self.textLabel.text = text; +} + +- (void)_updateTappableViewAccessibilityValue { + if (self.textContainerView.isHidden) { + self.tappableView.accessibilityValue = STDSLocalizedString(@"Collapsed", @"Accessibility label for expandandable text control to indicate text is hidden."); + } else { + self.tappableView.accessibilityValue = STDSLocalizedString(@"Expanded", @"Accessibility label for expandandable text control to indicate that the UI has been expanded and additional text is available."); + } +} + +- (void)_toggleTextExpansion { + if (self.didTap) { + self.didTap(); + } + self.textContainerView.hidden = !self.textContainerView.hidden; + + CGFloat rotationValue = (CGFloat)M_PI_2; + if (self.textContainerView.isHidden) { + rotationValue = (CGFloat)0; + [self.imageViewStackView removeArrangedSubview:self.imageViewSpacerView]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.titleLabel); + } else { + [self.imageViewStackView addArrangedSubview:self.imageViewSpacerView]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.textLabel); + } + [self _updateTappableViewAccessibilityValue]; + + [UIView animateWithDuration:kTitleImageViewRotationAnimationDuration animations:^{ + self.titleImageView.transform = CGAffineTransformMakeRotation(rotationValue); + }]; +} + +- (void)setCustomization:(STDSFooterCustomization * _Nullable)customization { + self.titleLabel.font = customization.headingFont; + self.titleLabel.textColor = customization.headingTextColor; + + self.textLabel.font = customization.font; + self.textLabel.textColor = customization.textColor; + + self.titleImageView.tintColor = customization.chevronColor; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSIPAddress.h b/Stripe3DS2/Stripe3DS2/STDSIPAddress.h new file mode 100644 index 00000000..a6d5911f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSIPAddress.h @@ -0,0 +1,15 @@ +// +// STDSIPAddress.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString * _Nullable STDSCurrentDeviceIPAddress(void); + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSIPAddress.m b/Stripe3DS2/Stripe3DS2/STDSIPAddress.m new file mode 100644 index 00000000..863eddb8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSIPAddress.m @@ -0,0 +1,43 @@ +// +// STDSIPAddress.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSIPAddress.h" + +#include +#include + +// source: https://zachwaugh.com/posts/programmatically-retrieving-ip-address-of-iphone +NSString * STDSCurrentDeviceIPAddress(void) { + NSString *address = nil; + struct ifaddrs *interfaces = NULL; + struct ifaddrs *temp_addr = NULL; + int success = 0; + + // retrieve the current interfaces - returns 0 on success + success = getifaddrs(&interfaces); + if (success == 0) { + // Loop through linked list of interfaces + temp_addr = interfaces; + while (temp_addr != NULL) { + if( temp_addr->ifa_addr->sa_family == AF_INET) { + // Check if interface is en0 which is the wifi connection on the iPhone + if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { + // Get NSString from C String + address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)]; + } + } + + temp_addr = temp_addr->ifa_next; + } + } + + // Free memory + freeifaddrs(interfaces); + + return address; +} diff --git a/Stripe3DS2/Stripe3DS2/STDSImageLoader.h b/Stripe3DS2/Stripe3DS2/STDSImageLoader.h new file mode 100644 index 00000000..15f1396a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSImageLoader.h @@ -0,0 +1,36 @@ +// +// STDSImageLoader.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/5/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^STDSImageDownloadCompletionBlock)(UIImage * _Nullable); + +@interface STDSImageLoader: NSObject + +/** + Initializes an `STDSImageLoader` with the given parameters. + + @param session The session to initialize the loader with. + @return Returns an initialized `STDSImageLoader` object. + */ +- (instancetype)initWithURLSession:(NSURLSession *)session; + +/** + Attempts to load an image from the specified URL. + + @param URL The URL to load an image for. + @param completion A completion block that is called when the image loading has finished. This will be called on the main queue. + */ +- (void)loadImageFromURL:(NSURL *)URL completion:(STDSImageDownloadCompletionBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSImageLoader.m b/Stripe3DS2/Stripe3DS2/STDSImageLoader.m new file mode 100644 index 00000000..1d87e44f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSImageLoader.m @@ -0,0 +1,50 @@ +// +// STDSImageLoader.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/5/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSImageLoader.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSImageLoader() + +@property (nonatomic, strong) NSURLSession *session; + +@end + +@implementation STDSImageLoader + +- (instancetype)initWithURLSession:(NSURLSession *)session { + self = [super init]; + + if (self) { + _session = session; + } + + return self; +} + +- (void)loadImageFromURL:(NSURL *)URL completion:(STDSImageDownloadCompletionBlock)completion { + NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + UIImage *image; + + if (data != nil) { + image = [UIImage imageWithData:data]; + } + + [NSOperationQueue.mainQueue addOperationWithBlock:^{ + completion(image); + }]; + }]; + + [dataTask resume]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.h b/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.h new file mode 100644 index 00000000..ed584dec --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.h @@ -0,0 +1,19 @@ +// +// STDSIntegrityChecker.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSIntegrityChecker : NSObject + ++ (BOOL)SDKIntegrityIsValid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.m b/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.m new file mode 100644 index 00000000..73ed1ea8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSIntegrityChecker.m @@ -0,0 +1,41 @@ +// +// STDSIntegrityChecker.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSIntegrityChecker.h" +#import "Stripe3DS2.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSIntegrityChecker + ++ (BOOL)SDKIntegrityIsValid { + + if (NSClassFromString(@"STDSIntegrityChecker") == [STDSIntegrityChecker class] && + NSClassFromString(@"STDSConfigParameters") == [STDSConfigParameters class] && + NSClassFromString(@"STDSThreeDS2Service") == [STDSThreeDS2Service class] && + NSClassFromString(@"STDSUICustomization") == [STDSUICustomization class] && + NSClassFromString(@"STDSWarning") == [STDSWarning class] && + NSClassFromString(@"STDSAlreadyInitializedException") == [STDSAlreadyInitializedException class] && + NSClassFromString(@"STDSNotInitializedException") == [STDSNotInitializedException class] && + NSClassFromString(@"STDSRuntimeException") == [STDSRuntimeException class] && + NSClassFromString(@"STDSErrorMessage") == [STDSErrorMessage class] && + NSClassFromString(@"STDSRuntimeErrorEvent") == [STDSRuntimeErrorEvent class] && + NSClassFromString(@"STDSProtocolErrorEvent") == [STDSProtocolErrorEvent class] && + NSClassFromString(@"STDSAuthenticationRequestParameters") == [STDSAuthenticationRequestParameters class] && + NSClassFromString(@"STDSChallengeParameters") == [STDSChallengeParameters class] && + NSClassFromString(@"STDSCompletionEvent") == [STDSCompletionEvent class] && + NSClassFromString(@"STDSTransaction") == [STDSTransaction class]) { + return YES; + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.h b/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.h new file mode 100644 index 00000000..2b2bc306 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.h @@ -0,0 +1,45 @@ +// +// STDSJSONWebEncryption.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/24/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServer.h" + +@class STDSDirectoryServerCertificate; +@class STDSJSONWebSignature; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSJSONWebEncryption : NSObject + ++ (nullable NSString *)encryptJSON:(NSDictionary *)json + forDirectoryServer:(STDSDirectoryServer)directoryServer + error:(out NSError * _Nullable *)error; + ++ (nullable NSString *)encryptJSON:(NSDictionary *)json + withCertificate:(STDSDirectoryServerCertificate *)certificate + directoryServerID:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + error:(out NSError * _Nullable *)error; + ++ (nullable NSString *)directEncryptJSON:(NSDictionary *)json + withContentEncryptionKey:(NSData *)contentEncryptionKey + forACSTransactionID:(NSString *)acsTransactionID + error:(out NSError * _Nullable *)error; + ++ (nullable NSDictionary *)decryptData:(NSData *)data + withContentEncryptionKey:(NSData *)contentEncryptionKey + error:(out NSError * _Nullable *)error; + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws forDirectoryServer:(STDSDirectoryServer)directoryServer; + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws withCertificate:(STDSDirectoryServerCertificate *)certificate rootCertificates:(NSArray *)rootCertificates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.m b/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.m new file mode 100644 index 00000000..57023a5e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJSONWebEncryption.m @@ -0,0 +1,407 @@ +// +// STDSJSONWebEncryption.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/24/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSJSONWebEncryption.h" + +#import +#import + +#import "NSData+JWEHelpers.h" +#import "NSError+Stripe3DS2.h" +#import "NSString+JWEHelpers.h" +#import "STDSDirectoryServerCertificate.h" +#import "STDSEphemeralKeyPair.h" +#import "STDSJSONWebSignature.h" +#import "STDSSecTypeUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - _STPJSONWebEncryptionResult +@interface _STPJSONWebEncryptionResult : NSObject + +- (instancetype)initWithCiphertext:(NSData *)ciphertextData + initializationVector:(NSData *)initializationVector + hmacTag:(NSData *)hmacTag; + +@property (nonatomic, copy, readonly) NSData *ciphertextData; +@property (nonatomic, copy, readonly) NSData *initializationVector; +@property (nonatomic, copy, readonly) NSData *hmacTag; + +@end + +@implementation _STPJSONWebEncryptionResult + +- (instancetype)initWithCiphertext:(NSData *)ciphertextData + initializationVector:(NSData *)initializationVector + hmacTag:(NSData *)hmacTag { + self = [super init]; + if (self) { + _ciphertextData = [ciphertextData copy]; + _initializationVector = [initializationVector copy]; + _hmacTag = [hmacTag copy]; + } + + return self; +} + +@end + +#pragma mark - STDSJSONWebEncryption + +static const size_t kContentEncryptionKeyByteCount = 32; +static const size_t kHMACSubKeyByteCount = 16; +static const size_t kAESSubKeyByteCount = 16; + +@implementation STDSJSONWebEncryption + ++ (nullable NSString *)encryptJSON:(NSDictionary *)json + forDirectoryServer:(STDSDirectoryServer)directoryServer + error:(out NSError *__autoreleasing _Nullable * _Nullable)error { + + NSString *ciphertext = nil; + + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate certificateForDirectoryServer:directoryServer]; + NSString *directoryServerID = STDSDirectoryServerIdentifier(directoryServer); + + if (certificate != nil && directoryServerID != nil) { + + ciphertext = [self encryptJSON:json + withCertificate:certificate + directoryServerID:directoryServerID + serverKeyID:nil + error:error]; + } + + if (error != nil && ciphertext == nil) { + *error = *error ?: [NSError _stds_jweError]; + } + + return ciphertext; +} + ++ (nullable NSString *)encryptJSON:(NSDictionary *)json + withCertificate:(STDSDirectoryServerCertificate *)certificate + directoryServerID:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + error:(out NSError * _Nullable *)error { + + NSString *ciphertext = nil; + NSError *jsonError = nil; + NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&jsonError]; + + if (jsonData != nil) { + + switch (certificate.keyType) { + case STDSDirectoryServerKeyTypeRSA: + ciphertext = [self _RSAEncryptPlaintext:jsonData + withCertificate:certificate + serverKeyID:serverKeyID]; + break; + + case STDSDirectoryServerKeyTypeEC: + ciphertext = [self _directEncryptPlaintext:jsonData + withCertificate:certificate + forDirectoryServer:directoryServerID]; + break; + + case STDSDirectoryServerKeyTypeUnknown: + break; + } + } + + if (error != nil && ciphertext == nil) { + *error = jsonError ?: [NSError _stds_jweError]; + } + + return ciphertext; +} + ++ (nullable NSString *)directEncryptJSON:(NSDictionary *)json + withContentEncryptionKey:(NSData *)contentEncryptionKey + forACSTransactionID:(NSString *)acsTransactionID + error:(out NSError * _Nullable *)error { + NSString *ciphertext = nil; + + NSError *jsonError = nil; + + + NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&jsonError]; + + if (jsonData != nil) { + NSString *headerString = [NSString stringWithFormat:@"{\"kid\":\"%@\",\"enc\":\"A128CBC-HS256\",\"alg\":\"dir\"}", acsTransactionID]; + ciphertext = [self _directEncryptPlaintext:jsonData + withContentEncryptionKey:contentEncryptionKey + headerString:headerString]; + } + + if (error != nil && ciphertext == nil) { + *error = jsonError ?: [NSError _stds_jweError]; + } + + return ciphertext; +} + ++ (nullable NSData *)_hmacTagWithKey:(NSData *)hmacSubKey + headerString:(NSString *)headerString + initializationVector:(NSData *)initializationVector + cipherText:(NSData *)cipherText { + NSString *encodedHeaderString = [headerString _stds_base64URLEncodedString]; + + // Per JWE spec, the encoded header data is included as additional authenticated data but with ASCII encoding https://tools.ietf.org/html/rfc7516#section-5.1 + NSData *additionalAuthenticatedData = [encodedHeaderString dataUsingEncoding:NSASCIIStringEncoding]; + uint64_t AL = CFSwapInt64HostToBig(additionalAuthenticatedData.length*8); + + NSMutableData *d = [NSMutableData data]; + [d appendBytes:additionalAuthenticatedData.bytes length:additionalAuthenticatedData.length]; + [d appendBytes:initializationVector.bytes length:initializationVector.length]; + [d appendBytes:cipherText.bytes length:cipherText.length]; + [d appendBytes:&AL length:sizeof(AL)]; + + NSMutableData *M = [[NSMutableData alloc] initWithLength:CC_SHA256_DIGEST_LENGTH]; + + CCHmac(kCCHmacAlgSHA256, + hmacSubKey.bytes, + kHMACSubKeyByteCount, + d.bytes, + d.length, + M.mutableBytes); + + + NSData *hmacTag = [M subdataWithRange:NSMakeRange(0, 16)]; + + return hmacTag; +} + ++ (nullable _STPJSONWebEncryptionResult *)_encryptPlaintext:(NSData *)plaintextData + withKey:(NSData *)contentEncryptionKeyData + headerString:(NSString *)headerString { + // ecrypt JSON according to JWE (RFC 7516) using JWE Compact Serialization + // enc: A128CBC-HS256 + + // Ref: rfc7518 sec 5.2.3 https://www.rfc-editor.org/rfc/rfc7518.txt + + _STPJSONWebEncryptionResult *result = nil; + + static const size_t kInitializationVectorByteCount = 16; + + NSAssert(contentEncryptionKeyData.length == kContentEncryptionKeyByteCount, @"Must use a valid 256 content encryption key"); + if (contentEncryptionKeyData.length == kContentEncryptionKeyByteCount) { + + NSData *hmacSubKeyData = [contentEncryptionKeyData subdataWithRange:NSMakeRange(0, kHMACSubKeyByteCount)]; + NSData *aesSubKeyData = [contentEncryptionKeyData subdataWithRange:NSMakeRange(kHMACSubKeyByteCount, kAESSubKeyByteCount)]; + NSData *initializationVectorData = STDSCryptoRandomData(kInitializationVectorByteCount); + + + // pad with block size for AES + NSMutableData *ciphertextData = [NSMutableData dataWithLength:plaintextData.length + kCCBlockSizeAES128]; + size_t outLength; + CCCryptorStatus aesEncryptionResult = CCCrypt(kCCEncrypt, + kCCAlgorithmAES, + kCCOptionPKCS7Padding, + aesSubKeyData.bytes, + kAESSubKeyByteCount, + initializationVectorData.bytes, + plaintextData.bytes, + (size_t)plaintextData.length, + ciphertextData.mutableBytes, + ciphertextData.length, + &outLength); + if (aesEncryptionResult == kCCSuccess) { + ciphertextData.length = outLength; + + NSData *hmacTag = [self _hmacTagWithKey:hmacSubKeyData + headerString:headerString + initializationVector:initializationVectorData + cipherText:ciphertextData]; + + result = [[_STPJSONWebEncryptionResult alloc] initWithCiphertext:ciphertextData + initializationVector:initializationVectorData + hmacTag:hmacTag]; + } + } + + return result; +} + ++ (nullable NSString *)_RSAEncryptPlaintext:(NSData *)plaintextData + withCertificate:(STDSDirectoryServerCertificate *)certificate + serverKeyID:(nullable NSString *)serverKeyID { + NSData *contentEncryptionKey = STDSCryptoRandomData(kContentEncryptionKeyByteCount); + if (contentEncryptionKey != nil) { + NSString *headerString = nil; + + if (serverKeyID != nil) { + headerString = [NSString stringWithFormat:@"{\"enc\":\"A128CBC-HS256\",\"alg\":\"RSA-OAEP-256\",\"kid\":\"%@\"}", serverKeyID]; + } else { + headerString = @"{\"enc\":\"A128CBC-HS256\",\"alg\":\"RSA-OAEP-256\"}"; + + } + _STPJSONWebEncryptionResult *encryptedData = [self _encryptPlaintext:plaintextData + withKey:contentEncryptionKey + headerString:headerString]; + if (encryptedData != nil) { + NSData *encryptedCEK = [certificate encryptDataUsingRSA_OAEP_SHA256:contentEncryptionKey]; + if (encryptedCEK != nil) { + return [NSString stringWithFormat:@"%@.%@.%@.%@.%@", [headerString _stds_base64URLEncodedString], [encryptedCEK _stds_base64URLEncodedString], [encryptedData.initializationVector _stds_base64URLEncodedString], [encryptedData.ciphertextData _stds_base64URLEncodedString], [encryptedData.hmacTag _stds_base64URLEncodedString]]; + } + } + } + + return nil; +} + ++ (nullable NSString *)_directEncryptPlaintext:(NSData *)plaintextData + withCertificate:(STDSDirectoryServerCertificate *)certificate + forDirectoryServer:(NSString *)directoryServerID { + + STDSEphemeralKeyPair *ephemeralKeyPair = [STDSEphemeralKeyPair ephemeralKeyPair]; + NSData *rawSharedSecret = [ephemeralKeyPair createSharedSecretWithCertificate:certificate]; + NSData *contentEncryptionKey = STDSCreateConcatKDFWithSHA256(rawSharedSecret, kContentEncryptionKeyByteCount, directoryServerID); + + NSString *headerString = [NSString stringWithFormat:@"{\"enc\":\"A128CBC-HS256\",\"alg\":\"ECDH-ES\",\"epk\":%@}", ephemeralKeyPair.publicKeyJWK]; + + return [self _directEncryptPlaintext:plaintextData + withContentEncryptionKey:contentEncryptionKey + headerString:headerString]; +} + ++ (nullable NSString *)_directEncryptPlaintext:(NSData *)plaintextData + withContentEncryptionKey:(NSData *)contentEncryptionKey + headerString:(NSString *)headerString { + + _STPJSONWebEncryptionResult *encryptedData = [self _encryptPlaintext:plaintextData + withKey:contentEncryptionKey + headerString:headerString]; + if (encryptedData != nil) { + return [NSString stringWithFormat:@"%@..%@.%@.%@", [headerString _stds_base64URLEncodedString], [encryptedData.initializationVector _stds_base64URLEncodedString], [encryptedData.ciphertextData _stds_base64URLEncodedString], [encryptedData.hmacTag _stds_base64URLEncodedString]]; + } + + return nil; +} + +#pragma mark - Decryption + ++ (nullable NSDictionary *)decryptData:(NSData *)data + withContentEncryptionKey:(NSData *)contentEncryptionKey + error:(out NSError * _Nullable *)error { + NSString *jweString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray *jweComponents = [jweString componentsSeparatedByString:@"."]; + if (jweComponents.count != 5) { + + // Data may be JSON describing error + NSError *jsonError = nil; + NSDictionary *errorJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (errorJSON != nil) { + return errorJSON; + } + if (error != NULL) { + *error = jsonError ?: [NSError _stds_jweError]; + } + return nil; + } + + NSString *headerString = [jweComponents[0] _stds_base64URLDecodedString]; + NSData *initializationVector = [jweComponents[2] _stds_base64URLDecodedData]; + NSData *ciphertextData = [jweComponents[3] _stds_base64URLDecodedData]; + NSData *hmacTag = [jweComponents[4] _stds_base64URLDecodedData]; + + if (headerString == nil || + initializationVector == nil || + ciphertextData == nil || + hmacTag == nil + ) { + if (error != NULL) { + *error = [NSError _stds_jweError]; + } + return nil; + } + NSAssert(contentEncryptionKey.length == kContentEncryptionKeyByteCount, @"Must use a valid 256 content encryption key"); + if (contentEncryptionKey.length != kContentEncryptionKeyByteCount) { + if (error != NULL) { + *error = [NSError _stds_jweError]; + } + return nil; + } + + NSData *hmacSubKeyData = [contentEncryptionKey subdataWithRange:NSMakeRange(0, kHMACSubKeyByteCount)]; + NSData *aesSubKeyData = [contentEncryptionKey subdataWithRange:NSMakeRange(kHMACSubKeyByteCount, kAESSubKeyByteCount)]; + + // pad with block size for AES + NSMutableData *plaintextData = [NSMutableData dataWithLength:ciphertextData.length + kCCBlockSizeAES128]; + size_t outLength; + CCCryptorStatus aesDecryptionResult = CCCrypt(kCCDecrypt, + kCCAlgorithmAES, + kCCOptionPKCS7Padding, + aesSubKeyData.bytes, + kAESSubKeyByteCount, + initializationVector.bytes, + ciphertextData.bytes, + (size_t)ciphertextData.length, + plaintextData.mutableBytes, + plaintextData.length, + &outLength); + + if (aesDecryptionResult != kCCSuccess) { + if (error != NULL) { + *error = [NSError _stds_jweError]; + } + return nil; + } + + plaintextData.length = outLength; + NSData *calculatedHMACTag = [self _hmacTagWithKey:hmacSubKeyData + headerString:headerString + initializationVector:initializationVector + cipherText:ciphertextData]; + + if (![calculatedHMACTag isEqualToData:hmacTag]) { + if (error != NULL) { + *error = [NSError _stds_jweError]; + } + return nil; + } + + NSDictionary *decryptedJSON = [NSJSONSerialization JSONObjectWithData:plaintextData + options:0 + error:error]; + if (*error != NULL) { + *error = [NSError _stds_jweError]; + return nil; + } + + return decryptedJSON; +} + +#pragma mark - JSON Web Signature Verification + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws forDirectoryServer:(STDSDirectoryServer)directoryServer { + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate certificateForDirectoryServer:directoryServer]; + NSString *certificateString = nil; + + if (directoryServer == STDSDirectoryServerULTestRSA || directoryServer == STDSDirectoryServerULTestEC) { + // for UL tests, the last certificate in the chain is the anchor/root + certificateString = jws.certificateChain.lastObject; + } else { + NSAssert(0, @"We shouldn't be using this path outside of UL testing"); + certificateString = certificate.certificateString; + } + + if (certificateString == nil) { + return NO; + } + + return [self verifyJSONWebSignature:jws withCertificate:certificate rootCertificates:@[certificateString]]; +} + ++ (BOOL)verifyJSONWebSignature:(STDSJSONWebSignature *)jws withCertificate:(__unused STDSDirectoryServerCertificate *)certificate rootCertificates:(NSArray *)rootCertificates { + return [STDSDirectoryServerCertificate verifyJSONWebSignature:jws withRootCertificates:rootCertificates]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.h b/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.h new file mode 100644 index 00000000..d6f17a19 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.h @@ -0,0 +1,41 @@ +// +// STDSJSONWebSignature.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/2/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSEllipticCurvePoint; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, STDSJSONWebSignatureAlgorithm) { + STDSJSONWebSignatureAlgorithmES256, + STDSJSONWebSignatureAlgorithmPS256, + STDSJSONWebSignatureAlgorithmUnknown, +}; + +@interface STDSJSONWebSignature : NSObject + +- (nullable instancetype)initWithString:(NSString *)jwsString; +- (nullable instancetype)initWithString:(NSString *)jwsString allowNilKey:(BOOL)allowNilKey; + +@property (nonatomic, readonly) STDSJSONWebSignatureAlgorithm algorithm; + +@property (nonatomic, readonly) NSData *digest; +@property (nonatomic, readonly) NSData *signature; + +@property (nonatomic, readonly) NSData *payload; + +/// non-nil if algorithm == STDSJSONWebSignatureAlgorithmES256 +@property (nonatomic, nullable, readonly) STDSEllipticCurvePoint *ellipticCurvePoint; + +/// non-nil if algorithm == STDSJSONWebSignatureAlgorithmPS256, can be non-nil for algorithm == STDSJSONWebSignatureAlgorithmES256 +@property (nonatomic, nullable, readonly) NSArray *certificateChain; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.m b/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.m new file mode 100644 index 00000000..2e294937 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJSONWebSignature.m @@ -0,0 +1,88 @@ +// +// STDSJSONWebSignature.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/2/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSJSONWebSignature.h" + +#import "NSString+JWEHelpers.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSSecTypeUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const kJWSAlgorithmKey = @"alg"; +static NSString * const kJWSAlgorithmES256 = @"ES256"; +static NSString * const kJWSAlgorithmPS256 = @"PS256"; + +static NSString * const kJWSCertificateChainKey = @"x5c"; + +@implementation STDSJSONWebSignature + +- (nullable instancetype)initWithString:(NSString *)jwsString { + return [self initWithString:jwsString allowNilKey:NO]; +} + +- (nullable instancetype)initWithString:(NSString *)jwsString allowNilKey:(BOOL)allowNilKey { + self = [super init]; + if (self) { + NSArray *components = [jwsString componentsSeparatedByString:@"."]; + if (components.count != 3) { + return nil; + } + NSData *headerData = [components[0] _stds_base64URLDecodedData]; + if (headerData == nil) { + return nil; + } + NSError *jsonError = nil; + NSDictionary * headerJSON = [NSJSONSerialization JSONObjectWithData:headerData options:0 error:&jsonError]; + if (headerJSON == nil) { + return nil; + } + + if (headerJSON[kJWSCertificateChainKey] != nil && ![headerJSON[kJWSCertificateChainKey] isKindOfClass:[NSArray class]]) { + return nil; + } + + _certificateChain = headerJSON[kJWSCertificateChainKey]; + + NSString *algorithm = headerJSON[kJWSAlgorithmKey]; + if ([algorithm compare:kJWSAlgorithmES256 options: NSCaseInsensitiveSearch] == NSOrderedSame) { + _algorithm = STDSJSONWebSignatureAlgorithmES256; + if (_certificateChain.count > 0) { + SecCertificateRef certificate = STDSSecCertificateFromString(_certificateChain.firstObject); + if (certificate != NULL) { + SecKeyRef key = SecCertificateCopyKey(certificate); + CFRelease(certificate); + if (key != NULL) { + _ellipticCurvePoint = [[STDSEllipticCurvePoint alloc] initWithKey:key]; + CFRelease(key); + } + } + if (_ellipticCurvePoint == nil && !allowNilKey) { + return nil; + } + } else if (!allowNilKey) { + return nil; + } + + } else if ([algorithm compare:kJWSAlgorithmPS256 options: NSCaseInsensitiveSearch] == NSOrderedSame) { + _algorithm = STDSJSONWebSignatureAlgorithmPS256; + } else { + return nil; + } + + _digest = [[@[components[0], components[1]] componentsJoinedByString:@"."] dataUsingEncoding:NSUTF8StringEncoding]; + _signature = [components[2] _stds_base64URLDecodedData]; + _payload = [components[1] _stds_base64URLDecodedData]; + } + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.h b/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.h new file mode 100644 index 00000000..303575c5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.h @@ -0,0 +1,19 @@ +// +// STDSJailbreakChecker.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSJailbreakChecker : NSObject + ++ (BOOL)isJailbroken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.m b/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.m new file mode 100644 index 00000000..6c0dcbf2 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSJailbreakChecker.m @@ -0,0 +1,31 @@ +// +// STDSJailbreakChecker.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSJailbreakChecker.h" + +@import UIKit; + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSJailbreakChecker + +// This was implemented under the following guidance: https://medium.com/@pinmadhon/how-to-check-your-app-is-installed-on-a-jailbroken-device-67fa0170cf56 ++ (BOOL)isJailbroken { + + // Check for existence of files that are common for jailbroken devices + if ([[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"] || + [[NSFileManager defaultManager] fileExistsAtPath:@"/Library/MobileSubstrate/MobileSubstrate.dylib"]) { + return YES; + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSLocalizedString.h b/Stripe3DS2/Stripe3DS2/STDSLocalizedString.h new file mode 100644 index 00000000..7a3a76c2 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSLocalizedString.h @@ -0,0 +1,18 @@ +// +// STDSLocalizedString.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 7/9/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSBundleLocator.h" + +#ifndef STDSLocalizedString_h +#define STDSLocalizedString_h + +#define STDSLocalizedString(key, comment) \ +[[STDSBundleLocator stdsResourcesBundle] localizedStringForKey:(key) value:@"" table:nil] + + +#endif /* STDSLocalizedString_h */ diff --git a/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.h b/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.h new file mode 100644 index 00000000..72b79cf1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.h @@ -0,0 +1,19 @@ +// +// STDSOSVersionChecker.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSOSVersionChecker : NSObject + ++ (BOOL)isSupportedOSVersion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.m b/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.m new file mode 100644 index 00000000..fea650cd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSOSVersionChecker.m @@ -0,0 +1,21 @@ +// +// STDSOSVersionChecker.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSOSVersionChecker.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSOSVersionChecker + ++ (BOOL)isSupportedOSVersion { + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSProcessingView.h b/Stripe3DS2/Stripe3DS2/STDSProcessingView.h new file mode 100644 index 00000000..95364fc5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSProcessingView.h @@ -0,0 +1,26 @@ +// +// STDSProcessingView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/19/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class STDSUICustomization; + +@interface STDSProcessingView : UIView + +/// Defaults to NO +@property (nonatomic) BOOL shouldDisplayBlurView; +/// Defaults to YES +@property (nonatomic) BOOL shouldDisplayDSLogo; + +- (instancetype)initWithCustomization:(STDSUICustomization *)customization directoryServerLogo:(nullable UIImage *)directoryServerLogo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSProcessingView.m b/Stripe3DS2/Stripe3DS2/STDSProcessingView.m new file mode 100644 index 00000000..3126f974 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSProcessingView.m @@ -0,0 +1,98 @@ +// +// STDSProcessingView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/19/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSProcessingView.h" +#import "STDSStackView.h" +#import "UIView+LayoutSupport.h" +#import "UIFont+DefaultFonts.h" +#import "STDSUICustomization.h" +#import "STDSBundleLocator.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSProcessingView() + +@property (nonatomic, strong) STDSStackView *imageStackView; +@property (nonatomic, strong) UIView *blurViewPlaceholder; +@property (nonatomic, strong) UIImageView *imageView; + +@end + +@implementation STDSProcessingView + +static const CGFloat kProcessingViewHorizontalMargin = 8; +static const CGFloat kProcessingViewTopPadding = 22; +static const CGFloat kProcessingViewBottomPadding = 36; + +- (instancetype)initWithCustomization:(STDSUICustomization *)customization directoryServerLogo:(nullable UIImage *)directoryServerLogo { + self = [super initWithFrame:CGRectZero]; + + if (self) { + _blurViewPlaceholder = [UIView new]; + _imageView = [[UIImageView alloc] initWithImage:directoryServerLogo]; + _imageView.contentMode = UIViewContentModeScaleAspectFit; + _shouldDisplayDSLogo = YES; + [self _setupViewHierarchyWithCustomization:customization]; + } + + return self; +} + +- (void)setShouldDisplayBlurView:(BOOL)shouldDisplayBlurView { + _shouldDisplayBlurView = shouldDisplayBlurView; + self.blurViewPlaceholder.hidden = shouldDisplayBlurView; +} + +- (void)setShouldDisplayDSLogo:(BOOL)shouldDisplayDSLogo { + _shouldDisplayDSLogo = shouldDisplayDSLogo; + self.imageView.hidden = !shouldDisplayDSLogo; +} + +- (void)_setupViewHierarchyWithCustomization:(STDSUICustomization *)customization { + self.blurViewPlaceholder.backgroundColor = customization.backgroundColor; + UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:customization.blurStyle]]; + blurView.translatesAutoresizingMaskIntoConstraints = NO; + + STDSStackView *containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + containerView.backgroundColor = customization.backgroundColor; + containerView.layer.cornerRadius = 13; + containerView.translatesAutoresizingMaskIntoConstraints = NO; + + UIActivityIndicatorView *indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:customization.activityIndicatorViewStyle]; + + [self addSubview:blurView]; + [blurView.contentView addSubview:self.blurViewPlaceholder]; + [self addSubview:containerView]; + + [self.blurViewPlaceholder _stds_pinToSuperviewBoundsWithoutMargin]; + [blurView _stds_pinToSuperviewBoundsWithoutMargin]; + + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:containerView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:containerView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0]; + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:containerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.0 constant:-kProcessingViewHorizontalMargin * 2]; + + [NSLayoutConstraint activateConstraints:@[ + centerXConstraint, + centerYConstraint, + widthConstraint, + [containerView.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor], + [self.bottomAnchor constraintGreaterThanOrEqualToAnchor:containerView.bottomAnchor], + ]]; + + [containerView addSpacer:kProcessingViewTopPadding]; + [containerView addArrangedSubview:self.imageView]; + [containerView addSpacer:20]; + [containerView addArrangedSubview:indicatorView]; + [containerView addSpacer:kProcessingViewBottomPadding]; + + [indicatorView startAnimating]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSProgressViewController.h b/Stripe3DS2/Stripe3DS2/STDSProgressViewController.h new file mode 100644 index 00000000..147fa4e4 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSProgressViewController.h @@ -0,0 +1,23 @@ +// +// STDSProgressViewController.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 5/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServer.h" + +@class STDSImageLoader, STDSUICustomization; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSProgressViewController : UIViewController + +- (instancetype)initWithDirectoryServer:(STDSDirectoryServer)directoryServer uiCustomization:(STDSUICustomization * _Nullable)uiCustomization didCancel:(void (^)(void))didCancel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSProgressViewController.m b/Stripe3DS2/Stripe3DS2/STDSProgressViewController.m new file mode 100644 index 00000000..a6de17cd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSProgressViewController.m @@ -0,0 +1,58 @@ +// +// STDSProgressViewController.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 5/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSProgressViewController.h" + +#import "STDSBundleLocator.h" +#import "STDSUICustomization.h" +#import "UIViewController+Stripe3DS2.h" +#import "STDSProcessingView.h" +#import "STDSVisionSupport.h" + +@interface STDSProgressViewController() +@property (nonatomic, strong, nullable) STDSUICustomization *uiCustomization; +@property (nonatomic, strong) void (^didCancel)(void); +@property (nonatomic) STDSDirectoryServer directoryServer; +@end + +@implementation STDSProgressViewController + +- (instancetype)initWithDirectoryServer:(STDSDirectoryServer)directoryServer uiCustomization:(STDSUICustomization * _Nullable)uiCustomization didCancel:(void (^)(void))didCancel { + self = [super initWithNibName:nil bundle:nil]; + + if (self) { + _directoryServer = directoryServer; + _uiCustomization = uiCustomization; + _didCancel = didCancel; + } + + return self; +} + +- (void)loadView { + NSString *imageName = STDSDirectoryServerImageName(self.directoryServer); + UIImage *dsImage = imageName ? [UIImage imageNamed:imageName inBundle:[STDSBundleLocator stdsResourcesBundle] compatibleWithTraitCollection:nil] : nil; + self.view = [[STDSProcessingView alloc] initWithCustomization:self.uiCustomization directoryServerLogo:dsImage]; +} + +#if !STP_TARGET_VISION +- (UIStatusBarStyle)preferredStatusBarStyle { + return self.uiCustomization.preferredStatusBarStyle; +} +#endif + +- (void)viewDidLoad { + [super viewDidLoad]; + [self _stds_setupNavigationBarElementsWithCustomization:self.uiCustomization cancelButtonSelector:@selector(_cancelButtonTapped:)]; +} + +- (void)_cancelButtonTapped:(UIButton *)sender { + self.didCancel(); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.h b/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.h new file mode 100644 index 00000000..d0d416e6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.h @@ -0,0 +1,49 @@ +// +// STDSSecTypeUtilities.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServer.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Returns the SecCertificateRef for the specified server or NULL if there's an error +SecCertificateRef _Nullable STDSCertificateForServer(STDSDirectoryServer server); + +/// Returns the public key in the certificate or NULL if there's an error +SecKeyRef _Nullable SecCertificateCopyKey(SecCertificateRef certificate); + +/// Returns one of the values defined for kSecAttrKeyType in or NULL +CFStringRef _Nullable STDSSecCertificateCopyPublicKeyType(SecCertificateRef certificate); + +/// Returns the hashed secret or nil +NSData * _Nullable STDSCreateConcatKDFWithSHA256(NSData *sharedSecret, NSUInteger keyLength, NSString *apv); + +/// Verifies the signature and payload using the Elliptic Curve P-256 with coordinateX and coordinateY +BOOL STDSVerifyEllipticCurveP256Signature(NSData *coordinateX, NSData *coordinateY, NSData *payload, NSData *signature); + +/// Verifies the signature and payload using RSA-PSS +BOOL STDSVerifyRSASignature(SecCertificateRef certificate, NSData *payload, NSData *signature); + +/// Returns data of length numBytes generated using CommonCrypto's CCRandomGenerateBytes or nil on failure +NSData * _Nullable STDSCryptoRandomData(size_t numBytes); + +/// Creates a certificate from base64encoded data +SecCertificateRef _Nullable STDSSecCertificateFromData(NSData *data); + +/// Creates a certificate from a PEM or DER encoded certificate string +SecCertificateRef _Nullable STDSSecCertificateFromString(NSString *certificateString); + +// Creates a public key using Elliptic Curve P-256 with coordinateX and coordinateY +SecKeyRef _Nullable STDSSecKeyRefFromCoordinates(NSData *coordinateX, NSData *coordinateY); + +// Creates a private key using Elliptic Curve P-256 with x, y, and d +SecKeyRef _Nullable STDSPrivateSecKeyRefFromCoordinates(NSData *x, NSData *y, NSData *d); + + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.m b/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.m new file mode 100644 index 00000000..2a2331fb --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSecTypeUtilities.m @@ -0,0 +1,366 @@ +// +// STDSSecTypeUtilities.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSecTypeUtilities.h" + +#import +#import +#import + +#import "STDSBundleLocator.h" +#import "STDSEllipticCurvePoint.h" + +NS_ASSUME_NONNULL_BEGIN + +SecCertificateRef _Nullable STDSCertificateForServer(STDSDirectoryServer server) { + static NSMutableDictionary *sCertificateData = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sCertificateData = [[NSMutableDictionary alloc] init]; + }); + + NSString *serverKey = nil; + switch (server) { + case STDSDirectoryServerULTestRSA: + serverKey = @"STDSDirectoryServerULTestRSA"; + break; + + case STDSDirectoryServerULTestEC: + serverKey = @"STDSDirectoryServerULTestEC"; + break; + + case STDSDirectoryServerSTPTestRSA: + serverKey = @"STDSDirectoryServerSTPTestRSA"; + break; + + case STDSDirectoryServerSTPTestEC: + serverKey = @"STDSDirectoryServerSTPTestEC"; + break; + + case STDSDirectoryServerAmex: + serverKey = @"STDSDirectoryServerAmex"; + break; + + case STDSDirectoryServerDiscover: + serverKey = @"STDSDirectoryServerDiscover"; + break; + + case STDSDirectoryServerMastercard: + serverKey = @"STDSDirectoryServerMastercard"; + break; + + case STDSDirectoryServerVisa: + serverKey = @"STDSDirectoryServerVisa"; + break; + + case STDSDirectoryServerCartesBancaires: + serverKey = @"STDSDirectoryServerCartesBancaires"; + break; + + case STDSDirectoryServerCustom: + break; + + case STDSDirectoryServerUnknown: + break; + } + + if (serverKey == nil) { + return NULL; + } + + NSData *certificateData = sCertificateData[serverKey]; + if (certificateData == nil) { + NSString *certificatePath = nil; + switch (server) { + case STDSDirectoryServerULTestRSA: + break; + + case STDSDirectoryServerULTestEC: + break; + + case STDSDirectoryServerSTPTestRSA: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"ul-test" ofType:@"der"]; + break; + + case STDSDirectoryServerSTPTestEC: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"ec_test" ofType:@"der"]; + break; + + case STDSDirectoryServerAmex: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"amex" ofType:@"der"]; + break; + + case STDSDirectoryServerDiscover: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"discover" ofType:@"der"]; + break; + + case STDSDirectoryServerMastercard: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"mastercard" ofType:@"der"]; + break; + + case STDSDirectoryServerVisa: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"visa" ofType:@"der"]; + break; + + case STDSDirectoryServerCartesBancaires: + certificatePath = [[STDSBundleLocator stdsResourcesBundle] pathForResource:@"cartes_bancaires" ofType:@"der"]; + break; + + case STDSDirectoryServerCustom: + break; + + case STDSDirectoryServerUnknown: + break; + } + + if (certificatePath != nil) { + certificateData = [NSData dataWithContentsOfFile:certificatePath]; + // cache the file data to limit file IO + sCertificateData[serverKey] = certificateData; + } + } + + // Note to Future: SecCertificateCreateWithData only works with DER formatted data. The other popular + // format for certificate files is PEM. These can be converted before adding to the SDK by invoking + // `openssl x509 -in certificate_PEM.crt -outform der -out certificate_DER.der` + return certificateData != nil ? SecCertificateCreateWithData(NULL, (CFDataRef)certificateData): NULL; +}; + +CFStringRef _Nullable STDSSecCertificateCopyPublicKeyType(SecCertificateRef certificate) { + CFStringRef ret = NULL; + + SecKeyRef key = SecCertificateCopyKey(certificate); + + if (key != NULL) { + CFDictionaryRef attributes = SecKeyCopyAttributes(key); + if (attributes == NULL) { + CFRelease(key); + return NULL; + } + + if (attributes != NULL) { + const void *keyType = CFDictionaryGetValue(attributes, kSecAttrKeyType); + if (keyType != NULL && CFGetTypeID(keyType) == CFStringGetTypeID()) { + ret = CFStringCreateCopy(kCFAllocatorDefault, (CFStringRef)keyType); + } + CFRelease(attributes); + } + CFRelease(key); + } + + return ret; +} + +SecCertificateRef _Nullable STDSSecCertificateFromString(NSString *certificateString) { + static NSString * const kCertificateAnchorPrefix = @"-----BEGIN CERTIFICATE-----"; + static NSString * const kCertificateAnchorSuffix = @"-----END CERTIFICATE-----"; + + // first remove newlines + NSString *certificateStringNoAnchors = [[[certificateString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@""]; + + // remove the begin/end certificate markers + NSUInteger fromIndex = [certificateStringNoAnchors hasPrefix:kCertificateAnchorPrefix] ? kCertificateAnchorPrefix.length : 0; + NSUInteger toIndex = [certificateStringNoAnchors hasSuffix:kCertificateAnchorSuffix] ? certificateStringNoAnchors.length - kCertificateAnchorSuffix.length : certificateStringNoAnchors.length; + certificateStringNoAnchors = [[certificateStringNoAnchors substringWithRange:NSMakeRange(fromIndex, toIndex - fromIndex)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if (certificateStringNoAnchors.length == 0) { + return NULL; + } + NSData *certificateData = [[NSData alloc] initWithBase64EncodedString:certificateStringNoAnchors options:0]; + if (certificateData == nil) { + return NULL; + } + + return STDSSecCertificateFromData(certificateData); +} + +SecCertificateRef _Nullable STDSSecCertificateFromData(NSData *data) { + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data); + return certificate; +} + +SecKeyRef _Nullable STDSPrivateSecKeyRefFromCoordinates(NSData *x, NSData *y, NSData *d) { + static unsigned char prefixBytes[] = {0x04}; + NSMutableData *bytes = [[NSMutableData alloc] initWithBytes:(void *)prefixBytes length:1]; + [bytes appendData:x]; + [bytes appendData:y]; + [bytes appendData:d]; + NSDictionary *attributes = @{ + (__bridge NSString *)kSecAttrKeyType: (__bridge NSString *)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge NSString *)kSecAttrKeyClass: (__bridge NSString *)kSecAttrKeyClassPrivate, + (__bridge NSString *)kSecAttrKeySizeInBits: @(256), + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef)bytes, (__bridge CFDictionaryRef)attributes, &error); + return key; +} + +SecKeyRef _Nullable STDSSecKeyRefFromCoordinates(NSData *coordinateX, NSData *coordinateY) { + static unsigned char prefixBytes[] = {0x04}; + NSMutableData *bytes = [[NSMutableData alloc] initWithBytes:(void *)prefixBytes length:1]; + [bytes appendData:coordinateX]; + [bytes appendData:coordinateY]; + NSDictionary *attributes = @{ + (__bridge NSString *)kSecAttrKeyType: (__bridge NSString *)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge NSString *)kSecAttrKeyClass: (__bridge NSString *)kSecAttrKeyClassPublic, + (__bridge NSString *)kSecAttrKeySizeInBits: @(256), + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef)bytes, (__bridge CFDictionaryRef)attributes, &error); + return key; +} + +// ref. https://crypto.stackexchange.com/questions/1795/how-can-i-convert-a-der-ecdsa-signature-to-asn-1/1797#1797 +NSData * _Nullable STDSDEREncodedSignature(NSData * signature) { + // make sure input signature is of correct R || S format + NSUInteger signatureLength = signature.length; + if (signatureLength == 0 || signatureLength % 2 != 0) { + return nil; + } + + static const uint8_t bytePrefix = 0x00; + + NSMutableData *rBytes = [[NSMutableData alloc] init]; + uint8_t firstRByte; + [signature getBytes:&firstRByte length:1]; + + if (firstRByte >= 0x80) { + // "Signed big-endian encoding of minimal length", we can't have the first bit be 1 because these are postive values + [rBytes appendBytes:&bytePrefix length:1]; + } + [rBytes appendBytes:signature.bytes length:signatureLength / 2]; + + NSMutableData *sBytes = [[NSMutableData alloc] init]; + uint8_t firstSByte; + [signature getBytes:&firstSByte range:NSMakeRange(signatureLength / 2, 1)]; + + if (firstSByte >= 0x80) { + // "Signed big-endian encoding of minimal length", we can't have the first bit be 1 because these are postive values + [sBytes appendBytes:&bytePrefix length:1]; + } + [sBytes appendBytes:(signature.bytes + (signatureLength / 2)) length:signatureLength / 2]; + + uint8_t rLength = (uint8_t)rBytes.length; + uint8_t sLength = (uint8_t)sBytes.length; + + static const uint8_t derBytePrefix = 0x30; + NSMutableData *derEncoded = [[NSMutableData alloc] initWithBytes:&derBytePrefix length:1]; + + static const uint8_t derSeparatorByte = 0x02; + // numBytes does not include the 0x30 byte + uint8_t numBytes = rLength + sLength + 2 + 2; // + 2 for separators, + 2 for r and s size bytes + [derEncoded appendBytes:&numBytes length:1]; + [derEncoded appendBytes:&derSeparatorByte length:1]; + + [derEncoded appendBytes:&rLength length:1]; + [derEncoded appendBytes:rBytes.bytes length:rBytes.length]; + + [derEncoded appendBytes:&derSeparatorByte length:1]; + + [derEncoded appendBytes:&sLength length:1]; + [derEncoded appendBytes:sBytes.bytes length:sBytes.length]; + + return [derEncoded copy]; +} + +BOOL STDSVerifyEllipticCurveP256Signature(NSData *coordinateX, NSData *coordinateY, NSData *payload, NSData *signature) { + BOOL ret = NO; + + // make P-256 curve key from coordinates + SecKeyRef key = STDSSecKeyRefFromCoordinates(coordinateX, coordinateY); + + if (key != NULL) { + size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH; + unsigned char hashBytes[hashBytesSize]; + CC_SHA256(payload.bytes, (CC_LONG)payload.length, hashBytes); + CFErrorRef error = NULL; + NSData *derEncodedSignature = STDSDEREncodedSignature(signature); + if (derEncodedSignature == nil) { + CFRelease(key); + return NO; + } + ret = (BOOL)SecKeyVerifySignature(key, kSecKeyAlgorithmECDSASignatureDigestX962SHA256, (__bridge CFDataRef)[NSData dataWithBytes:hashBytes length:hashBytesSize], (__bridge CFDataRef)derEncodedSignature, &error); + CFRelease(key); + } + return ret; +} + +BOOL STDSVerifyRSASignature(SecCertificateRef certificate, NSData *payload, NSData *signature) { + BOOL ret = NO; + + SecKeyRef key = SecCertificateCopyKey(certificate); + if (key != NULL && signature) { + CFErrorRef error = NULL; + size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH; + unsigned char hashBytes[hashBytesSize]; + CC_SHA256(payload.bytes, (CC_LONG)payload.length, hashBytes); + + ret = (BOOL)SecKeyVerifySignature(key, kSecKeyAlgorithmRSASignatureDigestPSSSHA256, (__bridge CFDataRef)[NSData dataWithBytes:hashBytes length:hashBytesSize], (__bridge CFDataRef)signature, &error); + CFRelease(key); + } + return ret; +} + +NSData * _Nullable STDSCryptoRandomData(size_t numBytes) { + void *randomBytes[numBytes]; + memset(randomBytes, 0, numBytes); + if (CCRandomGenerateBytes(randomBytes, numBytes) == kCCSuccess) { + NSData *data = [NSData dataWithBytes:randomBytes length:numBytes]; + return data; + } + return NULL; +} + +NSData * _Nullable _STPCreateKDFFormattedData(NSData *data) { + uint32_t bigEndianLength = CFSwapInt32HostToBig((uint32_t)data.length); + NSMutableData *encodedLength = [NSMutableData dataWithBytes:&bigEndianLength length:4]; + [encodedLength appendData:data]; + return [encodedLength copy]; +} + +NSData * _Nullable STDSCreateConcatKDFWithSHA256(NSData *sharedSecret, NSUInteger keyLength, NSString *apv) { + NSData *concatKDFData = nil; + + uint32_t bigEndianKeyLength = CFSwapInt32HostToBig((uint32_t)keyLength*8); + + // algorithmID and partyUInfo are intentionally empty strings based on the Core Spec + // section 6.2.3.3 which states that they should be null. The KDF standard, NIST.800-56A, + // requires that null values still have the length bytes set to 0. + NSData *algorithmID = _STPCreateKDFFormattedData([@"" dataUsingEncoding:NSASCIIStringEncoding]); + NSData *partyUInfo = _STPCreateKDFFormattedData([@"" dataUsingEncoding:NSUTF8StringEncoding]); + NSData *partyVInfo = _STPCreateKDFFormattedData([apv dataUsingEncoding:NSUTF8StringEncoding]); + NSData *suppPubInfo = [NSData dataWithBytes:&bigEndianKeyLength length:4]; + + if (algorithmID == nil || + partyUInfo == nil || + partyVInfo == nil || + suppPubInfo == nil + ) { + return nil; + } + + NSMutableData *otherInfo = [algorithmID mutableCopy]; + [otherInfo appendData:partyUInfo]; + [otherInfo appendData:partyVInfo]; + [otherInfo appendData:suppPubInfo]; + + const unsigned char roundOneBytes[4] = {0, 0, 0, 1}; + NSMutableData *roundOneHashInput = [[NSMutableData alloc] initWithBytes:roundOneBytes length:4]; + [roundOneHashInput appendData:sharedSecret]; + [roundOneHashInput appendData:otherInfo]; + + NSMutableData *roundOneHashOutput = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH]; + + if (CC_SHA256(roundOneHashInput.bytes, (CC_LONG)roundOneHashInput.length, roundOneHashOutput.mutableBytes) != nil) { + concatKDFData = [NSData dataWithBytes:roundOneHashOutput.bytes length:MAX(keyLength, roundOneHashOutput.length)]; + } + + return concatKDFData; +} + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSelectionButton.h b/Stripe3DS2/Stripe3DS2/STDSSelectionButton.h new file mode 100644 index 00000000..5cb78b7c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSelectionButton.h @@ -0,0 +1,26 @@ +// +// STDSSelectionButton.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSSelectionCustomization; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSSelectionButton : UIButton + +@property (nonatomic) STDSSelectionCustomization *customization; + +/// This control can either be styled as a radio button or a checkbox +@property (nonatomic) BOOL isCheckbox; + +- (instancetype)initWithCustomization:(STDSSelectionCustomization *)customization; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSelectionButton.m b/Stripe3DS2/Stripe3DS2/STDSSelectionButton.m new file mode 100644 index 00000000..97bf19e4 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSelectionButton.m @@ -0,0 +1,167 @@ +// +// STDSSelectionButton.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSelectionButton.h" + +#import "STDSSelectionCustomization.h" + +@interface _STDSSelectionButtonView: UIView +@property (nonatomic) BOOL isCheckbox; +@property (nonatomic) STDSSelectionCustomization *customization; +@property (nonatomic, getter = isSelected) BOOL selected; +@end + +@implementation _STDSSelectionButtonView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.opaque = NO; + } + + return self; +} + +- (void)setSelected:(BOOL)selected { + _selected = selected; + [self setNeedsDisplay]; +} + + +- (void)setCustomization:(STDSSelectionCustomization *)customization { + _customization = customization; + [self setNeedsDisplay]; +} + + +- (void)drawRect:(CGRect)rect { + if (self.isCheckbox) { + [self _drawCheckboxWithRect:rect]; + } else { + [self _drawRadioButtonWithRect:rect]; + } +} + +- (void)_drawRadioButtonWithRect:(CGRect)rect { + // Draw background + UIBezierPath *background = [UIBezierPath bezierPathWithOvalInRect:rect]; + if (self.isSelected) { + [self.customization.primarySelectedColor setFill]; + } else { + [self.customization.unselectedBackgroundColor setFill]; + } + [background fill]; + + // Draw unselected border + if (!self.isSelected) { + UIBezierPath *border = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(rect, 0.5, 0.5)]; + [self.customization.unselectedBorderColor setStroke]; + [border stroke]; + } + + // Draw inner circle if selected + if (self.isSelected) { + CGRect selectedRect = CGRectInset(rect, 9, 9); + UIBezierPath *selected = [UIBezierPath bezierPathWithOvalInRect:selectedRect]; + [self.customization.secondarySelectedColor setFill]; + [selected fill]; + } +} + +- (void)_drawCheckboxWithRect:(CGRect)rect { + // Draw background + UIBezierPath *background = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8]; + if (self.isSelected) { + [self.customization.primarySelectedColor setFill]; + } else { + [self.customization.unselectedBackgroundColor setFill]; + } + [background fill]; + + // Draw unselected border + if (!self.isSelected) { + UIBezierPath *border = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, 0.5, 0.5) cornerRadius:8]; + border.lineWidth = 0.5; + [self.customization.unselectedBorderColor setStroke]; + [border stroke]; + } + + // Draw check mark if selected + if (self.isSelected) { + UIBezierPath *checkmark = [UIBezierPath bezierPath]; + [checkmark moveToPoint: CGPointMake(10, 15)]; + [checkmark addLineToPoint:CGPointMake(13.5, 18.5)]; + [checkmark addLineToPoint:CGPointMake(22, 10)]; + [self.customization.secondarySelectedColor setStroke]; + checkmark.lineWidth = 2; + [checkmark stroke]; + } +} + +@end + +static const CGFloat kMinimumTouchAreaDimension = 42.f; +static const CGFloat kContentSizeDimension = 30.f; + +@implementation STDSSelectionButton { + _STDSSelectionButtonView *_contentView; +} + +- (instancetype)initWithCustomization:(STDSSelectionCustomization *)customization { + self = [super init]; + if (self) { + _contentView = [[_STDSSelectionButtonView alloc] initWithFrame:CGRectMake(0, 0, kContentSizeDimension, kContentSizeDimension)]; + _contentView.userInteractionEnabled = NO; + [self addSubview:_contentView]; + self.customization = customization; + } + return self; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + BOOL pointInside = [super pointInside:point withEvent:event]; + if (!pointInside && + self.enabled && + !self.isHidden && + (CGRectGetWidth(self.bounds) < kMinimumTouchAreaDimension || CGRectGetHeight(self.bounds) < kMinimumTouchAreaDimension) + ) { + // Make sure that we intercept touch events even outside our bounds if they are within the + // minimum touch area. Otherwise this button is too hard to tap + CGRect expandedBounds = CGRectInset(self.bounds, MIN(CGRectGetWidth(self.bounds) - kMinimumTouchAreaDimension, 0), MIN(CGRectGetHeight(self.bounds) < kMinimumTouchAreaDimension, 0)); + pointInside = CGRectContainsPoint(expandedBounds, point); + } + + return pointInside; +} + +- (CGSize)intrinsicContentSize { + return CGSizeMake(kContentSizeDimension, kContentSizeDimension); +} + +- (void)setSelected:(BOOL)selected { + [super setSelected:selected]; + _contentView.selected = selected; +} + +- (void)setCustomization:(STDSSelectionCustomization *)customization { + _contentView.customization = customization; +} + +- (STDSSelectionCustomization *)customization { + return _contentView.customization; +} + +- (void)setIsCheckbox:(BOOL)isCheckbox { + _contentView.isCheckbox = isCheckbox; +} + +- (BOOL)isCheckbox { + return _contentView.isCheckbox; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.h b/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.h new file mode 100644 index 00000000..7b6e77d7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.h @@ -0,0 +1,19 @@ +// +// STDSSimulatorChecker.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSSimulatorChecker : NSObject + ++ (BOOL)isRunningOnSimulator; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.m b/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.m new file mode 100644 index 00000000..47ea89e1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSimulatorChecker.m @@ -0,0 +1,25 @@ +// +// STDSSimulatorChecker.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/8/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSimulatorChecker.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSSimulatorChecker + ++ (BOOL)isRunningOnSimulator { +#if TARGET_IPHONE_SIMULATOR + return YES; +#else + return NO; +#endif +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSpacerView.h b/Stripe3DS2/Stripe3DS2/STDSSpacerView.h new file mode 100644 index 00000000..072ad1ed --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSpacerView.h @@ -0,0 +1,20 @@ +// +// STDSSpacerView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSStackView.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSSpacerView : UIView + +- (instancetype)initWithLayoutAxis:(STDSStackViewLayoutAxis)layoutAxis dimension:(CGFloat)dimension; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSpacerView.m b/Stripe3DS2/Stripe3DS2/STDSSpacerView.m new file mode 100644 index 00000000..a674e68a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSpacerView.m @@ -0,0 +1,34 @@ +// +// STDSSpacerView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSpacerView.h" + +@implementation STDSSpacerView + +- (instancetype)initWithLayoutAxis:(STDSStackViewLayoutAxis)layoutAxis dimension:(CGFloat)dimension { + self = [super initWithFrame:CGRectZero]; + + if (self) { + NSLayoutConstraint *constraint; + + switch (layoutAxis) { + case STDSStackViewLayoutAxisHorizontal: + constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:dimension]; + break; + case STDSStackViewLayoutAxisVertical: + constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:dimension]; + break; + } + + [NSLayoutConstraint activateConstraints:@[constraint]]; + } + + return self; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSStackView.h b/Stripe3DS2/Stripe3DS2/STDSStackView.h new file mode 100644 index 00000000..cfedb01a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSStackView.h @@ -0,0 +1,56 @@ +// +// STDSStackView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, STDSStackViewLayoutAxis) { + + /// A horizontal layout for the stack view to use. + STDSStackViewLayoutAxisHorizontal = 0, + + /// A vertical layout for the stack view to use. + STDSStackViewLayoutAxisVertical = 1, +}; + +@interface STDSStackView: UIView + +/** + Initializes an `STDSStackView`. + + @param alignment The alignment for the stack view to use. + @return An initialized `STDSStackView`. + */ +- (instancetype)initWithAlignment:(STDSStackViewLayoutAxis)alignment; + +/** + Adds a subview to the list of arranged subviews. Views will be displayed in the order they are added. + + @param view The view to add to the stack view. + */ +- (void)addArrangedSubview:(UIView *)view; + +/** + Removes a subview from the list of arranged subviews. + + @param view The view to remove. + */ +- (void)removeArrangedSubview:(UIView *)view; + +/** + Adds a spacer that fits the layout axis of the `STDSStackView`. + + @param dimension How wide or tall the spacer should be, depending on the axis of the `STDSStackView`. + @note Spacers added through this function will not be removed or hidden automatically when they no longer fall between two views. For more precise interactions, add an `STDSSpacerView` manually through `addArrangedSubview:`. + */ +- (void)addSpacer:(CGFloat)dimension; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSStackView.m b/Stripe3DS2/Stripe3DS2/STDSStackView.m new file mode 100644 index 00000000..7ec01d1e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSStackView.m @@ -0,0 +1,164 @@ +// +// STDSStackView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSStackView.h" +#import "STDSSpacerView.h" +#import "NSLayoutConstraint+LayoutSupport.h" + +@interface STDSStackView() + +@property (nonatomic) STDSStackViewLayoutAxis layoutAxis; +@property (nonatomic, strong) NSMutableArray *arrangedSubviews; +@property (nonatomic, strong, readonly) NSArray *visibleArrangedSubviews; + +@end + +@implementation STDSStackView + +static NSString *UIViewHiddenKeyPath = @"hidden"; + +- (instancetype)initWithAlignment:(STDSStackViewLayoutAxis)layoutAxis { + self = [super initWithFrame:CGRectZero]; + + if (self) { + _layoutAxis = layoutAxis; + _arrangedSubviews = [NSMutableArray array]; + } + + return self; +} + +- (NSArray *)visibleArrangedSubviews { + return [self.arrangedSubviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIView *object, NSDictionary *bindings) { + return !object.isHidden; + }]]; +} + +- (void)addArrangedSubview:(UIView *)view { + view.translatesAutoresizingMaskIntoConstraints = false; + + [self _deactivateExistingConstraints]; + + [self.arrangedSubviews addObject:view]; + [self addSubview:view]; + + [self _applyConstraints]; + + [view addObserver:self forKeyPath:UIViewHiddenKeyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; +} + +- (void)removeArrangedSubview:(UIView *)view { + if (![self.arrangedSubviews containsObject:view]) { + return; + } + + [self _deactivateExistingConstraints]; + + [view removeObserver:self forKeyPath:UIViewHiddenKeyPath]; + + [self.arrangedSubviews removeObject:view]; + [view removeFromSuperview]; + + [self _applyConstraints]; +} + +- (void)addSpacer:(CGFloat)dimension { + STDSSpacerView *spacerView = [[STDSSpacerView alloc] initWithLayoutAxis:self.layoutAxis dimension:dimension]; + + [self addArrangedSubview:spacerView]; +} + +- (void)dealloc { + for (UIView *view in self.arrangedSubviews) { + [view removeObserver:self forKeyPath:UIViewHiddenKeyPath]; + } +} + +- (void)_applyConstraints { + if (self.layoutAxis == STDSStackViewLayoutAxisHorizontal) { + [self _applyHorizontalConstraints]; + } else { + [self _applyVerticalConstraints]; + } +} + +- (void)_deactivateExistingConstraints { + [NSLayoutConstraint deactivateConstraints:self.constraints]; +} + +- (void)_applyVerticalConstraints { + UIView *previousView; + + for (UIView *view in self.visibleArrangedSubviews) { + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint _stds_leftConstraintWithItem:view toItem:self]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint _stds_rightConstraintWithItem:view toItem:self]; + NSLayoutConstraint *topConstraint; + + if (previousView == nil) { + topConstraint = [NSLayoutConstraint _stds_topConstraintWithItem:view toItem:self]; + } else { + topConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:previousView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; + } + + [NSLayoutConstraint activateConstraints:@[topConstraint, leftConstraint, rightConstraint]]; + + if (view == self.visibleArrangedSubviews.lastObject) { + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint _stds_bottomConstraintWithItem:view toItem:self]; + + [NSLayoutConstraint activateConstraints:@[bottomConstraint]]; + } + + previousView = view; + } +} + +- (void)_applyHorizontalConstraints { + UIView *previousView; + NSLayoutConstraint *previousRightConstraint; + + for (UIView *view in self.visibleArrangedSubviews) { + NSLayoutConstraint *topConstraint = [NSLayoutConstraint _stds_topConstraintWithItem:view toItem:self]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint _stds_bottomConstraintWithItem:view toItem:self]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint _stds_rightConstraintWithItem:view toItem:self]; + + if (previousView == nil) { + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint _stds_leftConstraintWithItem:view toItem:self]; + + [NSLayoutConstraint activateConstraints:@[topConstraint, leftConstraint, rightConstraint, bottomConstraint]]; + } else { + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:previousView attribute:NSLayoutAttributeRight multiplier:1 constant:0]; + + if (previousRightConstraint != nil) { + [NSLayoutConstraint deactivateConstraints:@[previousRightConstraint]]; + } + + NSLayoutConstraint *previousConstraint = [NSLayoutConstraint constraintWithItem:previousView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeLeft multiplier:1 constant:0]; + + [NSLayoutConstraint activateConstraints:@[topConstraint, leftConstraint, rightConstraint, previousConstraint, bottomConstraint]]; + } + + previousView = view; + previousRightConstraint = rightConstraint; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([object isKindOfClass:[UIView class]] && [keyPath isEqualToString:UIViewHiddenKeyPath]) { + BOOL hiddenStatusChanged = [change[NSKeyValueChangeNewKey] boolValue] != [change[NSKeyValueChangeOldKey] boolValue]; + + if (hiddenStatusChanged) { + [self _deactivateExistingConstraints]; + + [self _applyConstraints]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.h b/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.h new file mode 100644 index 00000000..883c6a35 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.h @@ -0,0 +1,26 @@ +// +// STDSSynchronousLocationManager.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class CLLocation; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSSynchronousLocationManager : NSObject + ++ (instancetype)sharedManager; + ++ (BOOL)hasPermissions; + +// May be long running. Will return nil on failure or if app lacks permissions +- (nullable CLLocation *)deviceLocation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.m b/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.m new file mode 100644 index 00000000..8d73003f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSSynchronousLocationManager.m @@ -0,0 +1,123 @@ +// +// STDSSynchronousLocationManager.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/23/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSynchronousLocationManager.h" +#import "STDSVisionSupport.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +static const int64_t kLocationFetchTimeoutSeconds = 15; + +typedef void (^LocationUpdateCompletionBlock)(CLLocation * _Nullable); + +@interface STDSSynchronousLocationManager () + +@end + +@implementation STDSSynchronousLocationManager +{ + CLLocationManager * _Nullable _locationManager; + dispatch_queue_t _Nullable _locationFetchQueue; + NSMutableArray *_pendingLocationUpdateCompletions; +} + ++ (instancetype)sharedManager { + static STDSSynchronousLocationManager *sharedManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedManager = [[STDSSynchronousLocationManager alloc] init]; + }); + return sharedManager; +} + ++ (BOOL)hasPermissions { +// TODO: Revisit this after we drop iOS 13, iOS 14 has a new API for authorizationStatus +#ifdef STP_TARGET_VISION + if (@available(iOS 14.0, *)) { + CLAuthorizationStatus authorizationStatus = [[[CLLocationManager alloc] init] authorizationStatus]; + return [CLLocationManager locationServicesEnabled] && + authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse; + } else { + // This should never happen + return NO; + } +#else + CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; + return [CLLocationManager locationServicesEnabled] && + (authorizationStatus == kCLAuthorizationStatusAuthorizedAlways || authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse); +#endif +} + +- (instancetype)init { + self = [super init]; + if (self) { + if ([STDSSynchronousLocationManager hasPermissions]) { + _locationManager = [[CLLocationManager alloc] init]; + _locationManager.delegate = self; + _locationFetchQueue = dispatch_queue_create("com.stripe.3ds2locationqueue", DISPATCH_QUEUE_SERIAL); + } + _pendingLocationUpdateCompletions = [NSMutableArray array]; + } + + return self; +} + +- (nullable CLLocation *)deviceLocation { + + __block CLLocation *location = nil; + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + [self _fetchDeviceLocation:^(CLLocation * _Nullable latestLocation) { + location = latestLocation; + dispatch_group_leave(group); + }]; + + dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * kLocationFetchTimeoutSeconds)); + return location; +} + +- (void)_fetchDeviceLocation:(void (^)(CLLocation * _Nullable))completion { + + if (![STDSSynchronousLocationManager hasPermissions] || _locationFetchQueue == nil) { + return completion(nil); + } + + dispatch_async(_locationFetchQueue, ^{ + [self->_pendingLocationUpdateCompletions addObject:completion]; + + if (self->_pendingLocationUpdateCompletions.count == 1) { + [self->_locationManager requestLocation]; + } + }); +} + +- (void)_stopUpdatingLocationAndReportResult:(nullable CLLocation *)location { + [_locationManager stopUpdatingLocation]; + + dispatch_async(_locationFetchQueue, ^{ + for (LocationUpdateCompletionBlock completion in self->_pendingLocationUpdateCompletions) { + completion(location); + } + [self->_pendingLocationUpdateCompletions removeAllObjects]; + }); +} + +#pragma mark - CLLocationManagerDelegate +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + [self _stopUpdatingLocationAndReportResult:locations.firstObject]; +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { + [self _stopUpdatingLocationAndReportResult:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.h b/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.h new file mode 100644 index 00000000..d44ea54a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.h @@ -0,0 +1,26 @@ +// +// STDSTextChallengeView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/5/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSTextFieldCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSTextField: UITextField + +@end + +@interface STDSTextChallengeView : UIView + +@property (nonatomic, strong, nullable) STDSTextFieldCustomization *textFieldCustomization; +@property (nonatomic, copy, readonly, nullable) NSString *inputText; +@property (nonatomic, strong) STDSTextField *textField; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.m b/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.m new file mode 100644 index 00000000..eb390e5e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSTextChallengeView.m @@ -0,0 +1,128 @@ +// +// STDSTextChallengeView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/5/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSTextChallengeView.h" +#import "STDSStackView.h" +#import "STDSVisionSupport.h" +#import "UIView+LayoutSupport.h" +#import "NSString+EmptyChecking.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSTextField + +static const CGFloat kTextFieldMargin = (CGFloat)8.0; + +- (CGRect)textRectForBounds:(CGRect)bounds { + return CGRectInset(bounds, kTextFieldMargin, 0); +} + +- (CGRect)editingRectForBounds:(CGRect)bounds { + return CGRectInset(bounds, kTextFieldMargin, 0); +} + +- (nullable NSString *)accessibilityIdentifier { + return @"STDSTextField"; +} + +@end + +@interface STDSTextChallengeView() + +@property (nonatomic, strong) STDSStackView *containerView; +@property (nonatomic, strong) NSLayoutConstraint *borderViewHeightConstraint; + +@end + +@implementation STDSTextChallengeView + +static const CGFloat kBorderViewHeight = 1; +static const CGFloat kTextFieldKernSpacing = 3; +static const CGFloat kTextFieldPlaceholderKernSpacing = 14; +static const CGFloat kTextChallengeViewBottomPadding = 11; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)_setupViewHierarchy { + self.layoutMargins = UIEdgeInsetsMake(0, 0, kTextChallengeViewBottomPadding, 0); + + self.containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + + self.textField = [[STDSTextField alloc] init]; + self.textField.autocorrectionType = UITextAutocorrectionTypeNo; + self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.textField.delegate = self; + self.textField.clearButtonMode = UITextFieldViewModeWhileEditing; + self.textField.textContentType = UITextContentTypeOneTimeCode; + [self.textField.defaultTextAttributes setValue:@(kTextFieldKernSpacing) forKey:NSKernAttributeName]; + + UIView *borderView = [UIView new]; + borderView.backgroundColor = [UIColor _stds_colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { + return [[UIColor _stds_systemGray2Color] colorWithAlphaComponent:(CGFloat)0.6]; + }]; + + [self.containerView addArrangedSubview:self.textField]; + [self.containerView addArrangedSubview:borderView]; + [self addSubview:self.containerView]; + [self.containerView _stds_pinToSuperviewBounds]; + + self.borderViewHeightConstraint = [NSLayoutConstraint constraintWithItem:borderView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:kBorderViewHeight]; + [NSLayoutConstraint activateConstraints:@[self.borderViewHeightConstraint]]; +} + +- (void)setTextFieldCustomization:(STDSTextFieldCustomization * _Nullable)textFieldCustomization { + _textFieldCustomization = textFieldCustomization; + + self.textField.font = textFieldCustomization.font; + self.textField.textColor = textFieldCustomization.textColor; + self.textField.layer.borderColor = textFieldCustomization.borderColor.CGColor; + self.textField.layer.borderWidth = textFieldCustomization.borderWidth; + self.textField.layer.cornerRadius = textFieldCustomization.cornerRadius; + self.textField.keyboardAppearance = textFieldCustomization.keyboardAppearance; + NSDictionary *placeholderTextAttributes = @{ + NSKernAttributeName: @(kTextFieldPlaceholderKernSpacing), + NSBaselineOffsetAttributeName: @(3.0f), + NSForegroundColorAttributeName: textFieldCustomization.placeholderTextColor, + }; + self.textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"••••••" attributes:placeholderTextAttributes]; +} + +- (NSString * _Nullable)inputText { + return self.textField.text; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + +#if !STP_TARGET_VISION + if (self.window.screen.nativeScale > 0) { + self.borderViewHeightConstraint.constant = kBorderViewHeight / self.window.screen.nativeScale; + } +#endif +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion+Private.h b/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion+Private.h new file mode 100644 index 00000000..43257e78 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion+Private.h @@ -0,0 +1,53 @@ +// +// STDSThreeDSProtocolVersion+Private.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSThreeDSProtocolVersion.h" + +NS_ASSUME_NONNULL_BEGIN + + +typedef NS_ENUM(NSInteger, STDSThreeDSProtocolVersion) { + STDSThreeDSProtocolVersion2_1_0, + STDSThreeDSProtocolVersion2_2_0, + STDSThreeDSProtocolVersionUnknown, + STDSThreeDSProtocolVersionFallbackTest, +}; + +static NSString * const kThreeDS2ProtocolVersion2_1_0 = @"2.1.0"; +static NSString * const kThreeDS2ProtocolVersion2_2_0 = @"2.2.0"; +static NSString * const kThreeDSProtocolVersionFallbackTest = @"2.0.0"; + +NS_INLINE STDSThreeDSProtocolVersion STDSThreeDSProtocolVersionForString(NSString *stringValue) { + if ([stringValue isEqualToString:kThreeDS2ProtocolVersion2_1_0]) { + return STDSThreeDSProtocolVersion2_1_0; + } else if ([stringValue isEqualToString:kThreeDS2ProtocolVersion2_2_0]) { + return STDSThreeDSProtocolVersion2_2_0; + } else if ([stringValue isEqualToString:kThreeDSProtocolVersionFallbackTest]) { + return STDSThreeDSProtocolVersionFallbackTest; + } else { + return STDSThreeDSProtocolVersionUnknown; + } +} + +NS_INLINE NSString * _Nullable STDSThreeDSProtocolVersionStringValue(STDSThreeDSProtocolVersion protocolVersion) { + switch (protocolVersion) { + case STDSThreeDSProtocolVersion2_1_0: + return kThreeDS2ProtocolVersion2_1_0; + + case STDSThreeDSProtocolVersion2_2_0: + return kThreeDS2ProtocolVersion2_2_0; + + case STDSThreeDSProtocolVersionFallbackTest: + return kThreeDSProtocolVersionFallbackTest; + + case STDSThreeDSProtocolVersionUnknown: + return nil; + } +} + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion.m b/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion.m new file mode 100644 index 00000000..3d275b14 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSThreeDSProtocolVersion.m @@ -0,0 +1,15 @@ +// +// STDSThreeDSProtocolVersion.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 6/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSThreeDSProtocolVersion+Private.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString * const Stripe3DS2ProtocolVersion = @"2.1.0"; + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSTransaction+Private.h b/Stripe3DS2/Stripe3DS2/STDSTransaction+Private.h new file mode 100644 index 00000000..2c35e956 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSTransaction+Private.h @@ -0,0 +1,41 @@ +// +// STDSTransaction+Private.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSTransaction.h" + +@class STDSDeviceInformation; +@class STDSDirectoryServerCertificate; + +#import "STDSDirectoryServer.h" +#import "STDSThreeDSProtocolVersion+Private.h" +#import "STDSUICustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSTransaction () + +- (instancetype)initWithDeviceInformation:(STDSDeviceInformation *)deviceInformation + directoryServer:(STDSDirectoryServer)directoryServer + protocolVersion:(STDSThreeDSProtocolVersion)protocolVersion + uiCustomization:(STDSUICustomization *)uiCustomization; + +- (instancetype)initWithDeviceInformation:(STDSDeviceInformation *)deviceInformation + directoryServerID:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + directoryServerCertificate:(STDSDirectoryServerCertificate *)directoryServerCertificate + rootCertificateStrings:(NSArray *)rootCertificateStrings + protocolVersion:(STDSThreeDSProtocolVersion)protocolVersion + uiCustomization:(STDSUICustomization *)uiCustomization; + +@property (nonatomic, strong) NSTimer *timeoutTimer; +@property (nonatomic) BOOL bypassTestModeVerification; // Should be used during internal testing ONLY +@property (nonatomic) BOOL useULTestLOA; // Should only be used when running tests with the UL reference app + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSVisionSupport.h b/Stripe3DS2/Stripe3DS2/STDSVisionSupport.h new file mode 100644 index 00000000..4d33e15f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSVisionSupport.h @@ -0,0 +1,21 @@ +// +// STDSVisionSupport.h +// Stripe3DS2 +// +// Created by David Estes on 11/21/23. +// + +#ifndef STDSVisionSupport_h +#define STDSVisionSupport_h + +#ifdef TARGET_OS_VISION +#if TARGET_OS_VISION +#define STP_TARGET_VISION 1 +#else +#endif +#else +#define STP_TARGET_VISION 0 +#endif + + +#endif /* STDSVisionSupport_h */ diff --git a/Stripe3DS2/Stripe3DS2/STDSWebView.h b/Stripe3DS2/Stripe3DS2/STDSWebView.h new file mode 100644 index 00000000..7e6673fc --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSWebView.h @@ -0,0 +1,22 @@ +// +// STDSWebView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSWebView : WKWebView + +/** + Convenience method that prepends the given HTML string with a CSP meta tag that disables external resource loading, and passes it to `loadHTMLString:baseURL:`. + */ +- (WKNavigation *)loadExternalResourceBlockingHTMLString:(NSString *)html; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSWebView.m b/Stripe3DS2/Stripe3DS2/STDSWebView.m new file mode 100644 index 00000000..d30da6bc --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSWebView.m @@ -0,0 +1,37 @@ +// +// STDSWebView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSWebView.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSWebView + +- (instancetype)init { + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; + configuration.preferences.javaScriptEnabled = NO; + return [super initWithFrame:CGRectZero configuration:configuration]; +} + +/// Overriden to do nothing per 3DS2 security guidelines. +- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler { + +} + +- (WKNavigation *)loadExternalResourceBlockingHTMLString:(NSString *)html { + NSString *cspMetaTag = @""; + return [self loadHTMLString:[cspMetaTag stringByAppendingString:html] baseURL:nil]; +} + +- (nullable NSString *)accessibilityIdentifier { + return @"STDSWebView"; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSWhitelistView.h b/Stripe3DS2/Stripe3DS2/STDSWhitelistView.h new file mode 100644 index 00000000..a744f2b8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSWhitelistView.h @@ -0,0 +1,25 @@ +// +// STDSWhitelistView.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseSelectionInfo.h" +#import "STDSLabelCustomization.h" +#import "STDSSelectionCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSWhitelistView : UIView + +@property (nonatomic, strong, nullable) NSString *whitelistText; +@property (nonatomic, readonly, nullable) id selectedResponse; +@property (nonatomic, strong, nullable) STDSLabelCustomization *labelCustomization; +@property (nonatomic, strong, nullable) STDSSelectionCustomization *selectionCustomization; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/STDSWhitelistView.m b/Stripe3DS2/Stripe3DS2/STDSWhitelistView.m new file mode 100644 index 00000000..a5181fa0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/STDSWhitelistView.m @@ -0,0 +1,103 @@ +// +// STDSWhitelistView.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSLocalizedString.h" +#import "STDSWhitelistView.h" +#import "STDSStackView.h" +#import "STDSChallengeResponseSelectionInfoObject.h" +#import "NSString+EmptyChecking.h" +#import "UIView+LayoutSupport.h" +#import "STDSSelectionButton.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSWhitelistView() + +@property (nonatomic, strong) UILabel *whitelistLabel; +@property (nonatomic, strong) STDSSelectionButton *selectionButton; + +@end + +@implementation STDSWhitelistView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self _setupViewHierarchy]; + } + + return self; +} + +- (void)_setupViewHierarchy { + self.layoutMargins = UIEdgeInsetsZero; + + STDSStackView *containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [self addSubview:containerView]; + [containerView _stds_pinToSuperviewBounds]; + + self.whitelistLabel = [[UILabel alloc] init]; + self.whitelistLabel.numberOfLines = 0; + + self.selectionButton = [[STDSSelectionButton alloc] initWithCustomization:self.selectionCustomization]; + self.selectionButton.isCheckbox = YES; + [self.selectionButton addTarget:self action:@selector(_selectionButtonWasTapped) forControlEvents:UIControlEventTouchUpInside]; + + UIStackView *stackView = [self _buildStackView]; + [stackView addArrangedSubview:self.selectionButton]; + [stackView addArrangedSubview:self.whitelistLabel]; + + [containerView addArrangedSubview:stackView]; +} + +- (void)setWhitelistText:(NSString * _Nullable)whitelistText { + _whitelistText = whitelistText; + + self.whitelistLabel.text = whitelistText; + self.whitelistLabel.hidden = [NSString _stds_isStringEmpty:whitelistText]; + self.selectionButton.hidden = self.whitelistLabel.hidden; +} + +- (id _Nullable)selectedResponse { + if (self.selectionButton.selected) { + return [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Y" value:STDSLocalizedString(@"Yes", @"The yes answer to a yes or no question.")];; + } + + return [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"N" value:STDSLocalizedString(@"No", @"The no answer to a yes or no question.")]; +} + +- (void)setLabelCustomization:(STDSLabelCustomization * _Nullable)labelCustomization { + _labelCustomization = labelCustomization; + + self.whitelistLabel.font = labelCustomization.font; + self.whitelistLabel.textColor = labelCustomization.textColor; +} + +- (void)setSelectionCustomization:(STDSSelectionCustomization * _Nullable)selectionCustomization { + _selectionCustomization = selectionCustomization; + self.selectionButton.customization = selectionCustomization; +} + +- (UIStackView *)_buildStackView { + UIStackView *stackView = [[UIStackView alloc] init]; + stackView.axis = UILayoutConstraintAxisHorizontal; + stackView.distribution = UIStackViewDistributionFillProportionally; + stackView.alignment = UIStackViewAlignmentCenter; + stackView.spacing = 20; + stackView.translatesAutoresizingMaskIntoConstraints = NO; + return stackView; +} + +- (void)_selectionButtonWasTapped { + self.selectionButton.selected = !self.selectionButton.selected; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/Stripe3DS2-Bridging-Header.h b/Stripe3DS2/Stripe3DS2/Stripe3DS2-Bridging-Header.h new file mode 100644 index 00000000..cfc1861e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/Stripe3DS2-Bridging-Header.h @@ -0,0 +1,12 @@ +// +// Stripe3DS2-Bridging-Header.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 4/10/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#ifndef Stripe3DS2_Bridging_Header_h +#define Stripe3DS2_Bridging_Header_h + +#endif /* Stripe3DS2_Bridging_Header_h */ diff --git a/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.h b/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.h new file mode 100644 index 00000000..badb4d6d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.h @@ -0,0 +1,21 @@ +// +// UIButton+CustomInitialization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSUICustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UIButton (CustomInitialization) + ++ (UIButton *)_stds_buttonWithTitle:(NSString * _Nullable)title customization:(STDSButtonCustomization * _Nullable)customization; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.m b/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.m new file mode 100644 index 00000000..9dc7e574 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIButton+CustomInitialization.m @@ -0,0 +1,69 @@ +// +// UIButton+CustomInitialization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIButton+CustomInitialization.h" +#import "STDSVisionSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation UIButton (CustomInitialization) + +#if !STP_TARGET_VISION +static const CGFloat kDefaultButtonContentInset = (CGFloat)12.0; +#endif + ++ (UIButton *)_stds_buttonWithTitle:(NSString * _Nullable)title customization:(STDSButtonCustomization * _Nullable)customization { + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + button.clipsToBounds = YES; +#if !STP_TARGET_VISION // UIButton edge insets not supported on visionOS + button.contentEdgeInsets = UIEdgeInsetsMake(kDefaultButtonContentInset, 0, kDefaultButtonContentInset, 0); +#endif + [[self class] _stds_configureButton:button withTitle:title customization:customization]; + + return button; +} + ++ (void)_stds_configureButton:(UIButton *)button withTitle:(NSString * _Nullable)buttonTitle customization:(STDSButtonCustomization * _Nullable)buttonCustomization { + button.backgroundColor = buttonCustomization.backgroundColor; + button.layer.cornerRadius = buttonCustomization.cornerRadius; + + UIFont *font = buttonCustomization.font; + UIColor *textColor = buttonCustomization.textColor; + + if (buttonTitle != nil) { + NSMutableDictionary *attributesDictionary = [NSMutableDictionary dictionary]; + + if (font != nil) { + attributesDictionary[NSFontAttributeName] = font; + } + + if (textColor != nil) { + attributesDictionary[NSForegroundColorAttributeName] = textColor; + } + switch (buttonCustomization.titleStyle) { + case STDSButtonTitleStyleDefault: + break; + case STDSButtonTitleStyleSentenceCapitalized: + buttonTitle = [buttonTitle localizedCapitalizedString]; + break; + case STDSButtonTitleStyleLowercase: + buttonTitle = [buttonTitle localizedLowercaseString]; + break; + case STDSButtonTitleStyleUppercase: + buttonTitle = [buttonTitle localizedUppercaseString]; + break; + } + + NSAttributedString *title = [[NSAttributedString alloc] initWithString:buttonTitle attributes:attributesDictionary]; + [button setAttributedTitle:title forState:UIControlStateNormal]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.h b/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.h new file mode 100644 index 00000000..3c45eb9c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.h @@ -0,0 +1,21 @@ +// +// UIColor+DefaultColors.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIColor (DefaultColors) + +/// The challenge view footer background color ++ (UIColor *)_stds_defaultFooterBackgroundColor; ++ (UIColor *)_stds_blueColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.m b/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.m new file mode 100644 index 00000000..8c33d519 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIColor+DefaultColors.m @@ -0,0 +1,24 @@ +// +// UIColor+DefaultColors.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIColor+DefaultColors.h" +#import "UIColor+ThirteenSupport.h" + +@implementation UIColor (DefaultColors) + ++ (UIColor *)_stds_defaultFooterBackgroundColor { + return [UIColor _stds_systemGray5Color]; +} + ++ (UIColor *)_stds_blueColor { + return [UIColor _stds_colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { + return (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) ? [UIColor colorWithRed:(CGFloat)(29.0 / 255.0) green:(CGFloat)(115.0 / 255.0) blue:(CGFloat)(250.0 / 255.0) alpha:1.0] : [UIColor colorWithRed:(CGFloat)(39.0 / 255.0) green:(CGFloat)(125.0 / 255.0) blue:(CGFloat)(255.0 / 255.0) alpha:1.0]; + }]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.h b/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.h new file mode 100644 index 00000000..fdccd761 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.h @@ -0,0 +1,24 @@ +// +// UIColor+ThirteenSupport.h +// Stripe3DS2 +// +// Created by David Estes on 8/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIColor (STDSThirteenSupport) + ++ (UIColor *)_stds_colorWithDynamicProvider:(UIColor * _Nonnull (^)(UITraitCollection *traitCollection))dynamicProvider; ++ (UIColor *)_stds_systemGray5Color; ++ (UIColor *)_stds_systemGray2Color; ++ (UIColor *)_stds_systemBackgroundColor; ++ (UIColor *)_stds_labelColor; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.m b/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.m new file mode 100644 index 00000000..ffff47be --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIColor+ThirteenSupport.m @@ -0,0 +1,33 @@ +// +// UIColor+ThirteenSupport.m +// Stripe3DS2 +// +// Created by David Estes on 8/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIColor+ThirteenSupport.h" + +@implementation UIColor (STDSThirteenSupport) + ++ (UIColor *)_stds_colorWithDynamicProvider:(UIColor * _Nonnull (^)(UITraitCollection *traitCollection))dynamicProvider { + return [UIColor colorWithDynamicProvider:dynamicProvider]; +} + ++ (UIColor *)_stds_systemGray5Color { + return [UIColor systemGray5Color]; +} + ++ (UIColor *)_stds_systemGray2Color { + return [UIColor systemGray2Color]; +} + ++ (UIColor *)_stds_systemBackgroundColor { + return [UIColor systemBackgroundColor]; +} + ++ (UIColor *)_stds_labelColor { + return [UIColor labelColor]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.h b/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.h new file mode 100644 index 00000000..ceb5bfa9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.h @@ -0,0 +1,23 @@ +// +// UIFont+DefaultFonts.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIFont (DefaultFonts) + ++ (UIFont *)_stds_defaultHeadingTextFont; + ++ (UIFont *)_stds_defaultLabelTextFontWithScale:(CGFloat)scale; ++ (UIFont *)_stds_defaultButtonTextFontWithScale:(CGFloat)scale; ++ (UIFont *)_stds_defaultBoldLabelTextFontWithScale:(CGFloat)scale; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.m b/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.m new file mode 100644 index 00000000..26e10ef7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIFont+DefaultFonts.m @@ -0,0 +1,41 @@ +// +// UIFont+DefaultFonts.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIFont+DefaultFonts.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation UIFont (DefaultFonts) + ++ (UIFont *)_stds_defaultHeadingTextFont { + UIFontDescriptor *fontDescriptor = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline] fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold]; + + return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize * (CGFloat)1.1]; +} + ++ (UIFont *)_stds_defaultLabelTextFontWithScale:(CGFloat)scale { + UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; + + return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize * scale]; +} + ++ (UIFont *)_stds_defaultBoldLabelTextFontWithScale:(CGFloat)scale { + UIFontDescriptor *fontDescriptor = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody] fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold]; + + return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize * scale]; +} + ++ (UIFont *)_stds_defaultButtonTextFontWithScale:(CGFloat)scale { + UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline]; + + return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize * scale]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.h b/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.h new file mode 100644 index 00000000..2c849dee --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.h @@ -0,0 +1,24 @@ +// +// UIView+LayoutSupport.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIView (LayoutSupport) + +/** + Pins the view to its superview's bounds. + */ +- (void)_stds_pinToSuperviewBounds; + +- (void)_stds_pinToSuperviewBoundsWithoutMargin; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.m b/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.m new file mode 100644 index 00000000..374088b0 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIView+LayoutSupport.m @@ -0,0 +1,35 @@ +// +// UIView+LayoutSupport.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIView+LayoutSupport.h" + +@implementation UIView (LayoutSupport) + +- (void)_stds_pinToSuperviewBounds { + self.translatesAutoresizingMaskIntoConstraints = false; + + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeTopMargin multiplier:1 constant:0]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeBottomMargin multiplier:1 constant:0]; + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeLeftMargin multiplier:1 constant:0]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeRightMargin multiplier:1 constant:0]; + + [NSLayoutConstraint activateConstraints:@[topConstraint, bottomConstraint, leftConstraint, rightConstraint]]; +} + +- (void)_stds_pinToSuperviewBoundsWithoutMargin { + self.translatesAutoresizingMaskIntoConstraints = false; + + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeTop multiplier:1 constant:0]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeLeft multiplier:1 constant:0]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeRight multiplier:1 constant:0]; + + [NSLayoutConstraint activateConstraints:@[topConstraint, bottomConstraint, leftConstraint, rightConstraint]]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.h b/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.h new file mode 100644 index 00000000..130a490f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.h @@ -0,0 +1,21 @@ +// +// UIViewController+Stripe3DS2.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 5/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class STDSUICustomization; + +@interface UIViewController (Stripe3DS2) + +- (void)_stds_setupNavigationBarElementsWithCustomization:(STDSUICustomization *)customization cancelButtonSelector:(SEL)cancelButtonSelector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.m b/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.m new file mode 100644 index 00000000..e681ba95 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/UIViewController+Stripe3DS2.m @@ -0,0 +1,49 @@ +// +// UIViewController+Stripe3DS2.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 5/6/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "UIViewController+Stripe3DS2.h" + +#import "UIButton+CustomInitialization.h" +#import "STDSUICustomization.h" + +@implementation UIViewController (Stripe3DS2) + +- (void)_stds_setupNavigationBarElementsWithCustomization:(STDSUICustomization *)customization cancelButtonSelector:(SEL)cancelButtonSelector { + STDSNavigationBarCustomization *navigationBarCustomization = customization.navigationBarCustomization; + + self.navigationController.navigationBar.barStyle = customization.navigationBarCustomization.barStyle; + + // Cancel button + STDSButtonCustomization *cancelButtonCustomization = [customization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeCancel]; + UIButton *cancelButton = [UIButton _stds_buttonWithTitle:navigationBarCustomization.buttonText customization:cancelButtonCustomization]; + // The cancel button's frame has a size of 0 in iOS 8 + cancelButton.frame = CGRectMake(0, 0, cancelButton.intrinsicContentSize.width, cancelButton.intrinsicContentSize.height); + cancelButton.accessibilityIdentifier = @"Cancel"; + [cancelButton addTarget:self action:cancelButtonSelector forControlEvents:UIControlEventTouchUpInside]; + UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:cancelButton]; + self.navigationItem.rightBarButtonItem = cancelBarButtonItem; + + // Title + self.title = navigationBarCustomization.headerText; + NSMutableDictionary *titleTextAttributes = [NSMutableDictionary dictionary]; + UIFont *headerFont = navigationBarCustomization.font; + if (headerFont) { + titleTextAttributes[NSFontAttributeName] = headerFont; + } + UIColor *headerColor = navigationBarCustomization.textColor; + if (headerColor) { + titleTextAttributes[NSForegroundColorAttributeName] = headerColor; + } + self.navigationController.navigationBar.titleTextAttributes = titleTextAttributes; + + // Color + self.navigationController.navigationBar.barTintColor = navigationBarCustomization.barTintColor; + self.navigationController.navigationBar.translucent = navigationBarCustomization.translucent; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.h b/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.h new file mode 100644 index 00000000..c39a85d8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.h @@ -0,0 +1,22 @@ +// +// STDSAlreadyInitializedException.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSAlreadyInitializedException` represents an exception that will be thrown in the `STDSThreeDS2Service` instance has already been initialized. + + @see STDSThreeDS2Service + */ +@interface STDSAlreadyInitializedException : STDSException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.m b/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.m new file mode 100644 index 00000000..af161983 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSAlreadyInitializedException.m @@ -0,0 +1,17 @@ +// +// STDSAlreadyInitializedException.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSAlreadyInitializedException.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSAlreadyInitializedException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.h b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.h new file mode 100644 index 00000000..766e4857 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.h @@ -0,0 +1,68 @@ +// +// STDSAuthenticationRequestParameters.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONEncodable.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSAuthenticationRequestParameters : NSObject + +/** + Designated initializer for `STDSAuthenticationRequestParameters`. + + @param sdkTransactionIdentifier The SDK Transaction Identifier, as created by `[STDSTransaction createTransaction]` + @param deviceData Optional device data collected by the SDK. + @param sdkEphemeralPublicKey The SDK ephemeral public key. + @param sdkAppIdentifier The SDK app identifier. + @param sdkReferenceNumber The SDK reference number. + @param messageVersion The protocol version that is supported by the SDK and used for the transaction. + + @exception InvalidInputException Thrown if an input parameter is invalid. @see InvalidInputException + */ +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)sdkTransactionIdentifier + deviceData:(nullable NSString *)deviceData + sdkEphemeralPublicKey:(NSString *)sdkEphemeralPublicKey + sdkAppIdentifier:(NSString *)sdkAppIdentifier + sdkReferenceNumber:(NSString *)sdkReferenceNumber + messageVersion:(NSString *)messageVersion; + +/** + The encrypted device data as a JWE string. + */ +@property (nonatomic, readonly, nullable) NSString *deviceData; + +/** + The SDK Transaction Identifier. + */ +@property (nonatomic, readonly) NSString *sdkTransactionIdentifier; + +/** + The SDK App Identifier. + */ +@property (nonatomic, readonly) NSString *sdkAppIdentifier; + +/** + The SDK reference number. + */ +@property (nonatomic, readonly) NSString *sdkReferenceNumber; + +/** + The SDK ephemeral public key. + */ +@property (nonatomic, readonly) NSString *sdkEphemeralPublicKey; + +/** + The protocol version that is used for the transaction. + */ +@property (nonatomic, readonly) NSString *messageVersion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.m b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.m new file mode 100644 index 00000000..dc6286b7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationRequestParameters.m @@ -0,0 +1,63 @@ +// +// STDSAuthenticationRequestParameters.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSAuthenticationRequestParameters.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSAuthenticationRequestParameters () + +@property (nonatomic, nullable, readonly) NSDictionary *sdkEphemeralPublicKeyJSON; + +@end + +@implementation STDSAuthenticationRequestParameters + +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)sdkTransactionIdentifier + deviceData:(nullable NSString *)deviceData + sdkEphemeralPublicKey:(NSString *)sdkEphemeralPublicKey + sdkAppIdentifier:(NSString *)sdkAppIdentifier + sdkReferenceNumber:(NSString *)sdkReferenceNumber + messageVersion:(NSString *)messageVersion { + self = [super init]; + if (self) { + _sdkTransactionIdentifier = [sdkTransactionIdentifier copy]; + _deviceData = [deviceData copy]; + _sdkEphemeralPublicKey = [sdkEphemeralPublicKey copy]; + _sdkAppIdentifier = [sdkAppIdentifier copy]; + _sdkReferenceNumber = [sdkReferenceNumber copy]; + _messageVersion = [messageVersion copy]; + } + return self; +} + +- (nullable NSDictionary *)sdkEphemeralPublicKeyJSON { + NSData *data = [self.sdkEphemeralPublicKey dataUsingEncoding:NSUTF8StringEncoding]; + if (data == nil) { + return nil; + } + + return [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; +} + +#pragma mark - STDSJSONEncodable + ++ (NSDictionary *)propertyNamesToJSONKeysMapping { + return @{ + NSStringFromSelector(@selector(sdkTransactionIdentifier)): @"sdkTransID", + NSStringFromSelector(@selector(deviceData)): @"sdkEncData", + NSStringFromSelector(@selector(sdkEphemeralPublicKeyJSON)): @"sdkEphemPubKey", + NSStringFromSelector(@selector(sdkAppIdentifier)): @"sdkAppID", + NSStringFromSelector(@selector(sdkReferenceNumber)): @"sdkReferenceNumber", + NSStringFromSelector(@selector(messageVersion)): @"messageVersion", + }; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationResponse.h b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationResponse.h new file mode 100644 index 00000000..d527f38d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSAuthenticationResponse.h @@ -0,0 +1,115 @@ +// +// STDSAuthenticationResponse.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The `STDSACSStatusType` enum defines the status of a transaction, as detailed in + 3DS2 Spec Seq 3.33: + */ +typedef NS_ENUM(NSInteger, STDSACSStatusType) { + /// The status is unknown or invalid + STDSACSStatusTypeUnknown = 0, + + /// Authenticated + STDSACSStatusTypeAuthenticated = 1, + + /// Requires a Cardholder challenge to complete authentication + STDSACSStatusTypeChallengeRequired = 2, + + /// Requires a Cardholder challenge using Decoupled Authentication + STDSACSStatusTypeDecoupledAuthentication = 3, + + /// Not authenticated + STDSACSStatusTypeNotAuthenticated = 4, + + /// Not authenticated, but a proof of authentication attempt (Authentication Value) + /// was generated + STDSACSStatusTypeProofGenerated = 5, + + /// Not authenticated, as authentication could not be performed due to technical or + /// other issue + STDSACSStatusTypeError = 6, + + /// Not authenticated because the Issuer is rejecting authentication and requesting + /// that authorisation not be attempted + STDSACSStatusTypeRejected = 7, + + /// Authentication not requested by the 3DS Server for data sent for informational + /// purposes only + STDSACSStatusTypeInformationalOnly = 8, +}; + +/** + A native protocol representing the response sent by the 3DS Server. + Only parameters relevant to performing 3DS2 authentication in the mobile SDK are exposed. + */ +@protocol STDSAuthenticationResponse + +/// Universally unique transaction identifier assigned by the 3DS Server to identify a single transaction. +@property (nonatomic, readonly) NSString *threeDSServerTransactionID; + +/// Transaction status +@property (nonatomic, readonly) STDSACSStatusType status; + +/// Indication of whether a challenge is required. +@property (nonatomic, readonly, getter=isChallengeRequired) BOOL challengeRequired; + +/// Indicates whether the ACS confirms utilisation of Decoupled Authentication and agrees to utilise Decoupled Authentication to authenticate the Cardholder. +@property (nonatomic, readonly) BOOL willUseDecoupledAuthentication; + +/** + DS assigned ACS identifier. + Each DS can provide a unique ID to each ACS on an individual basis. + */ +@property (nonatomic, readonly, nullable) NSString *acsOperatorID; + +/// Unique identifier assigned by the EMVCo Secretariat upon Testing and Approval. +@property (nonatomic, readonly, nullable) NSString *acsReferenceNumber; + +/// Contains the JWS object (represented as a string) created by the ACS for the ARes message. +@property (nonatomic, readonly, nullable) NSString *acsSignedContent; + +/// Universally Unique transaction identifier assigned by the ACS to identify a single transaction. +@property (nonatomic, readonly) NSString *acsTransactionID; + +/// Fully qualified URL of the ACS to be used for the challenge. +@property (nonatomic, readonly, nullable) NSURL *acsURL; + +/** + Text provided by the ACS/Issuer to Cardholder during a Frictionless or Decoupled transaction. The Issuer can provide information to Cardholder. + For example, “Additional authentication is needed for this transaction, please contact (Issuer Name) at xxx-xxx-xxxx.” + */ +@property (nonatomic, readonly, nullable) NSString *cardholderInfo; + +/// EMVCo-assigned unique identifier to track approved DS. +@property (nonatomic, readonly, nullable) NSString *directoryServerReferenceNumber; + +/// Universally unique transaction identifier assigned by the DS to identify a single transaction. +@property (nonatomic, readonly, nullable) NSString *directoryServerTransactionID; + +/** + Protocol version identifier This shall be the Protocol Version Number of the specification utilised by the system creating this message. + The Message Version Number is set by the 3DS Server which originates the protocol with the AReq message. + The Message Version Number does not change during a 3DS transaction. + */ +@property (nonatomic, readonly) NSString *protocolVersion; + +/// Universally unique transaction identifier assigned by the 3DS SDK to identify a single transaction. +@property (nonatomic, readonly) NSString *sdkTransactionID; + +@end + +/// A utility to parse an STDSAuthenticationResponse from JSON +id _Nullable STDSAuthenticationResponseFromJSON(NSDictionary *json); + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.h new file mode 100644 index 00000000..dd63f982 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.h @@ -0,0 +1,83 @@ +// +// STDSButtonCustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// +#import +#import + +#import "STDSCustomization.h" + +/// An enum that defines the different types of buttons that are able to be customized. +typedef NS_ENUM(NSInteger, STDSUICustomizationButtonType) { + + /// The submit button type. + STDSUICustomizationButtonTypeSubmit = 0, + + /// The continue button type. + STDSUICustomizationButtonTypeContinue = 1, + + /// The next button type. + STDSUICustomizationButtonTypeNext = 2, + + /// The cancel button type. + STDSUICustomizationButtonTypeCancel = 3, + + /// The resend button type. + STDSUICustomizationButtonTypeResend = 4, +}; + +/// An enumeration of the case transformations that can be applied to the button's title +typedef NS_ENUM(NSInteger, STDSButtonTitleStyle) { + /// Default style, doesn't modify the title + STDSButtonTitleStyleDefault, + + /// Applies localizedUppercaseString to the title + STDSButtonTitleStyleUppercase, + + /// Applies localizedLowercaseString to the title + STDSButtonTitleStyleLowercase, + + /// Applies localizedCapitalizedString to the title + STDSButtonTitleStyleSentenceCapitalized, +}; + +NS_ASSUME_NONNULL_BEGIN + +/// A customization object to use to configure the UI of a button. +@interface STDSButtonCustomization: STDSCustomization + +/// The default settings for the provided button type. ++ (instancetype)defaultSettingsForButtonType:(STDSUICustomizationButtonType)type; + +/** + Initializes an instance of STDSButtonCustomization with the given backgroundColor and colorRadius. + */ +- (instancetype)initWithBackgroundColor:(UIColor *)backgroundColor cornerRadius:(CGFloat)cornerRadius; + +/** + This is unavailable because there are no sensible default property values without a button type. + Use `defaultSettingsForButtonType:` or `initWithBackgroundColor:cornerRadius:` instead. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + The background color of the button. + The default for .resend and .cancel is clear. + The default for .submit, .continue, and .next is blue. + */ +@property (nonatomic) UIColor *backgroundColor; + +/// The corner radius of the button. Defaults to 8. +@property (nonatomic) CGFloat cornerRadius; + +/** + The capitalization style of the button title + */ +@property (nonatomic) STDSButtonTitleStyle titleStyle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.m new file mode 100644 index 00000000..a82a6cbe --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSButtonCustomization.m @@ -0,0 +1,69 @@ +// +// STDSButtonCustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSButtonCustomization.h" + +#import "STDSUICustomization.h" +#import "UIColor+DefaultColors.h" +#import "UIFont+DefaultFonts.h" + +static const CGFloat kDefaultButtonCornerRadius = 8.0; +static const CGFloat kDefaultButtonFontScale = (CGFloat)0.9; + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSButtonCustomization + ++ (instancetype)defaultSettingsForButtonType:(STDSUICustomizationButtonType)type { + UIColor *backgroundColor = [UIColor _stds_blueColor]; + CGFloat cornerRadius = kDefaultButtonCornerRadius; + UIFont *font = [UIFont _stds_defaultBoldLabelTextFontWithScale:kDefaultButtonFontScale]; + UIColor *textColor = UIColor.whiteColor; + switch (type) { + case STDSUICustomizationButtonTypeContinue: + case STDSUICustomizationButtonTypeSubmit: + case STDSUICustomizationButtonTypeNext: + break; + case STDSUICustomizationButtonTypeResend: + backgroundColor = UIColor.clearColor; + textColor = [UIColor _stds_blueColor]; + font = nil; + break; + case STDSUICustomizationButtonTypeCancel: + backgroundColor = UIColor.clearColor; + textColor = nil; + font = nil; + break; + } + STDSButtonCustomization *buttonCustomization = [[self alloc] initWithBackgroundColor:backgroundColor cornerRadius:cornerRadius]; + buttonCustomization.font = font; + buttonCustomization.textColor = textColor; + return buttonCustomization; +} + +- (instancetype)initWithBackgroundColor:(UIColor *)backgroundColor cornerRadius:(CGFloat)cornerRadius { + self = [super init]; + if (self) { + _backgroundColor = backgroundColor; + _cornerRadius = cornerRadius; + } + return self; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + STDSButtonCustomization *copy = [super copyWithZone:zone]; + copy.backgroundColor = self.backgroundColor; + copy.cornerRadius = self.cornerRadius; + copy.titleStyle = self.titleStyle; + + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.h b/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.h new file mode 100644 index 00000000..e1aa151c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.h @@ -0,0 +1,61 @@ +// +// STDSChallengeParameters.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@protocol STDSAuthenticationResponse; + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSChallengeParameters` contains information from the 3DS Server's + authentication response that are used by the 3DS2 SDK to initiate + the challenge flow. + */ +@interface STDSChallengeParameters : NSObject + +/** + Convenience intiializer to create an instace of `STDSChallengeParameters` from an + `STDSAuthenticationResponse` + */ +- (instancetype)initWithAuthenticationResponse:(id)authResponse; + +/** + Transaction identifier assigned by the 3DS Server to uniquely identify + a transaction. + */ +@property (nonatomic, copy) NSString *threeDSServerTransactionID; + +/** + Transaction identifier assigned by the Access Control Server (ACS) + to uniquely identify a transaction. + */ +@property (nonatomic, copy) NSString *acsTransactionID; + +/** + The reference number of the relevant Access Control Server. + */ +@property (nonatomic, copy) NSString *acsReferenceNumber; + +/** + The encrypted message sent by the Access Control Server + containing the ACS URL, epthemeral public key, and the + 3DS2 SDK ephemeral public key. + */ +@property (nonatomic, copy) NSString *acsSignedContent; + +/** + The URL for the application that is requesting 3DS2 verification. + This property can be optionally set and will be included with the + messages sent to the Directory Server during the challenge flow. + */ +@property (nonatomic, copy, nullable) NSString *threeDSRequestorAppURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.m b/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.m new file mode 100644 index 00000000..ac166cd7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSChallengeParameters.m @@ -0,0 +1,31 @@ +// +// STDSChallengeParameters.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeParameters.h" + +#import "STDSAuthenticationResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSChallengeParameters + +- (instancetype)initWithAuthenticationResponse:(id)authResponse { + self = [self init]; + if (self) { + _threeDSServerTransactionID = [authResponse.threeDSServerTransactionID copy]; + _acsTransactionID = [authResponse.acsTransactionID copy]; + _acsReferenceNumber = [authResponse.acsReferenceNumber copy]; + _acsSignedContent = [authResponse.acsSignedContent copy]; + } + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSChallengeStatusReceiver.h b/Stripe3DS2/Stripe3DS2/include/STDSChallengeStatusReceiver.h new file mode 100644 index 00000000..a352e0c4 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSChallengeStatusReceiver.h @@ -0,0 +1,67 @@ +// +// STDSChallengeStatusReceiver.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +@class STDSTransaction, STDSCompletionEvent, STDSRuntimeErrorEvent, STDSProtocolErrorEvent; + +NS_ASSUME_NONNULL_BEGIN + +/** + Implement the `STDSChallengeStatusReceiver` protocol to receive challenge status notifications at the end of the challenge process. + @see `STDSTransaction.doChallenge` + */ +@protocol STDSChallengeStatusReceiver + +/** + Called when the challenge process is completed. + + @param completionEvent Information about the completion of the challenge process. @see `STDSCompletionEvent` + */ +- (void)transaction:(STDSTransaction *)transaction didCompleteChallengeWithCompletionEvent:(STDSCompletionEvent *)completionEvent; + +/** + Called when the user selects the option to cancel the transaction on the challenge screen. + */ +- (void)transactionDidCancel:(STDSTransaction *)transaction; + +/** + Called when the challenge process reaches or exceeds the timeout interval that was passed to `STDSTransaction.doChallenge` + */ +- (void)transactionDidTimeOut:(STDSTransaction *)transaction; + +/** + Called when the 3DS SDK receives an EMV 3-D Secure protocol-defined error message from the ACS. + + @param protocolErrorEvent The error code and details. @see `STDSProtocolErrorEvent` + */ +- (void)transaction:(STDSTransaction *)transaction didErrorWithProtocolErrorEvent:(STDSProtocolErrorEvent *)protocolErrorEvent; + +/** + Called when the 3DS SDK encounters errors during the challenge process. These errors include all errors except those covered by `didErrorWithProtocolErrorEvent`. + + @param runtimeErrorEvent The error code and details. @see `STDSRuntimeErrorEvent` + */ +- (void)transaction:(STDSTransaction *)transaction didErrorWithRuntimeErrorEvent:(STDSRuntimeErrorEvent *)runtimeErrorEvent; + +@optional + +/** + Optional method that will be called when the transaction displays a new challenge screen. + */ +- (void)transactionDidPresentChallengeScreen:(STDSTransaction *)transaction; + +/** + Optional method for custom dismissal of the challenge view controller. Meant only for internal use by Stripe SDK. + */ +- (void)dismissChallengeViewController:(UIViewController *)challengeViewController forTransaction:(STDSTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.h b/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.h new file mode 100644 index 00000000..851c71db --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.h @@ -0,0 +1,40 @@ +// +// STDSCompletionEvent.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSCompletionEvent` contains information about completion of the challenge process. + */ +@interface STDSCompletionEvent : NSObject + +/** + Designated initializer for `STDSCompletionEvent`. + */ +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)identifier transactionStatus:(NSString *)transactionStatus NS_DESIGNATED_INITIALIZER; + +/** + `STDSCompletionEvent` should not be directly initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + The SDK Transaction ID. + */ +@property (nonatomic, readonly) NSString *sdkTransactionIdentifier; + +/** + The transaction status that was received in the final challenge response. + */ +@property (nonatomic, readonly) NSString *transactionStatus; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.m b/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.m new file mode 100644 index 00000000..6ee3c43d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSCompletionEvent.m @@ -0,0 +1,26 @@ +// +// STDSCompletionEvent.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSCompletionEvent.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSCompletionEvent + +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)identifier transactionStatus:(NSString *)transactionStatus { + self = [super init]; + if (self) { + _sdkTransactionIdentifier = [identifier copy]; + _transactionStatus = [transactionStatus copy]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.h b/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.h new file mode 100644 index 00000000..4d77ba5d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.h @@ -0,0 +1,96 @@ +// +// STDSConfigParameters.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The default group name that will be used to group additional + configuration parameters. + */ +extern NSString * const kSTDSConfigDefaultGroupName; + +/** + `STDSConfigParameters` represents additional configuration parameters + that can be passed to the Stripe3DS2 SDK during initialization. + + There are currently no supported additional parameters and apps can + just pass `[STDSConfigParameters alloc] initWithStandardParameters` + to the `STDSThreeDS2Service` instance. + */ +@interface STDSConfigParameters : NSObject + +/** + Convenience initializer to get an `STDSConfigParameters` instance + with the default expected configuration parameters. + */ +- (instancetype)initWithStandardParameters; + +/** + Adds the parameter to this instance. + + @param paramName The name of the parameter to add + @param paramValue The value of the parameter to add + @param paramGroup The group to which this parameter will be added. If `nil` the parameter will be added to `kSTDSConfigDefaultGroupName` + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` or `paramValue` are `nil`. @see STDSInvalidInputException + */ +- (void)addParameterNamed:(NSString *)paramName withValue:(NSString *)paramValue toGroup:(nullable NSString *)paramGroup; + +/** + Adds the parameter to the default group in this instance. + + @param paramName The name of the parameter to add + @param paramValue The value of the parameter to add + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` or `paramValue` are `nil`. @see STDSInvalidInputException + */ +- (void)addParameterNamed:(NSString *)paramName withValue:(NSString *)paramValue; + +/** + Returns the value for `paramName` in `paramGroup` or `nil` if the parameter value is not set. + + @param paramName The name of the parameter to return + @param paramGroup The group from which to fetch the parameter value. If `nil` will default to `kSTDSConfigDefaultGroupName` + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` is `nil`. @see STDSInvalidInputException + */ +- (nullable NSString *)parameterValue:(NSString *)paramName inGroup:(nullable NSString *)paramGroup; + +/** + Returns the value for `paramName` in the default group or `nil` if the parameter value is not set. + + @param paramName The name of the parameter to return + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` is `nil`. @see STDSInvalidInputException + */ +- (nullable NSString *)parameterValue:(NSString *)paramName; + +/** + Removes the specified parameter from the group and returns the value or `nil` if the parameter was not found. + + @param paramName The name of the parameter to remove + @param paramGroup The group from which to remove this parameter. If `nil` will default to `kSTDSConfigDefaultGroupName` + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` is `nil`. @see STDSInvalidInputException + */ +- (nullable NSString *)removeParameterNamed:(NSString *)paramName fromGroup:(nullable NSString *)paramGroup; + +/** + Removes the specified parameter from the default group and returns the value or `nil` if the parameter was not found. + + @param paramName The name of the parameter to remove + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `paramName` is `nil`. @see STDSInvalidInputException + */ +- (nullable NSString *)removeParameterNamed:(NSString *)paramName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.m b/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.m new file mode 100644 index 00000000..e334a5bb --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSConfigParameters.m @@ -0,0 +1,113 @@ +// +// STDSConfigParameters.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSConfigParameters.h" + +#import "STDSException+Internal.h" +#import "STDSInvalidInputException.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString * const kSTDSConfigDefaultGroupName = @"STDSConfigParameters.group.default"; + +@implementation STDSConfigParameters +{ + NSMutableDictionary *_parameters; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _parameters = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +- (instancetype)initWithStandardParameters { + self = [self init]; + if (self) { + // Nothing for now because we don't have any standard parameters + } + + return self; +} + +- (void)addParameterNamed:(NSString *)paramName withValue:(NSString *)paramValue { + [self _addParameterNamed:paramName withValue:paramValue toGroup:kSTDSConfigDefaultGroupName]; +} + +- (void)addParameterNamed:(NSString *)paramName withValue:(NSString *)paramValue toGroup:(nullable NSString *)paramGroup { + [self _addParameterNamed:paramName withValue:paramValue toGroup:paramGroup ?: kSTDSConfigDefaultGroupName]; +} + +- (void)_addParameterNamed:(NSString *)paramName withValue:(NSString *)paramValue toGroup:(NSString *)paramGroup { + if (paramName == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramName passed to instance %@", self]; + } else if (paramValue == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramValue passed to instance %@", self]; + } else if (paramGroup == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramGroup passed to instance %@", self]; + } + + NSMutableDictionary *groupParameters = _parameters[paramGroup]; + if (groupParameters == nil) { + groupParameters = [[NSMutableDictionary alloc] init]; + _parameters[paramGroup] = groupParameters; + } + + if (groupParameters[paramName] != nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"Cannot override value of %@ for parameter %@ in group %@ with value %@", groupParameters[paramName], paramName, paramGroup, paramValue]; + } + + groupParameters[paramName] = paramValue; +} + +- (nullable NSString *)parameterValue:(NSString *)paramName { + return [self _parameterValue:paramName inGroup:kSTDSConfigDefaultGroupName]; +} + +- (nullable NSString *)parameterValue:(NSString *)paramName inGroup:(nullable NSString *)paramGroup { + return [self _parameterValue:paramName inGroup:paramGroup ?: kSTDSConfigDefaultGroupName]; +} + +- (nullable NSString *)_parameterValue:(NSString *)paramName inGroup:(NSString *)paramGroup { + if (paramName == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramName passed to instance %@", self]; + } else if (paramGroup == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramGroup passed to instance %@", self]; + } + + NSMutableDictionary *groupParameters = _parameters[paramGroup]; + return groupParameters[paramName]; +} + +- (nullable NSString *)removeParameterNamed:(NSString *)paramName { + return [self _removeParameterNamed:paramName fromGroup:kSTDSConfigDefaultGroupName]; +} + +- (nullable NSString *)removeParameterNamed:(NSString *)paramName fromGroup:(nullable NSString *)paramGroup { + return [self _removeParameterNamed:paramName fromGroup:paramGroup ?: kSTDSConfigDefaultGroupName]; +} + +- (nullable NSString *)_removeParameterNamed:(NSString *)paramName fromGroup:(NSString *)paramGroup { + if (paramName == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramName passed to instance %@", self]; + } else if (paramGroup == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"nil paramGroup passed to instance %@", self]; + } + + NSMutableDictionary *groupParameters = _parameters[paramGroup]; + NSString *paramValue = groupParameters[paramName]; + [groupParameters removeObjectForKey:paramName]; + return paramValue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSCustomization.h new file mode 100644 index 00000000..516eff9e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSCustomization.h @@ -0,0 +1,25 @@ +// +// STDSCustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// This class provides a common set of customization parameters, used to customize elements of the UI. +@interface STDSCustomization : NSObject + +/// The font to use for text. +@property (nonatomic, nullable) UIFont *font; + +/// The color to use for the text. +@property (nonatomic, nullable) UIColor *textColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSCustomization.m new file mode 100644 index 00000000..4544024c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSCustomization.m @@ -0,0 +1,25 @@ +// +// STDSCustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSCustomization + +- (id)copyWithZone:(nullable NSZone *)zone { + STDSCustomization *copy = [[[self class] allocWithZone:zone] init]; + copy.font = self.font; + copy.textColor = self.textColor; + + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.h b/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.h new file mode 100644 index 00000000..29c6c5a6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.h @@ -0,0 +1,103 @@ +// +// STDSErrorMessage.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONEncodable.h" +#import "STDSJSONDecodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Error codes as defined by the 3DS2 spec. +typedef NS_ENUM(NSInteger, STDSErrorMessageCode) { + /// The SDK received a message that is not an ARes, CRes, or ErrorMessage. + STDSErrorMessageCodeInvalidMessage = 101, + + /// A required data element is missing from the network response. + STDSErrorMessageCodeRequiredDataElementMissing = 201, + + // Critical message extension not recognised + STDSErrorMessageCodeUnrecognizedCriticalMessageExtension = 202, + + /// A data element is not in the required format or the value is invalid. + STDSErrorMessageErrorInvalidDataElement = 203, + + // Transaction ID not recognized + STDSErrorMessageErrorTransactionIDNotRecognized = 301, + + /// A network response could not be decrypted or verified. + STDSErrorMessageErrorDataDecryptionFailure = 302, + + /// The SDK timed out + STDSErrorMessageErrorTimeout = 402, +}; + +/** + `STDSErrorMessage` represents an error message that is returned by the ACS or to be sent to the ACS. + */ +@interface STDSErrorMessage : NSObject + +/** + Designated initializer for `STDSErrorMessage`. + + @param errorCode The error code. + @param errorComponent The component that identified the error. + @param errorDescription Text describing the error. + @param errorDetails Additional error details. Optional. + */ +- (instancetype)initWithErrorCode:(NSString *)errorCode + errorComponent:(NSString *)errorComponent + errorDescription:(NSString *)errorDescription + errorDetails:(nullable NSString *)errorDetails + messageVersion:(NSString *)messageVersion + acsTransactionIdentifier:(nullable NSString *)acsTransactionIdentifier + errorMessageType:(NSString *)errorMessageType; + +/** + The error code. + */ +@property (nonatomic, readonly) NSString *errorCode; + +/** + The 3-D Secure component that identified the error. + */ +@property (nonatomic, readonly) NSString *errorComponent; + +/** + Text describing the error. + */ +@property (nonatomic, readonly) NSString *errorDescription; + +/** + Additional error details. + */ +@property (nonatomic, nullable, readonly) NSString *errorDetails; + +/** + The protocol version identifier. + */ +@property (nonatomic, readonly) NSString *messageVersion; + +/** + The ACS transaction identifier. + */ +@property (nonatomic, readonly, nullable) NSString *acsTransactionIdentifier; + +/** + The message type that was identified as erroneous. + */ +@property (nonatomic, readonly, nullable) NSString *errorMessageType; + +/** + A representation of the `STDSErrorMessage` as an `NSError` + */ +- (NSError *)NSErrorValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.m b/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.m new file mode 100644 index 00000000..1e80b645 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSErrorMessage.m @@ -0,0 +1,94 @@ +// +// STDSErrorMessage.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSErrorMessage.h" + +#import "NSDictionary+DecodingHelpers.h" +#import "STDSJSONEncoder.h" +#import "STDSStripe3DS2Error.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSErrorMessage + +- (instancetype)initWithErrorCode:(NSString *)errorCode + errorComponent:(NSString *)errorComponent + errorDescription:(NSString *)errorDescription + errorDetails:(nullable NSString *)errorDetails + messageVersion:(NSString *)messageVersion + acsTransactionIdentifier:(nullable NSString *)acsTransactionIdentifier + errorMessageType:(NSString *)errorMessageType { + self = [super init]; + if (self) { + _errorCode = [errorCode copy]; + _errorComponent = [errorComponent copy]; + _errorDescription = [errorDescription copy]; + _errorDetails = [errorDetails copy]; + _messageVersion = [messageVersion copy]; + _acsTransactionIdentifier = [acsTransactionIdentifier copy]; + _errorMessageType = [errorMessageType copy]; + } + return self; +} + +- (NSString *)messageType { + return @"Erro"; +} + +- (NSError *)NSErrorValue { + return [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:[self.errorCode integerValue] + userInfo: [STDSJSONEncoder dictionaryForObject:self]]; +} + +#pragma mark - STDSJSONEncodable + ++ (NSDictionary *)propertyNamesToJSONKeysMapping { + return @{ + NSStringFromSelector(@selector(errorCode)): @"errorCode", + NSStringFromSelector(@selector(errorComponent)): @"errorComponent", + NSStringFromSelector(@selector(errorDescription)): @"errorDescription", + NSStringFromSelector(@selector(errorDetails)): @"errorDetail", + NSStringFromSelector(@selector(messageType)): @"messageType", + NSStringFromSelector(@selector(messageVersion)): @"messageVersion", + NSStringFromSelector(@selector(acsTransactionIdentifier)): @"acsTransID", + NSStringFromSelector(@selector(errorMessageType)): @"errorMessageType", + }; +} + +#pragma mark - STDSJSONDecodable + ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError * _Nullable __autoreleasing * _Nullable)outError { + if (json == nil) { + return nil; + } + NSError *error; + + // Required + NSString *errorCode = [json _stds_stringForKey:@"errorCode" required:YES error:&error]; + NSString *errorComponent = [json _stds_stringForKey:@"errorComponent" required:YES error:&error]; + NSString *errorDescription = [json _stds_stringForKey:@"errorDescription" required:YES error:&error]; + NSString *errorDetail = [json _stds_stringForKey:@"errorDetail" required:YES error:&error]; + NSString *messageVersion = [json _stds_stringForKey:@"messageVersion" required:YES error:&error]; + + // Optional + NSString *errorMessageType = [json _stds_stringForKey:@"errorMessageType" required:NO error:&error]; + NSString *acsTransactionIdentifier = [json _stds_stringForKey:@"acsTransID" required:NO error:nil]; + + if (error) { + if (outError) { + *outError = error; + } + return nil; + } + return [[self alloc] initWithErrorCode:errorCode errorComponent:errorComponent errorDescription:errorDescription errorDetails:errorDetail messageVersion:messageVersion acsTransactionIdentifier:acsTransactionIdentifier errorMessageType:errorMessageType]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSException.h b/Stripe3DS2/Stripe3DS2/include/STDSException.h new file mode 100644 index 00000000..1f2c5ecd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSException.h @@ -0,0 +1,25 @@ +// +// STDSException.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An abstract class to represent 3DS2 SDK custom exceptions + */ +@interface STDSException : NSException + +/** + A description of the exception. + */ +@property (nonatomic, readonly) NSString *message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSException.m b/Stripe3DS2/Stripe3DS2/include/STDSException.m new file mode 100644 index 00000000..673d5bbd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSException.m @@ -0,0 +1,27 @@ +// +// STDSException.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSException + ++ (instancetype)exceptionWithMessage:(NSString *)format, ... { + va_list args; + va_start(args, format); + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + STDSException *exception = [[[self class] alloc] initWithName:NSStringFromClass([self class]) reason:message userInfo:nil]; + exception->_message = [message copy]; + return exception; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.h new file mode 100644 index 00000000..990ffd9b --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.h @@ -0,0 +1,41 @@ +// +// STDSFooterCustomization.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/10/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The Challenge view displays a footer with additional details that + expand when tapped. This object configures the appearance of that view. +*/ +@interface STDSFooterCustomization : STDSCustomization + +/// The default settings. ++ (instancetype)defaultSettings; + +/** + The background color of the footer. + Defaults to gray. + */ +@property (nonatomic) UIColor *backgroundColor; + +/// The color of the chevron. Defaults to a dark gray. +@property (nonatomic) UIColor *chevronColor; + +/// The color of the heading text. Defaults to black. +@property (nonatomic) UIColor *headingTextColor; + +/// The font to use for the heading text. +@property (nonatomic) UIFont *headingFont; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.m new file mode 100644 index 00000000..6c770fda --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSFooterCustomization.m @@ -0,0 +1,45 @@ +// +// STDSFooterCustomization.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/10/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSFooterCustomization.h" + +#import "UIFont+DefaultFonts.h" +#import "UIColor+DefaultColors.h" +#import "UIColor+ThirteenSupport.h" + +@implementation STDSFooterCustomization + ++ (instancetype)defaultSettings { + return [self new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.textColor = UIColor._stds_labelColor; + _headingTextColor = UIColor._stds_labelColor; + _backgroundColor = [UIColor _stds_defaultFooterBackgroundColor]; + _chevronColor = [UIColor _stds_systemGray2Color]; + self.font = [UIFont _stds_defaultLabelTextFontWithScale:(CGFloat)0.9]; + _headingFont = [UIFont _stds_defaultLabelTextFontWithScale:(CGFloat)0.9]; + } + return self; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + STDSFooterCustomization *copy = [super copyWithZone:zone]; + copy.headingTextColor = self.headingTextColor; + copy.headingFont = self.headingFont; + copy.backgroundColor = self.backgroundColor; + copy.chevronColor = self.chevronColor; + + return copy; +} + + +@end diff --git a/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.h b/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.h new file mode 100644 index 00000000..b30d4e10 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.h @@ -0,0 +1,21 @@ +// +// STDSInvalidInputException.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSInvalidInputException` represents an exception that will be thrown by + Stripe3DS2 SDK methods that are called with invalid input arguments. + */ +@interface STDSInvalidInputException : STDSException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.m b/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.m new file mode 100644 index 00000000..7ff4cd3e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSInvalidInputException.m @@ -0,0 +1,17 @@ +// +// STDSInvalidInputException.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSInvalidInputException.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSInvalidInputException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSJSONDecodable.h b/Stripe3DS2/Stripe3DS2/include/STDSJSONDecodable.h new file mode 100644 index 00000000..f93428ac --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSJSONDecodable.h @@ -0,0 +1,33 @@ +// +// STDSJSONDecodable.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol STDSJSONDecodable + +/** + Initializes an instance of the class from its JSON representation. + + This method recognizes two categories of errors: + - a required field is missing. + - a required field value is in valid (e.g. expected 'Y' or 'N' but received 'X'). + + Errors populating optional fields are ignored. + + @param json The JSON dictionary that represents an object of this type + @param outError If there was a missing required field or invalid field value, contains an instance of NSError. + + @return The object represented by the JSON dictionary. If the object could not be decoded, returns nil and populates the outError argument. + */ ++ (nullable instancetype)decodedObjectFromJSON:(nullable NSDictionary *)json error:(NSError **)outError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSJSONEncodable.h b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncodable.h new file mode 100644 index 00000000..9861a23c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncodable.h @@ -0,0 +1,22 @@ +// +// STDSJSONEncodable.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol STDSJSONEncodable + +/** + Returns a map of property names to their JSON representation's key value. For example, `STDSChallengeParameters` has a property called `acsTransactionID`, but the 3DS2 JSON spec expects a field called `acsTransID`. This dictionary represents a mapping from the former to the latter (in other words, [STDSChallengeParameters propertyNamesToJSONKeysMapping][@"acsTransactionID"] == @"acsTransID".) + */ ++ (NSDictionary *)propertyNamesToJSONKeysMapping; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.h b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.h new file mode 100644 index 00000000..7ccc07e6 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.h @@ -0,0 +1,27 @@ +// +// STDSJSONEncoder.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONEncodable.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSJSONEncoder` is a utility class to help with converting API objects into JSON + */ +@interface STDSJSONEncoder : NSObject + +/** + Method to convert an STDSJSONEncodable object into a JSON dictionary. + */ ++ (NSDictionary *)dictionaryForObject:(NSObject *)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.m b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.m new file mode 100644 index 00000000..4c55b02c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSJSONEncoder.m @@ -0,0 +1,51 @@ +// +// STDSJSONEncoder.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSJSONEncoder.h" + +@implementation STDSJSONEncoder + ++ (NSDictionary *)dictionaryForObject:(nonnull NSObject *)object { + NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary]; + [[object.class propertyNamesToJSONKeysMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull keyName, __unused BOOL * _Nonnull stop) { + id value = [self jsonEncodableValueForObject:[object valueForKey:propertyName]]; + if (value != [NSNull null]) { + keyPairs[keyName] = value; + } + }]; + return [keyPairs copy]; +} + ++ (id)jsonEncodableValueForObject:(NSObject *)object { + if ([object conformsToProtocol:@protocol(STDSJSONEncodable)]) { + return [self dictionaryForObject:(NSObject*)object]; + } else if ([object isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)object; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:dict.count]; + + [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, __unused BOOL * _Nonnull stop) { + result[key] = [self jsonEncodableValueForObject:value]; + }]; + + return result; + } else if ([object isKindOfClass:[NSArray class]]) { + NSArray *array = (NSArray *)object; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; + + for (NSObject *element in array) { + [result addObject:[self jsonEncodableValueForObject:element]]; + } + return result; + } else if ([object isKindOfClass:[NSString class]] || [object isKindOfClass:[NSNumber class]]) { + return object; + } else { + return [NSNull null]; + } +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.h new file mode 100644 index 00000000..af75cf39 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.h @@ -0,0 +1,31 @@ +// +// STDSLabelCustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A customization object to use to configure the UI of a text label. + + The font and textColor inherited from `STDSCustomization` configure non-heading labels. + */ +@interface STDSLabelCustomization : STDSCustomization + +/// The default settings. ++ (instancetype)defaultSettings; + +/// The color of the heading text. Defaults to black. +@property (nonatomic) UIColor *headingTextColor; + +/// The font to use for the heading text. +@property (nonatomic) UIFont *headingFont; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.m new file mode 100644 index 00000000..bb57c5f8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSLabelCustomization.m @@ -0,0 +1,43 @@ +// +// STDSLabelCustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSLabelCustomization.h" + +#import "UIFont+DefaultFonts.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSLabelCustomization + ++ (instancetype)defaultSettings { + return [self new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.textColor = UIColor._stds_labelColor; + _headingTextColor = UIColor._stds_labelColor; + self.font = [UIFont _stds_defaultLabelTextFontWithScale:(CGFloat)0.9]; + _headingFont = [UIFont _stds_defaultHeadingTextFont]; + } + return self; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + STDSLabelCustomization *copy = [super copyWithZone:zone]; + copy.headingTextColor = self.headingTextColor; + copy.headingFont = self.headingFont; + + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.h new file mode 100644 index 00000000..18affaaf --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.h @@ -0,0 +1,59 @@ +// +// STDSNavigationBarCustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +#import "STDSCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A customization object to use to configure a UINavigationBar. + + The font and textColor inherited from `STDSCustomization` configure the + title of the navigation bar, and default to nil. + */ +@interface STDSNavigationBarCustomization : STDSCustomization + +/// The default settings. ++ (instancetype)defaultSettings; + +/** + The tint color of the navigation bar background. + Defaults to nil. + */ +@property (nonatomic, nullable) UIColor *barTintColor; + +/** + The navigation bar style. + Defaults to UIBarStyleDefault. + */ +@property (nonatomic) UIBarStyle barStyle; + +/** + A Boolean value indicating whether the navigation bar is translucent or not. + Defaults to YES. + */ +@property (nonatomic) BOOL translucent; + +/** + The text to display in the title of the navigation bar. + Defaults to "Secure checkout". + */ +@property (nonatomic, copy) NSString *headerText; + +/** + The text to display for the button in the navigation bar. + Defaults to "Cancel". + */ +@property (nonatomic, copy) NSString *buttonText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.m new file mode 100644 index 00000000..5e062356 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSNavigationBarCustomization.m @@ -0,0 +1,44 @@ +// +// STDSNavigationBarCustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSLocalizedString.h" +#import "STDSNavigationBarCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSNavigationBarCustomization + ++ (instancetype)defaultSettings { + return [self new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _barTintColor = nil; + _headerText = STDSLocalizedString(@"Secure checkout", @"The title for the challenge response step of an authenticated checkout."); + _buttonText = STDSLocalizedString(@"Cancel", "The text for the button that cancels the current challenge process."); + _translucent = YES; + } + return self; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + STDSNavigationBarCustomization *copy = [super copyWithZone:zone]; + copy.barTintColor = self.barTintColor; + copy.headerText = self.headerText; + copy.buttonText = self.buttonText; + copy.barStyle = self.barStyle; + copy.translucent = self.translucent; + + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.h b/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.h new file mode 100644 index 00000000..cb836d5e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.h @@ -0,0 +1,23 @@ +// +// STDSNotInitializedException.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSNotInitializedException` represents an exception that will be thrown by + the the Stripe3DS2 SDK if methods are called without initializing `STDSThreeDS2Service`. + + @see STDSThreeDS2Service + */ +@interface STDSNotInitializedException : STDSException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.m b/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.m new file mode 100644 index 00000000..58544cc2 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSNotInitializedException.m @@ -0,0 +1,17 @@ +// +// STDSNotInitializedException.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSNotInitializedException.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSNotInitializedException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.h b/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.h new file mode 100644 index 00000000..1e3142a7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.h @@ -0,0 +1,42 @@ +// +// STDSProtocolErrorEvent.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSErrorMessage; + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSProtocolErrorEvent` contains details about erorrs received from or sent to the ACS. + */ +@interface STDSProtocolErrorEvent : NSObject + +/** + Designated initializer for `STDSProtocolErrorEvent`. + */ +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)identifier errorMessage:(STDSErrorMessage *)errorMessage; + +/** + `STDSProtocolErrorEvent` should not be directly initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + Details about the error. + */ +@property (nonatomic, readonly) STDSErrorMessage *errorMessage; + +/** + The SDK Transaction Identifier. + */ +@property (nonatomic, readonly) NSString *sdkTransactionIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.m b/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.m new file mode 100644 index 00000000..0dbd75b5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSProtocolErrorEvent.m @@ -0,0 +1,28 @@ +// +// STDSProtocolErrorEvent.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSProtocolErrorEvent.h" + +#import "STDSErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSProtocolErrorEvent + +- (instancetype)initWithSDKTransactionIdentifier:(NSString *)identifier errorMessage:(STDSErrorMessage *)errorMessage { + self = [super init]; + if (self) { + _sdkTransactionIdentifier = identifier; + _errorMessage = errorMessage; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.h b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.h new file mode 100644 index 00000000..658cdb90 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.h @@ -0,0 +1,53 @@ +// +// STDSRuntimeErrorEvent.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXTERN NSString * const kSTDSRuntimeErrorCodeParsingError; +FOUNDATION_EXTERN NSString * const kSTDSRuntimeErrorCodeEncryptionError; + +/** + `STDSRuntimeErrorEvent` contains details about run-time errors encountered during authentication. + + The following are examples of run-time errors: + - ACS is unreachable + - Unparseable message + - Network issues + */ +@interface STDSRuntimeErrorEvent : NSObject + +/** + A code corresponding to the type of error this represents. + */ +@property (nonatomic, readonly) NSString *errorCode; + +/** + Details about the error. + */ +@property (nonatomic, readonly) NSString *errorMessage; + +/** + Designated initializer for `STDSRuntimeErrorEvent`. + */ +- (instancetype)initWithErrorCode:(NSString *)errorCode errorMessage:(NSString *)errorMessage NS_DESIGNATED_INITIALIZER; + +/** + A representation of the `STDSRuntimeErrorEvent` as an `NSError` + */ +- (NSError *)NSErrorValue; + +/** + `STDSRuntimeErrorEvent` should not be directly initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.m b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.m new file mode 100644 index 00000000..8155e06e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeErrorEvent.m @@ -0,0 +1,37 @@ +// +// STDSRuntimeErrorEvent.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSRuntimeErrorEvent.h" + +#import "STDSStripe3DS2Error.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString * const kSTDSRuntimeErrorCodeParsingError = @"STDSRuntimeErrorCodeParsingError"; +NSString * const kSTDSRuntimeErrorCodeEncryptionError = @"STDSRuntimeErrorCodeEncryptionError"; + +@implementation STDSRuntimeErrorEvent + +- (instancetype)initWithErrorCode:(NSString *)errorCode errorMessage:(NSString *)errorMessage { + self = [super init]; + if (self) { + _errorCode = [errorCode copy]; + _errorMessage = [errorMessage copy]; + } + return self; +} + +- (NSError *)NSErrorValue { + return [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:[self.errorCode isEqualToString:kSTDSRuntimeErrorCodeParsingError] ? STDSErrorCodeRuntimeParsing : STDSErrorCodeRuntimeEncryption + userInfo:@{@"errorMessage": self.errorMessage}]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.h b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.h new file mode 100644 index 00000000..5b63a2d3 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.h @@ -0,0 +1,21 @@ +// +// STDSRuntimeException.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSException.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSRuntimeException` represents an exception that will be thrown by the + Stripe3DS2 SDK if it encounters an internal error. + */ +@interface STDSRuntimeException : STDSException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.m b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.m new file mode 100644 index 00000000..3a70d376 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSRuntimeException.m @@ -0,0 +1,17 @@ +// +// STDSRuntimeException.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSRuntimeException.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSRuntimeException + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.h new file mode 100644 index 00000000..a9818bb9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.h @@ -0,0 +1,48 @@ +// +// STDSSelectionCustomization.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A customization object that configures the appearance of + radio buttons and checkboxes. + */ +@interface STDSSelectionCustomization: NSObject + +/// The default settings. ++ (instancetype)defaultSettings; + +/** + The primary color of the selected state. + Defaults to blue. + */ +@property (nonatomic) UIColor *primarySelectedColor; + +/** + The secondary color of the selected state (e.g. the checkmark color). + Defaults to white. + */ +@property (nonatomic) UIColor *secondarySelectedColor; + +/** + The background color displayed in the unselected state. + Defaults to light blue. + */ +@property (nonatomic) UIColor *unselectedBackgroundColor; + +/** + The color of the border drawn around the view in the unselected state. + Defaults to blue. + */ +@property (nonatomic) UIColor *unselectedBorderColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.m new file mode 100644 index 00000000..d5fdcf35 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSSelectionCustomization.m @@ -0,0 +1,64 @@ +// +// STDSSelectionCustomization.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 6/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSSelectionCustomization.h" + +#import "UIColor+DefaultColors.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSSelectionCustomization + ++ (instancetype)defaultSettings { + return [self new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _primarySelectedColor = [UIColor _stds_blueColor]; + _secondarySelectedColor = UIColor.whiteColor; + _unselectedBackgroundColor = [UIColor _stds_colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { + return (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) ? + [UIColor colorWithRed:(CGFloat)231.0 / (CGFloat)255.0 + green:(CGFloat)241.0 / (CGFloat)255.0 + blue:(CGFloat)254.0 / (CGFloat)255.0 + alpha:1.0] : + [UIColor colorWithRed:(CGFloat)30.0 / (CGFloat)255.0 + green:(CGFloat)63.0 / (CGFloat)255.0 + blue:(CGFloat)84.0 / (CGFloat)255.0 + alpha:1.0]; + }]; + _unselectedBorderColor = [UIColor _stds_colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { + return (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) ? + [UIColor colorWithRed:(CGFloat)131.0 / (CGFloat)255.0 + green:(CGFloat)191.0 / (CGFloat)255.0 + blue:(CGFloat)250.0 / (CGFloat)255.0 + alpha:1] : + [UIColor colorWithRed:(CGFloat)65.0 / (CGFloat)255.0 + green:(CGFloat)94.0 / (CGFloat)255.0 + blue:(CGFloat)123.0 / (CGFloat)255.0 + alpha:1]; + }]; + } + return self; +} + +- (nonnull id)copyWithZone:(nullable NSZone *)zone { + STDSSelectionCustomization *copy = [STDSSelectionCustomization new]; + copy.primarySelectedColor = self.primarySelectedColor; + copy.secondarySelectedColor = self.secondarySelectedColor; + copy.unselectedBackgroundColor = self.unselectedBackgroundColor; + copy.unselectedBorderColor = self.unselectedBorderColor; + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.h b/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.h new file mode 100644 index 00000000..2e506fe2 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.h @@ -0,0 +1,68 @@ +// +// STDSStripe3DS2Error.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString * const STDSStripe3DS2ErrorDomain; + +/** + NSError.userInfo contains this key if we received an ErrorMessage instead of the expected response object. + The value of this key is the ErrorMessage. + */ +FOUNDATION_EXPORT NSString * const STDSStripe3DS2ErrorMessageErrorKey; + +/** + NSError.userInfo contains this key if we errored parsing JSON. + The value of this key is the invalid or missing field. + */ +FOUNDATION_EXPORT NSString * const STDSStripe3DS2ErrorFieldKey; + +/** + NSError.userInfo contains this key if we couldn't recognize critical message extension(s) + The value of this key is an array of identifiers. + */ +FOUNDATION_EXPORT NSString * const STDSStripe3DS2UnrecognizedCriticalMessageExtensionsKey; + + +typedef NS_ENUM(NSInteger, STDSErrorCode) { + + /// Code triggered an assertion + STDSErrorCodeAssertionFailed = 204, + + // JSON Parsing + /// Received invalid or malformed data + STDSErrorCodeJSONFieldInvalid = 203, + /// Expected field missing + STDSErrorCodeJSONFieldMissing = 201, + + /// Critical message extension not recognised + STDSErrorCodeUnrecognizedCriticalMessageExtension = 202, + + /// Decryption or verification error + STDSErrorCodeDecryptionVerification = 302, + + /// Error code corresponding to a `STDSRuntimeErrorEvent` for an unparseable network response + STDSErrorCodeRuntimeParsing = 400, + /// Error code corresponding to a `STDSRuntimeErrorEvent` for an error with decrypting or verifying a network response + STDSErrorCodeRuntimeEncryption = 401, + + // Networking + /// We received an ErrorMessage instead of the expected response object. `userInfo[STDSStripe3DS2ErrorMessageErrorKey]` will contain the ErrorMessage object. + STDSErrorCodeReceivedErrorMessage = 1000, + /// We received an unknown message type. + STDSErrorCodeUnknownMessageType = 1001, + /// Request timed out + STDSErrorCodeTimeout = 1002, + + /// Unknown + STDSErrorCodeUnknownError = 2000, +}; + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.m b/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.m new file mode 100644 index 00000000..6d2330ed --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSStripe3DS2Error.m @@ -0,0 +1,15 @@ +// +// STDSStripe3DS2Error.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSStripe3DS2Error.h" + +NSString * const STDSStripe3DS2ErrorDomain = @"com.stripe3ds2"; +NSString * const STDSStripe3DS2ErrorMessageErrorKey = @"STDSStripe3DS2ErrorMessageErrorKey"; +NSString * const STDSStripe3DS2ErrorFieldKey = @"STDSStripe3DS2ErrorFieldKey"; +NSString * const STDSStripe3DS2UnrecognizedCriticalMessageExtensionsKey = @"STDSStripe3DS2UnrecognizedCriticalMessageExtensionsKey"; + diff --git a/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.h b/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.h new file mode 100644 index 00000000..47443368 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.h @@ -0,0 +1,50 @@ +// +// STDSSwiftTryCatch.h +// +// Created by William Falcon on 10/10/14. +// Copyright (c) 2014 William Falcon. All rights reserved. +// +/* + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Provides try catch functionality for swift by wrapping around Objective-C + */ +@interface STDSSwiftTryCatch : NSObject + +/** + Provides try catch functionality for swift by wrapping around Objective-C + */ ++ (void)tryBlock:(void(^)(void))tryBlock catchBlock:(void(^)(NSException*exception))catchBlock finallyBlock:(void(^)(void))finallyBlock; +/** + Throws Objective-C exception with name and reason set to `s` + */ ++ (void)throwString:(NSString*)s; + +/** + Throws exception `e` + */ ++ (void)throwException:(NSException*)e; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.m b/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.m new file mode 100644 index 00000000..a9ac8601 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSSwiftTryCatch.m @@ -0,0 +1,59 @@ +// +// STDSSwiftTryCatch.h +// +// Created by William Falcon on 10/10/14. +// Copyright (c) 2014 William Falcon. All rights reserved. +// +/* + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +#import "STDSSwiftTryCatch.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSSwiftTryCatch + ++ (void)tryBlock:(void(^)(void))tryBlock catchBlock:(void(^)(NSException*exception))catchBlock finallyBlock:(void(^)(void))finallyBlock { + @try { + tryBlock ? tryBlock() : nil; + } + + @catch (NSException *exception) { + catchBlock ? catchBlock(exception) : nil; + } + @finally { + finallyBlock ? finallyBlock() : nil; + } +} + ++ (void)throwString:(NSString*)s +{ + @throw [NSException exceptionWithName:s reason:s userInfo:nil]; +} + ++ (void)throwException:(NSException*)e +{ + @throw e; +} + + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.h new file mode 100644 index 00000000..6f2bfede --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.h @@ -0,0 +1,47 @@ +// +// STDSTextFieldCustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A customization object to use to configure the UI of a text field. + + The font and textColor inherited from `STDSCustomization` configure + the user input text. + */ +@interface STDSTextFieldCustomization : STDSCustomization + +/** + The default settings. + + The default textColor is black. + */ ++ (instancetype)defaultSettings; + +/// The border width of the text field. Defaults to 2. +@property (nonatomic) CGFloat borderWidth; + +/// The color of the border of the text field. Defaults to clear. +@property (nonatomic) UIColor *borderColor; + +/// The corner radius of the edges of the text field. Defaults to 8. +@property (nonatomic) CGFloat cornerRadius; + +/// The appearance of the keyboard. Defaults to UIKeyboardAppearanceDefault. +@property (nonatomic) UIKeyboardAppearance keyboardAppearance; + +/// The color of the placeholder text. Defaults to light gray. +@property (nonatomic) UIColor *placeholderTextColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.m new file mode 100644 index 00000000..18a85c8d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSTextFieldCustomization.m @@ -0,0 +1,50 @@ +// +// STDSTextFieldCustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSTextFieldCustomization.h" + +#import "UIFont+DefaultFonts.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSTextFieldCustomization + ++ (instancetype)defaultSettings { + return [self new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.font = [UIFont _stds_defaultLabelTextFontWithScale:(CGFloat)1.9]; + _borderWidth = 2; + _cornerRadius = 8; + _keyboardAppearance = UIKeyboardAppearanceDefault; + + self.textColor = UIColor._stds_labelColor; + _borderColor = UIColor.clearColor; + _placeholderTextColor = UIColor._stds_systemGray2Color; + } + return self; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + STDSTextFieldCustomization *copy = [super copyWithZone:zone]; + copy.borderWidth = self.borderWidth; + copy.borderColor = self.borderColor; + copy.cornerRadius = self.cornerRadius; + copy.keyboardAppearance = self.keyboardAppearance; + copy.placeholderTextColor = self.placeholderTextColor; + + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.h b/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.h new file mode 100644 index 00000000..c0baa55f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.h @@ -0,0 +1,84 @@ +// +// STDSThreeDS2Service.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@class STDSConfigParameters; +@class STDSTransaction; +@class STDSUICustomization; +@class STDSWarning; + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSThreeDS2Service` is the main 3DS SDK interface and provides methods to process transactions. + */ +@interface STDSThreeDS2Service : NSObject + +/** + A list of warnings that may be populated once the SDK has been initialized. + */ +@property (nonatomic, readonly, nullable) NSArray *warnings; + +/** + Initializes the 3DS SDK instance. + + This method should be called at the start of the payment stage of a transaction. + **Note: Until the `STDSThreeDS2Service instance is initialized, it will be unusable.** + + - Performs security checks + - Collects device information + + @param config Configuration information that will be used during initialization. @see STDSConfigParameters + @param locale Optional override for the locale to use in UI. If `nil`, will default to the current system locale. + @param uiSettings Optional custom UI settings. If `nil`, will default to `[STDSUICustomization defaultSettings]`. + This argument is copied; any further changes to the customization object have no effect. @see STDSUICustomization + + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `config` is `nil` or any of `config`, `locale`, or `uiSettings` are invalid. @see STDSInvalidInputException + @exception STDSAlreadyInitializedException Will throw an `STDSAlreadyInitializedException` if the 3DS SDK instance has already been initialized. @see STDSSDKAlreadyInitializedException + @exception STDSRuntimeException Will throw an `STDSRuntimeException` if there is an internal error in the SDK. @see STDSRuntimeException + */ +- (void)initializeWithConfig:(STDSConfigParameters *)config + locale:(nullable NSLocale *)locale + uiSettings:(nullable STDSUICustomization *)uiSettings; + +/** + Creates and returns an instance of `STDSTransaction`. + + @param directoryServerID The Directory Server identifier returned in the authentication response + @param protocolVersion 3DS protocol version according to which the transaction will be created. Uses the default value of 2.1.0 if nil + + @exception STDSNotInitializedException Will throw an `STDSNotInitializedException` if the the `STDSThreeDS2Service` instance hasn't been initialized with a call to `initializeWithConfig:locale:uiSettings:`. @see STDSNotInitializedException + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if `directoryServerID` is not recognized or if the `protocolVersion` is not supported by this version of the SDK. @see STDSInvalidInputException + @exception STDSRuntimeException Will throw an `STDSRuntimeException` if there is an internal error in the SDK. @see STDSRuntimeException + */ +- (STDSTransaction *)createTransactionForDirectoryServer:(NSString *)directoryServerID + withProtocolVersion:(nullable NSString *)protocolVersion; + +/** + Creates and returns an instance of `STDSTransaction` using a custom directory server certificate. + Will return nil if unable to create a certificate from the provided params. + + @param directoryServerID The Directory Server identifier returned in the authentication response + @param serverKeyID An additional authentication key used by some Directory Servers + @param certificateString A Base64-encoded PEM or DER formatted certificate string containing the directory server's public key + @param rootCertificateStrings An arry of base64-encoded PEM or DER formatted certificate strings containing the DS root certificate used for signature checks + @param protocolVersion 3DS protocol version according to which the transaction will be created. Uses the default value of 2.1.0 if nil + + @exception STDSNotInitializedException Will throw an `STDSNotInitializedException` if the the `STDSThreeDS2Service` instance hasn't been initialized with a call to `initializeWithConfig:locale:uiSettings:`. @see STDSNotInitializedException + @exception STDSInvalidInputException Will throw an `STDSInvalidInputException` if the `protocolVersion` is not supported by this version of the SDK. @see STDSInvalidInputException + */ +- (nullable STDSTransaction *)createTransactionForDirectoryServer:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + certificateString:(NSString *)certificateString + rootCertificateStrings:(NSArray *)rootCertificateStrings + withProtocolVersion:(nullable NSString *)protocolVersion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.m b/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.m new file mode 100644 index 00000000..2c45de66 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSThreeDS2Service.m @@ -0,0 +1,191 @@ +// +// STDSThreeDS2Service.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSThreeDS2Service.h" + +#include + +#import "STDSAlreadyInitializedException.h" +#import "STDSConfigParameters.h" +#import "STDSDebuggerChecker.h" +#import "STDSDeviceInformationManager.h" +#import "STDSDirectoryServerCertificate.h" +#import "STDSException+Internal.h" +#import "STDSInvalidInputException.h" +#import "STDSLocalizedString.h" +#import "STDSJailbreakChecker.h" +#import "STDSIntegrityChecker.h" +#import "STDSNotInitializedException.h" +#import "STDSOSVersionChecker.h" +#import "STDSSecTypeUtilities.h" +#import "STDSSimulatorChecker.h" +#import "STDSThreeDSProtocolVersion.h" +#import "STDSTransaction+Private.h" +#import "STDSWarning.h" + +static const int kServiceNotInitialized = 0; +static const int kServiceInitialized = 1; + +static NSString * const kInternalStripeTestingConfigParam = @"kInternalStripeTestingConfigParam"; +static NSString * const kIgnoreDeviceInformationRestrictionsParam = @"kIgnoreDeviceInformationRestrictionsParam"; +static NSString * const kUseULTestLOAParam = @"kUseULTestLOAParam"; + +@implementation STDSThreeDS2Service +{ + atomic_int _initialized; + + STDSDeviceInformation *_deviceInformation; + STDSUICustomization *_uiSettings; + STDSConfigParameters *_configuration; +} + +@synthesize warnings = _warnings; + +- (void)initializeWithConfig:(STDSConfigParameters *)config + locale:(nullable NSLocale *)locale + uiSettings:(nullable STDSUICustomization *)uiSettings { + if (config == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:[NSString stringWithFormat:@"%@ config parameter must be non-nil.", NSStringFromSelector(_cmd)]]; + } + + int notInitialized = kServiceNotInitialized; // Can't pass a const to atomic_compare_exchange_strong_explicit so copy here + if (!atomic_compare_exchange_strong_explicit(&_initialized, ¬Initialized, kServiceInitialized, memory_order_release, memory_order_relaxed)) { + @throw [STDSAlreadyInitializedException exceptionWithMessage:[NSString stringWithFormat:@"STDSThreeDS2Service instance %p has already been initialized.", self]]; + } + + _configuration = config; + _uiSettings = uiSettings ? [uiSettings copy] : [STDSUICustomization defaultSettings]; + + NSMutableArray *warnings = [NSMutableArray array]; + if ([STDSJailbreakChecker isJailbroken]) { + STDSWarning *jailbrokenWarning = [[STDSWarning alloc] initWithIdentifier:@"SW01" message:STDSLocalizedString(@"The device is jailbroken.", @"The text for warning when a device is jailbroken") severity:STDSWarningSeverityHigh]; + [warnings addObject:jailbrokenWarning]; + } + + if (![STDSIntegrityChecker SDKIntegrityIsValid]) { + STDSWarning *integrityWarning = [[STDSWarning alloc] initWithIdentifier:@"SW02" message:STDSLocalizedString(@"The integrity of the SDK has been tampered.", @"The text for warning when the integrity of the SDK has been tampered with") severity:STDSWarningSeverityHigh]; + [warnings addObject:integrityWarning]; + } + + if ([STDSSimulatorChecker isRunningOnSimulator]) { + STDSWarning *simulatorWarning = [[STDSWarning alloc] initWithIdentifier:@"SW03" message:STDSLocalizedString(@"An emulator is being used to run the App.", @"The text for warning when an emulator is being used to run the application.") severity:STDSWarningSeverityHigh]; + [warnings addObject:simulatorWarning]; + } + + if ([STDSDebuggerChecker processIsCurrentlyAttachedToDebugger]) { + STDSWarning *debuggerWarning = [[STDSWarning alloc] initWithIdentifier:@"SW04" message:STDSLocalizedString(@"A debugger is attached to the App.", @"The text for warning when a debugger is currently attached to the process.") severity:STDSWarningSeverityMedium]; + [warnings addObject:debuggerWarning]; + } + + if (![STDSOSVersionChecker isSupportedOSVersion]) { + STDSWarning *versionWarning = [[STDSWarning alloc] initWithIdentifier:@"SW05" message:STDSLocalizedString(@"The OS or the OS Version is not supported.", "The text for warning when the SDK is running on an unsupported OS or OS version.") severity:STDSWarningSeverityHigh]; + [warnings addObject:versionWarning]; + } + + _warnings = [warnings copy]; + + _deviceInformation = [STDSDeviceInformationManager deviceInformationWithWarnings:_warnings + ignoringRestrictions:[[_configuration parameterValue:kIgnoreDeviceInformationRestrictionsParam] isEqualToString:@"Y"]]; + +} + +- (STDSTransaction *)createTransactionForDirectoryServer:(NSString *)directoryServerID + withProtocolVersion:(nullable NSString *)protocolVersion { + if (_initialized != kServiceInitialized) { + @throw [STDSNotInitializedException exceptionWithMessage:@"STDSThreeDS2Service instance %p has not been initialized before call to %@", self, NSStringFromSelector(_cmd)]; + } + + if (directoryServerID == nil) { + @throw [STDSInvalidInputException exceptionWithMessage:@"%@ directoryServerID parameter must be non-nil.", NSStringFromSelector(_cmd)]; + } + + STDSDirectoryServer directoryServer = STDSDirectoryServerForID(directoryServerID); + if (directoryServer == STDSDirectoryServerUnknown) { + if ([[_configuration parameterValue:kInternalStripeTestingConfigParam] isEqualToString:@"Y"]) { + directoryServer = STDSDirectoryServerULTestRSA; + } else { + @throw [STDSInvalidInputException exceptionWithMessage:@"%@ is an invalid directoryServerID value", directoryServerID]; + } + } + + if (protocolVersion != nil && ![self _supportsProtocolVersion:protocolVersion]) { + @throw [STDSInvalidInputException exceptionWithMessage:@"3DS2 Protocol Version %@ is not supported by this SDK", protocolVersion]; + } + + + + STDSTransaction *transaction = [[STDSTransaction alloc] initWithDeviceInformation:_deviceInformation + directoryServer:directoryServer + protocolVersion:(protocolVersion != nil) ? STDSThreeDSProtocolVersionForString(protocolVersion) : STDSThreeDSProtocolVersion2_1_0 + uiCustomization:_uiSettings]; + transaction.bypassTestModeVerification = [[_configuration parameterValue:kInternalStripeTestingConfigParam] isEqualToString:@"Y"]; + transaction.useULTestLOA = [[_configuration parameterValue:kUseULTestLOAParam] isEqualToString:@"Y"]; + return transaction; + +} + +- (nullable STDSTransaction *)createTransactionForDirectoryServer:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + certificateString:(NSString *)certificateString + rootCertificateStrings:(NSArray *)rootCertificateStrings + withProtocolVersion:(nullable NSString *)protocolVersion { + if (_initialized != kServiceInitialized) { + @throw [STDSNotInitializedException exceptionWithMessage:@"STDSThreeDS2Service instance %p has not been initialized before call to %@", self, NSStringFromSelector(_cmd)]; + } + + if (protocolVersion != nil && ![self _supportsProtocolVersion:protocolVersion]) { + @throw [STDSInvalidInputException exceptionWithMessage:@"3DS2 Protocol Version %@ is not supported by this SDK", protocolVersion]; + } + + STDSTransaction *transaction = nil; + + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate customCertificateWithString:certificateString]; + + if (certificate != nil) { + transaction = [[STDSTransaction alloc] initWithDeviceInformation:_deviceInformation + directoryServerID:directoryServerID + serverKeyID:serverKeyID + directoryServerCertificate:certificate + rootCertificateStrings:rootCertificateStrings + protocolVersion:(protocolVersion != nil) ? STDSThreeDSProtocolVersionForString(protocolVersion) : STDSThreeDSProtocolVersion2_1_0 + uiCustomization:_uiSettings]; + transaction.bypassTestModeVerification = [_configuration parameterValue:kInternalStripeTestingConfigParam] != nil; + } + + return transaction; +} + +- (nullable NSArray *)warnings { + if (_initialized != kServiceInitialized) { + @throw [STDSNotInitializedException exceptionWithMessage:@"STDSThreeDS2Service instance %p has not been initialized before call to %@", self, NSStringFromSelector(_cmd)]; + } + + return _warnings; +} + +#pragma mark - Internal + +- (BOOL)_supportsProtocolVersion:(NSString *)protocolVersion { + STDSThreeDSProtocolVersion version = STDSThreeDSProtocolVersionForString(protocolVersion); + switch (version) { + case STDSThreeDSProtocolVersion2_1_0: + return YES; + + case STDSThreeDSProtocolVersion2_2_0: + return YES; + + case STDSThreeDSProtocolVersionFallbackTest: + // only support fallback test if we have the internal testing config param + return [[_configuration parameterValue:kInternalStripeTestingConfigParam] isEqualToString:@"Y"]; + + case STDSThreeDSProtocolVersionUnknown: + return NO; + } +} + +@end diff --git a/Stripe3DS2/Stripe3DS2/include/STDSThreeDSProtocolVersion.h b/Stripe3DS2/Stripe3DS2/include/STDSThreeDSProtocolVersion.h new file mode 100644 index 00000000..b4161a6e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSThreeDSProtocolVersion.h @@ -0,0 +1,15 @@ +// +// STDSThreeDSProtocolVersion.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 6/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString * const Stripe3DS2ProtocolVersion; + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSTransaction.h b/Stripe3DS2/Stripe3DS2/include/STDSTransaction.h new file mode 100644 index 00000000..02a1f1af --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSTransaction.h @@ -0,0 +1,94 @@ +// +// STDSTransaction.h +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import + +typedef void (^STDSTransactionVoidBlock)(void); + +@class STDSAuthenticationRequestParameters, STDSChallengeParameters; +@protocol STDSChallengeStatusReceiver; + +NS_ASSUME_NONNULL_BEGIN + +/** + `STDSTransaction` holds parameters that the 3DS Server requires to create AReq messages and to perform the Challenge Flow. + */ +@interface STDSTransaction : NSObject + +/** + The UI type of the presented challenge for this transaction if applicable. Will be one of + "none" + "text" + "single_select" + "multi_select" + "oob" + "html" + */ +@property (nonatomic, readonly, copy) NSString *presentedChallengeUIType; + +/** + Encrypts device information collected during initialization and returns it along with SDK details. + + @return Encrypted device information and details about this SDK. @see STDSAuthenticationRequestParameters + + @exception SDKRuntimeException Thrown if an internal error is encountered. + */ +- (STDSAuthenticationRequestParameters *)createAuthenticationRequestParameters; + +/** + Returns a UIViewController instance displaying the Directory Server logo and a spinner. Present this during the Authentication Request/Response. + */ +- (UIViewController *)createProgressViewControllerWithDidCancel:(STDSTransactionVoidBlock)didCancel; + +/** + Initiates the challenge process, displaying challenge UI as needed. + + @param presentingViewController The UIViewController used to present the challenge response UIViewController + @param challengeParameters Details required to conduct the challenge process. @see STDSChallengeParameters + @param challengeStatusReceiver A callback object to receive the status of the challenge. See @STDSChallengeStatusReceiver + @param timeout An interval in seconds within which the challenge process will finish. Must be at least 5 minutes. + + @exception STDSInvalidInputException Thrown if an argument is invalid (e.g. timeout less than 5 minutes). @see STDSInvalidInputException + @exception STDSSDKRuntimeException Thrown if an internal error is encountered, and if you call this method after calling `close`. @see SDKRuntimeException + + @note challengeStatusReceiver must conform to . This is a workaround: When the static Stripe3DS2 is compiled into Stripe.framework, the resulting swiftinterface and generated .h files reference this protocol. To allow users to build without including Stripe3DS2 directly, we'll take an `id` here instead. + */ +- (void)doChallengeWithViewController:(UIViewController *)presentingViewController + challengeParameters:(STDSChallengeParameters *)challengeParameters + challengeStatusReceiver:(id)challengeStatusReceiver + timeout:(NSTimeInterval)timeout; + +/** + Returns the version of the Stripe3DS2 SDK, e.g. @"1.0" + */ +- (NSString *)sdkVersion; + +/** +Cleans up resources held by `STDSTransaction`. Call this when the transaction is completed, if `doChallengeWithChallengeParameters:challengeStatusReceiver:timeout` is not called. + + @note Don't use this object after calling this method. Calling `doChallengeWithViewController:challengeParameters:challengeStatusReceiver:timeout` after calling this method will throw an `STDSSDKRuntimeException` + */ +- (void)close; + +/** + Alternate challenge initiation method meant only for internal use by Stripe SDK. + */ +- (void)doChallengeWithChallengeParameters:(STDSChallengeParameters *)challengeParameters + challengeStatusReceiver:(id)challengeStatusReceiver + timeout:(NSTimeInterval)timeout + presentationBlock:(void (^)(UIViewController *, void(^)(void)))presentationBlock; + +/** + Function to manually cancel the challenge flow. + */ +- (void)cancelChallengeFlow; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSTransaction.m b/Stripe3DS2/Stripe3DS2/include/STDSTransaction.m new file mode 100644 index 00000000..654efa64 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSTransaction.m @@ -0,0 +1,531 @@ +// +// STDSTransaction.m +// Stripe3DS2 +// +// Created by Yuki Tokuhiro on 3/21/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSTransaction.h" +#import "STDSTransaction+Private.h" + +#import "STDSBundleLocator.h" +#import "NSDictionary+DecodingHelpers.h" +#import "NSError+Stripe3DS2.h" +#import "NSString+JWEHelpers.h" +#import "STDSACSNetworkingManager.h" +#import "STDSAuthenticationRequestParameters.h" +#import "STDSChallengeRequestParameters.h" +#import "STDSCompletionEvent.h" +#import "STDSChallengeParameters.h" +#import "STDSChallengeResponseObject.h" +#import "STDSChallengeResponseViewController.h" +#import "STDSChallengeStatusReceiver.h" +#import "STDSDeviceInformation.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSEphemeralKeyPair.h" +#import "STDSErrorMessage+Internal.h" +#import "STDSException+Internal.h" +#import "STDSInvalidInputException.h" +#import "STDSJSONWebEncryption.h" +#import "STDSJSONWebSignature.h" +#import "STDSProgressViewController.h" +#import "STDSProtocolErrorEvent.h" +#import "STDSRuntimeErrorEvent.h" +#import "STDSRuntimeException.h" +#import "STDSSecTypeUtilities.h" +#import "STDSStripe3DS2Error.h" +#import "STDSDeviceInformationParameter.h" + +static const NSTimeInterval kMinimumTimeout = 5 * 60; +static NSString * const kStripeLOA = @"3DS_LOA_SDK_STIN_020100_00162"; +static NSString * const kULTestLOA = @"3DS_LOA_SDK_PPFU_020100_00007"; + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSTransaction() + +@property (nonatomic, weak) id challengeStatusReceiver; +@property (nonatomic, strong, nullable) STDSChallengeResponseViewController *challengeResponseViewController; +/// Stores the most recent parameters used to make a CReq +@property (nonatomic, nullable) STDSChallengeRequestParameters *challengeRequestParameters; +/// YES if `close` was called or the challenge flow finished. +@property (nonatomic, getter=isCompleted) BOOL completed; +@end + +@implementation STDSTransaction +{ + STDSDeviceInformation *_deviceInformation; + STDSDirectoryServer _directoryServer; + STDSEphemeralKeyPair *_ephemeralKeyPair; + STDSThreeDSProtocolVersion _protocolVersion; + NSString *_identifier; + + STDSDirectoryServerCertificate *_customDirectoryServerCertificate; + NSArray *_rootCertificateStrings; + NSString *_customDirectoryServerID; + NSString *_serverKeyID; + + STDSACSNetworkingManager *_networkingManager; + + STDSUICustomization *_uiCustomization; +} + +- (instancetype)initWithDeviceInformation:(STDSDeviceInformation *)deviceInformation + directoryServer:(STDSDirectoryServer)directoryServer + protocolVersion:(STDSThreeDSProtocolVersion)protocolVersion + uiCustomization:(nonnull STDSUICustomization *)uiCustomization { + self = [super init]; + if (self) { + _deviceInformation = deviceInformation; + _directoryServer = directoryServer; + _protocolVersion = protocolVersion; + _completed = NO; + _identifier = [NSUUID UUID].UUIDString.lowercaseString; + _ephemeralKeyPair = [STDSEphemeralKeyPair ephemeralKeyPair]; + _uiCustomization = uiCustomization; + if (_ephemeralKeyPair == nil) { + return nil; + } + } + + return self; +} + +- (instancetype)initWithDeviceInformation:(STDSDeviceInformation *)deviceInformation + directoryServerID:(NSString *)directoryServerID + serverKeyID:(nullable NSString *)serverKeyID + directoryServerCertificate:(STDSDirectoryServerCertificate *)directoryServerCertificate + rootCertificateStrings:(NSArray *)rootCertificateStrings + protocolVersion:(STDSThreeDSProtocolVersion)protocolVersion + uiCustomization:(STDSUICustomization *)uiCustomization { + self = [super init]; + if (self) { + _deviceInformation = deviceInformation; + _directoryServer = STDSDirectoryServerCustom; + _customDirectoryServerCertificate = directoryServerCertificate; + _rootCertificateStrings = rootCertificateStrings; + _customDirectoryServerID = [directoryServerID copy]; + _serverKeyID = [serverKeyID copy]; + _protocolVersion = protocolVersion; + _completed = NO; + _identifier = [NSUUID UUID].UUIDString.lowercaseString; + _ephemeralKeyPair = [STDSEphemeralKeyPair ephemeralKeyPair]; + _uiCustomization = uiCustomization; + if (_ephemeralKeyPair == nil) { + return nil; + } + } + + return self; +} + +- (NSString *)sdkVersion { + return [[STDSBundleLocator stdsResourcesBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; +} + +- (NSString *)presentedChallengeUIType { + switch (self.challengeResponseViewController.response.acsUIType) { + + case STDSACSUITypeNone: + return @"none"; + + case STDSACSUITypeText: + return @"text"; + + case STDSACSUITypeSingleSelect: + return @"single_select"; + + case STDSACSUITypeMultiSelect: + return @"multi_select"; + + case STDSACSUITypeOOB: + return @"oob"; + + case STDSACSUITypeHTML: + return @"html"; + } +} + +- (STDSAuthenticationRequestParameters *)createAuthenticationRequestParameters { + NSError *error = nil; + NSString *encryptedDeviceData = nil; + + if (_directoryServer == STDSDirectoryServerCustom) { + encryptedDeviceData = [STDSJSONWebEncryption encryptJSON:_deviceInformation.dictionaryValue + withCertificate:_customDirectoryServerCertificate + directoryServerID:_customDirectoryServerID + serverKeyID:_serverKeyID + error:&error]; + } else { + encryptedDeviceData = [STDSJSONWebEncryption encryptJSON:_deviceInformation.dictionaryValue + forDirectoryServer:_directoryServer + error:&error]; + } + if (encryptedDeviceData == nil) { + @throw [STDSRuntimeException exceptionWithMessage:@"Error encrypting device information %@", error]; + } + + return [[STDSAuthenticationRequestParameters alloc] initWithSDKTransactionIdentifier:_identifier + deviceData:encryptedDeviceData + sdkEphemeralPublicKey:_ephemeralKeyPair.publicKeyJWK + sdkAppIdentifier:[STDSDeviceInformationParameter sdkAppIdentifier] + sdkReferenceNumber:self.useULTestLOA ? kULTestLOA : kStripeLOA + messageVersion:[self _messageVersion]]; +} + +- (UIViewController *)createProgressViewControllerWithDidCancel:(void (^)(void))didCancel { + return [[STDSProgressViewController alloc] initWithDirectoryServer:[self _directoryServerForUI] uiCustomization:_uiCustomization didCancel:didCancel]; +} + +- (void)doChallengeWithViewController:(UIViewController *)presentingViewController + challengeParameters:(STDSChallengeParameters *)challengeParameters + challengeStatusReceiver:(id)challengeStatusReceiver + timeout:(NSTimeInterval)timeout { + + [self doChallengeWithChallengeParameters:challengeParameters + challengeStatusReceiver:challengeStatusReceiver + timeout:timeout + presentationBlock:^(UIViewController * _Nonnull challengeVC, void (^completion)(void)) { + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:challengeVC]; + + // Disable "swipe to dismiss" behavior in iOS 13 + if ([navigationController respondsToSelector:NSSelectorFromString(@"isModalInPresentation")]) { + [navigationController setValue:@YES forKey:@"modalInPresentation"]; + } + + [presentingViewController presentViewController:navigationController animated:YES completion:^{ + completion(); + }]; + }]; +} + +- (void)doChallengeWithChallengeParameters:(STDSChallengeParameters *)challengeParameters + challengeStatusReceiver:(id)challengeStatusReceiver + timeout:(NSTimeInterval)timeout + presentationBlock:(void (^)(UIViewController *, void(^)(void)))presentationBlock { + if (self.isCompleted) { + @throw [STDSRuntimeException exceptionWithMessage:@"The transaction has already completed."]; + } else if (timeout < kMinimumTimeout) { + @throw [STDSInvalidInputException exceptionWithMessage:@"Timeout value of %lf seconds is less than 5 minutes", timeout]; + } + self.challengeStatusReceiver = challengeStatusReceiver; + self.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:(timeout) target:self selector:@selector(_didTimeout) userInfo:nil repeats:NO]; + + self.challengeRequestParameters = [[STDSChallengeRequestParameters alloc] initWithChallengeParameters:challengeParameters + transactionIdentifier:_identifier + messageVersion:[self _messageVersion]]; + + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:challengeParameters.acsSignedContent allowNilKey:self.bypassTestModeVerification]; + BOOL validJWS = jws != nil; + if (validJWS && !self.bypassTestModeVerification) { + if (_customDirectoryServerCertificate != nil) { + if (_rootCertificateStrings.count == 0) { + validJWS = NO; + } else { + validJWS = [STDSJSONWebEncryption verifyJSONWebSignature:jws withCertificate:_customDirectoryServerCertificate rootCertificates:_rootCertificateStrings]; + } + } else { + validJWS = [STDSJSONWebEncryption verifyJSONWebSignature:jws forDirectoryServer:_directoryServer]; + } + } + if (!validJWS) { + dispatch_async(dispatch_get_main_queue(), ^{ + [challengeStatusReceiver transaction:self + didErrorWithRuntimeErrorEvent:[[STDSRuntimeErrorEvent alloc] initWithErrorCode:kSTDSRuntimeErrorCodeEncryptionError errorMessage:@"Error verifying JWS response."]]; + [self _cleanUp]; + }); + return; + } + + NSError *jsonError = nil; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jws.payload options:0 error:&jsonError]; + NSDictionary *acsEphmeralKeyJWK = [json _stds_dictionaryForKey:@"acsEphemPubKey" required:NO error:NULL]; + STDSEllipticCurvePoint *acsEphemeralKey = [[STDSEllipticCurvePoint alloc] initWithJWK:acsEphmeralKeyJWK]; + NSString *acsURLString = [json _stds_stringForKey:@"acsURL" required:NO error:NULL]; + NSURL *acsURL = [NSURL URLWithString:acsURLString ?: @""]; + if (acsEphemeralKey == nil || acsURL == nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + [challengeStatusReceiver transaction:self + didErrorWithRuntimeErrorEvent:[[STDSRuntimeErrorEvent alloc] initWithErrorCode:kSTDSRuntimeErrorCodeParsingError errorMessage:[NSString stringWithFormat:@"Unable to create key or url from ACS json: %@\n\n jsonError: %@", json, jsonError]]]; + [self _cleanUp]; + }); + return; + } + + NSData *ecdhSecret = [_ephemeralKeyPair createSharedSecretWithEllipticCurveKey:acsEphemeralKey]; + if (ecdhSecret == nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + [challengeStatusReceiver transaction:self + didErrorWithRuntimeErrorEvent:[[STDSRuntimeErrorEvent alloc] initWithErrorCode:kSTDSRuntimeErrorCodeEncryptionError errorMessage:@"Error during Diffie-Helman key exchange"]]; + [self _cleanUp]; + }); + return; + } + + NSData *contentEncryptionKeySDKtoACS = STDSCreateConcatKDFWithSHA256(ecdhSecret, 32, self.useULTestLOA ? kULTestLOA : kStripeLOA); + // These two keys are intentionally identical + // ref. Protocol and Core Functions Specification Version 2.2.0 Section 6.2.3.2 & 6.2.3.3 + // "In this version of the specification [contentEncryptionKeyACStoSDK] and [contentEncryptionKeySDKtoACS] + // are extracted with the same value." + NSData *contentEncryptionKeyACStoSDK = [contentEncryptionKeySDKtoACS copy]; + + _networkingManager = [[STDSACSNetworkingManager alloc] initWithURL:acsURL + sdkContentEncryptionKey:contentEncryptionKeySDKtoACS + acsContentEncryptionKey:contentEncryptionKeyACStoSDK + acsTransactionIdentifier:self.challengeRequestParameters.acsTransactionIdentifier]; + // Start the Challenge flow + STDSImageLoader *imageLoader = [[STDSImageLoader alloc] initWithURLSession:NSURLSession.sharedSession]; + self.challengeResponseViewController = [[STDSChallengeResponseViewController alloc] initWithUICustomization:_uiCustomization imageLoader:imageLoader directoryServer:[self _directoryServerForUI]]; + self.challengeResponseViewController.delegate = self; + + presentationBlock(self.challengeResponseViewController, ^{ [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; }); +} + +- (void)close { + [self _cleanUp]; +} + +- (void)cancelChallengeFlow { + [self challengeResponseViewControllerDidCancel:self.challengeResponseViewController]; +} + +- (void)dealloc { + [self _cleanUp]; +} + +#pragma mark - Private + +// When we get a directory certificate and ID from the server, we mark it as Custom for encryption, +// but may have a correct mapping to a DS logo for the UI +- (STDSDirectoryServer)_directoryServerForUI { + return (_customDirectoryServerID != nil) ? STDSDirectoryServerForID(_customDirectoryServerID) : _directoryServer; +} + +- (void)_makeChallengeRequest:(STDSChallengeRequestParameters *)challengeRequestParameters didCancel:(BOOL)didCancel { + [self.challengeResponseViewController setLoading]; + __weak STDSTransaction *weakSelf = self; + [_networkingManager submitChallengeRequest:self.challengeRequestParameters + withCompletion:^(id _Nullable response, NSError * _Nullable error) { + STDSTransaction *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.isCompleted) { + return; + } + // Parsing or network errors + if (response == nil || error) { + if (!error) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeUnknownError userInfo:nil]; + } + [strongSelf _handleError:error]; + return; + } + // Consistency errors (e.g. acsTransID changes) + NSError *validationError; + if (![strongSelf _validateChallengeResponse:response error:&validationError]) { + [strongSelf _handleError:validationError]; + return; + } + [strongSelf _handleChallengeResponse:response didCancel:didCancel]; + }]; +} + +- (BOOL)_validateChallengeResponse:(id)challengeResponse error:(NSError **)outError { + NSError *error; + if (![challengeResponse.acsTransactionID isEqualToString:self.challengeRequestParameters.acsTransactionIdentifier] || + ![challengeResponse.threeDSServerTransactionID isEqualToString:self.challengeRequestParameters.threeDSServerTransactionIdentifier] || + ![challengeResponse.sdkTransactionID isEqualToString:self.challengeRequestParameters.sdkTransactionIdentifier]) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorMessageErrorTransactionIDNotRecognized userInfo:nil]; + } else if (![challengeResponse.messageVersion isEqualToString:self.challengeRequestParameters.messageVersion]) { + error = [NSError _stds_invalidJSONFieldError:@"messageVersion"]; + } else if (!self.bypassTestModeVerification && ![challengeResponse.acsCounterACStoSDK isEqualToString:self.challengeRequestParameters.sdkCounterStoA]) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeDecryptionVerification userInfo:nil]; + } else if (challengeResponse.acsUIType == STDSACSUITypeHTML && !challengeResponse.acsHTML) { + error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeDecryptionVerification userInfo:nil]; + } + + if (error && outError) { + *outError = error; + } + return error == nil; +} + +- (void) _handleError:(NSError *)error { + // All the codes corresponding to errors that we treat as protocol errors (ie send to the ACS and report as an STDSProtocolErrorEvent) + NSSet *protocolErrorCodes = [NSSet setWithArray:@[@(STDSErrorCodeUnknownMessageType), + @(STDSErrorCodeJSONFieldInvalid), + @(STDSErrorCodeJSONFieldMissing), + @(STDSErrorCodeReceivedErrorMessage), + @(STDSErrorMessageErrorTransactionIDNotRecognized), + @(STDSErrorCodeUnrecognizedCriticalMessageExtension), + @(STDSErrorCodeDecryptionVerification)]]; + if (error.domain == STDSStripe3DS2ErrorDomain) { + NSString *sdkTransactionIdentifier = _identifier; + NSString *acsTransactionIdentifier = self.challengeRequestParameters.acsTransactionIdentifier; + NSString *messageVersion = [self _messageVersion]; + STDSErrorMessage *errorMessage; + switch (error.code) { + case STDSErrorCodeReceivedErrorMessage: + errorMessage = error.userInfo[STDSStripe3DS2ErrorMessageErrorKey]; + break; + case STDSErrorCodeUnknownMessageType: + errorMessage = [STDSErrorMessage errorForInvalidMessageWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion]; + break; + case STDSErrorCodeJSONFieldInvalid: + errorMessage = [STDSErrorMessage errorForJSONFieldInvalidWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion error:error]; + break; + case STDSErrorCodeJSONFieldMissing: + errorMessage = [STDSErrorMessage errorForJSONFieldMissingWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion error:error]; + break; + case STDSErrorCodeTimeout: + errorMessage = [STDSErrorMessage errorForTimeoutWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion]; + break; + case STDSErrorMessageErrorTransactionIDNotRecognized: + errorMessage = [STDSErrorMessage errorForUnrecognizedIDWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion]; + break; + case STDSErrorCodeUnrecognizedCriticalMessageExtension: + errorMessage = [STDSErrorMessage errorForUnrecognizedCriticalMessageExtensionsWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion error:error]; + break; + case STDSErrorCodeDecryptionVerification: + errorMessage = [STDSErrorMessage errorForDecryptionErrorWithACSTransactionID:acsTransactionIdentifier messageVersion:messageVersion]; + break; + default: + break; + } + + // Send the ErrorMessage (unless we received one) + if (error.code != STDSErrorCodeReceivedErrorMessage && errorMessage != nil) { + [_networkingManager sendErrorMessage:errorMessage]; + } + + // If it's a protocol error, call back to the challengeStatusReceiver + if ([protocolErrorCodes containsObject:@(error.code)] && errorMessage != nil) { + STDSProtocolErrorEvent *protocolErrorEvent = [[STDSProtocolErrorEvent alloc] initWithSDKTransactionIdentifier:sdkTransactionIdentifier + errorMessage:errorMessage]; + [self.challengeStatusReceiver transaction:self didErrorWithProtocolErrorEvent:protocolErrorEvent]; + } + + } + + if (error.domain != STDSStripe3DS2ErrorDomain || ![protocolErrorCodes containsObject:@(error.code)]) { + // This error is not a protocol error, and therefore a runtime error. + NSString *errorCode = [NSString stringWithFormat:@"%ld", (long)error.code]; + STDSRuntimeErrorEvent *runtimeErrorEvent = [[STDSRuntimeErrorEvent alloc] initWithErrorCode:errorCode errorMessage:error.localizedDescription]; + [self.challengeStatusReceiver transaction:self didErrorWithRuntimeErrorEvent:runtimeErrorEvent]; + } + + [self _dismissChallengeResponseViewController]; + [self _cleanUp]; +} + +- (void)_handleChallengeResponse:(id)challengeResponse didCancel:(BOOL)didCancel { + if (challengeResponse.challengeCompletionIndicator) { + // Final CRes + // We need to pass didCancel to here because we can't distinguish between cancellation and auth failure from the CRes + // (they both result in a transactionStatus of "N") + if (didCancel) { + // We already dismissed the view controller + [self.challengeStatusReceiver transactionDidCancel:self]; + [self _cleanUp]; + } else { + [self _dismissChallengeResponseViewController]; + STDSCompletionEvent *completionEvent = [[STDSCompletionEvent alloc] initWithSDKTransactionIdentifier:_identifier + transactionStatus:challengeResponse.transactionStatus]; + [self.challengeStatusReceiver transaction:self didCompleteChallengeWithCompletionEvent:completionEvent]; + [self _cleanUp]; + } + } else { + [self.challengeResponseViewController setChallengeResponse:challengeResponse animated:YES]; + + if ([self.challengeStatusReceiver respondsToSelector:@selector(transactionDidPresentChallengeScreen:)]) { + [self.challengeStatusReceiver transactionDidPresentChallengeScreen:self]; + } + } +} + +- (void)_cleanUp { + [self.timeoutTimer invalidate]; + self.completed = YES; + self.challengeResponseViewController = nil; + self.challengeStatusReceiver = nil; + _networkingManager = nil; +} + +- (void)_didTimeout { + [self _dismissChallengeResponseViewController]; + [_networkingManager sendErrorMessage:[STDSErrorMessage errorForTimeoutWithACSTransactionID:self.challengeRequestParameters.acsTransactionIdentifier messageVersion:[self _messageVersion]]]; + [self.challengeStatusReceiver transactionDidTimeOut:self]; + [self _cleanUp]; +} + +- (void)_dismissChallengeResponseViewController { + if ([self.challengeStatusReceiver respondsToSelector:@selector(dismissChallengeViewController:forTransaction:)]) { + [self.challengeStatusReceiver dismissChallengeViewController:self.challengeResponseViewController forTransaction:self]; + } else { + [self.challengeResponseViewController dismissViewControllerAnimated:YES completion:nil]; + } +} + +#pragma mark Helpers + +- (nullable NSString *)_messageVersion { + NSString *messageVersion = STDSThreeDSProtocolVersionStringValue(_protocolVersion); + if (messageVersion == nil) { + @throw [STDSRuntimeException exceptionWithMessage:@"Error determining message version."]; + } + return messageVersion; +} + +/// Convenience method to construct a CSV from the names of each STDSChallengeResponseSelectionInfo in the given array +- (NSString *)_csvForChallengeResponseSelectionInfo:(NSArray> *)selectionInfoArray { + NSMutableArray *selectionInfoNames = [NSMutableArray new]; + for (id selectionInfo in selectionInfoArray) { + [selectionInfoNames addObject:selectionInfo.name]; + } + return [selectionInfoNames componentsJoinedByString:@","]; +} + +#pragma mark - STDSChallengeResponseViewController + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitInput:(nonnull NSString *)userInput whitelistSelection:(nonnull id)whitelistSelection { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.challengeDataEntry = userInput; + self.challengeRequestParameters.whitelistingDataEntry = whitelistSelection.name; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; +} + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitSelection:(nonnull NSArray> *)selection whitelistSelection:(nonnull id)whitelistSelection { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.challengeDataEntry = [self _csvForChallengeResponseSelectionInfo:selection]; + self.challengeRequestParameters.whitelistingDataEntry = whitelistSelection.name; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; +} + +- (void)challengeResponseViewControllerDidOOBContinue:(nonnull STDSChallengeResponseViewController *)viewController whitelistSelection:(nonnull id)whitelistSelection { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.oobContinue = @(YES); + self.challengeRequestParameters.whitelistingDataEntry = whitelistSelection.name; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; +} + +- (void)challengeResponseViewControllerDidCancel:(STDSChallengeResponseViewController *)viewController { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.challengeCancel = @(STDSChallengeCancelTypeCardholderSelectedCancel); + [self _dismissChallengeResponseViewController]; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:YES]; +} + +- (void)challengeResponseViewControllerDidRequestResend:(STDSChallengeResponseViewController *)viewController { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.resendChallenge = @"Y"; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; +} + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitHTMLForm:(nonnull NSString *)form { + self.challengeRequestParameters = [self.challengeRequestParameters nextChallengeRequestParametersByIncrementCounter]; + self.challengeRequestParameters.challengeHTMLDataEntry = form; + [self _makeChallengeRequest:self.challengeRequestParameters didCancel:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.h b/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.h new file mode 100644 index 00000000..651b22f1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.h @@ -0,0 +1,109 @@ +// +// STDSUICustomization.h +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSCustomization.h" +#import "STDSButtonCustomization.h" +#import "STDSNavigationBarCustomization.h" +#import "STDSLabelCustomization.h" +#import "STDSTextFieldCustomization.h" +#import "STDSFooterCustomization.h" +#import "STDSSelectionCustomization.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The `STDSUICustomization` provides configuration for UI elements. + + It's important to configure this object appropriately before using it to initialize a + `STDSThreeDS2Service` object. `STDSThreeDS2Service` makes a copy of the customization + settings you provide; it ignores any subsequent changes you make to your `STDSUICustomization` instance. +*/ +@interface STDSUICustomization: NSObject + +/// The default settings. See individual properties for their default values. ++ (instancetype)defaultSettings; + +/** + Provides custom settings for the UINavigationBar of all UIViewControllers the SDK display. + The default is `[STDSNavigationBarCustomization defaultSettings]`. + */ +@property (nonatomic) STDSNavigationBarCustomization *navigationBarCustomization; + +/** + Provides custom settings for labels. + The default is `[STDSLabelCustomization defaultSettings]`. + */ +@property (nonatomic) STDSLabelCustomization *labelCustomization; + +/** + Provides custom settings for text fields. + The default is `[STDSTextFieldCustomization defaultSettings]`. + */ +@property (nonatomic) STDSTextFieldCustomization *textFieldCustomization; + +/** + The primary background color of all UIViewControllers the SDK display. + Defaults to white. + */ +@property (nonatomic) UIColor *backgroundColor; + +/** + The Challenge view displays a footer with additional details. This controls the background color of that view. + Defaults to gray. + */ +@property (nonatomic) STDSFooterCustomization *footerCustomization; + +/** + Sets a given button customization for the specified type. + + @param buttonCustomization The buttom customization to use. + @param buttonType The type of button to use the customization for. + */ +- (void)setButtonCustomization:(STDSButtonCustomization *)buttonCustomization forType:(STDSUICustomizationButtonType)buttonType; + +/** + Retrieves a button customization object for the given button type. + + @param buttonType The button type to retrieve a customization object for. + @return A button customization object, or the default if none was set. + @see STDSButtonCustomization + */ +- (STDSButtonCustomization *)buttonCustomizationForButtonType:(STDSUICustomizationButtonType)buttonType; + +/** + Provides custom settings for radio buttons and checkboxes. + The default is `[STDSSelectionCustomization defaultSettings]`. + */ +@property (nonatomic) STDSSelectionCustomization *selectionCustomization; + + +/** + The preferred status bar style for all UIViewControllers the SDK display. + Defaults to UIStatusBarStyleDefault. + */ +@property (nonatomic) UIStatusBarStyle preferredStatusBarStyle; + +#pragma mark - Progress View + +/** + The style of UIActivityIndicatorViews displayed. + This should contrast with `backgroundColor`. Defaults to regular on iOS 13+, + gray on iOS 10-12. + */ +@property (nonatomic) UIActivityIndicatorViewStyle activityIndicatorViewStyle; + +/** + The style of the UIBlurEffect displayed underneath the UIActivityIndicatorView. + Defaults to UIBlurEffectStyleDefault. + */ +@property (nonatomic) UIBlurEffectStyle blurStyle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.m b/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.m new file mode 100644 index 00000000..8d1d6812 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSUICustomization.m @@ -0,0 +1,83 @@ +// +// STDSUICustomization.m +// Stripe3DS2 +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSUICustomization.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSUICustomization() + +@property (nonatomic, strong) NSMutableDictionary *buttonCustomizationDictionary; + +@end + +@implementation STDSUICustomization + ++ (instancetype)defaultSettings { + return [[STDSUICustomization alloc] init]; +} + +- (instancetype)init { + self = [super init]; + + if (self) { + _buttonCustomizationDictionary = [@{ + @(STDSUICustomizationButtonTypeNext): [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeNext + ], + @(STDSUICustomizationButtonTypeCancel): [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeCancel], + @(STDSUICustomizationButtonTypeResend): [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeResend], + @(STDSUICustomizationButtonTypeSubmit): [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeSubmit], + @(STDSUICustomizationButtonTypeContinue): [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeContinue], + } mutableCopy]; + _navigationBarCustomization = [STDSNavigationBarCustomization defaultSettings]; + _labelCustomization = [STDSLabelCustomization defaultSettings]; + _textFieldCustomization = [STDSTextFieldCustomization defaultSettings]; + _footerCustomization = [STDSFooterCustomization defaultSettings]; + _selectionCustomization = [STDSSelectionCustomization defaultSettings]; + _backgroundColor = UIColor._stds_systemBackgroundColor; + _activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; + _blurStyle = UIBlurEffectStyleRegular; + _preferredStatusBarStyle = UIStatusBarStyleDefault; + } + + return self; +} + +- (void)setButtonCustomization:(STDSButtonCustomization *)buttonCustomization forType:(STDSUICustomizationButtonType)buttonType { + self.buttonCustomizationDictionary[@(buttonType)] = buttonCustomization; +} + +- (STDSButtonCustomization *)buttonCustomizationForButtonType:(STDSUICustomizationButtonType)buttonType { + return self.buttonCustomizationDictionary[@(buttonType)]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + STDSUICustomization *copy = [[[self class] allocWithZone:zone] init]; + copy.navigationBarCustomization = [self.navigationBarCustomization copy]; + copy.labelCustomization = [self.labelCustomization copy]; + copy.textFieldCustomization = [self.textFieldCustomization copy]; + NSMutableDictionary *buttonCustomizationDictionary = [NSMutableDictionary new]; + for (NSNumber *buttonCustomization in self.buttonCustomizationDictionary) { + buttonCustomizationDictionary[buttonCustomization] = [self.buttonCustomizationDictionary[buttonCustomization] copy]; + } + copy.buttonCustomizationDictionary = buttonCustomizationDictionary; + copy.footerCustomization = [self.footerCustomization copy]; + copy.selectionCustomization = [self.selectionCustomization copy]; + copy.backgroundColor = self.backgroundColor; + copy.activityIndicatorViewStyle = self.activityIndicatorViewStyle; + copy.blurStyle = self.blurStyle; + copy.preferredStatusBarStyle = self.preferredStatusBarStyle; + return copy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSWarning.h b/Stripe3DS2/Stripe3DS2/include/STDSWarning.h new file mode 100644 index 00000000..5f09fe70 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSWarning.h @@ -0,0 +1,69 @@ +// +// STDSWarning.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/12/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The `STDSWarningSeverity` enum defines the severity levels of warnings generated + during SDK initialization. @see STDSThreeDS2Service + */ +typedef NS_ENUM(NSInteger, STDSWarningSeverity) { + /** + Low severity + */ + STDSWarningSeverityLow = 0, + + /** + Medium severity + */ + STDSWarningSeverityMedium, + + /** + High severity + */ + STDSWarningSeverityHigh, +}; + +/** + The `STDSWarning` class represents warnings generated by `STDSThreeDS2Service` during + security checks run during initialization. @see STDSThreeDS2Service + */ +@interface STDSWarning : NSObject + +/** + Designated initializer for `STDSWarning`. + */ +- (instancetype)initWithIdentifier:(NSString *)identifier + message:(NSString *)message + severity:(STDSWarningSeverity)severity NS_DESIGNATED_INITIALIZER; + +/** + `STDSWarning` should not be directly initialized. + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + The identifier for this warning instance. + */ +@property (nonatomic, readonly) NSString *identifier; + +/** + The descriptive message for this warning. + */ +@property (nonatomic, readonly) NSString *message; + +/** + The severity of this warning. + */ +@property (nonatomic, readonly) STDSWarningSeverity severity; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/STDSWarning.m b/Stripe3DS2/Stripe3DS2/include/STDSWarning.m new file mode 100644 index 00000000..ad0d6301 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/STDSWarning.m @@ -0,0 +1,30 @@ +// +// STDSWarning.m +// Stripe3DS2 +// +// Created by Cameron Sabol on 2/12/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSWarning.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation STDSWarning + +- (instancetype)initWithIdentifier:(NSString *)identifier + message:(NSString *)message + severity:(STDSWarningSeverity)severity { + self = [super init]; + if (self) { + _identifier = [identifier copy]; + _message = [message copy]; + _severity = severity; + } + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2/include/Stripe3DS2-Prefix.pch b/Stripe3DS2/Stripe3DS2/include/Stripe3DS2-Prefix.pch new file mode 100644 index 00000000..888096ff --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/Stripe3DS2-Prefix.pch @@ -0,0 +1,14 @@ +// +// Stripe3DS2-Prefix.pch +// Stripe3DS2 +// +// Created by Cameron Sabol on 4/16/20. +// Copyright © 2020 Stripe. All rights reserved. +// + +#ifndef Stripe3DS2_pch +#define Stripe3DS2_pch + +#import "STDSLocalizedString.h" + +#endif /* Stripe3DS2_pch */ diff --git a/Stripe3DS2/Stripe3DS2/include/Stripe3DS2.h b/Stripe3DS2/Stripe3DS2/include/Stripe3DS2.h new file mode 100644 index 00000000..9e9f3eba --- /dev/null +++ b/Stripe3DS2/Stripe3DS2/include/Stripe3DS2.h @@ -0,0 +1,52 @@ +// +// Stripe3DS2.h +// Stripe3DS2 +// +// Created by Cameron Sabol on 1/16/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +//! Project version number for Stripe3DS2. +FOUNDATION_EXPORT double Stripe3DS2VersionNumber; + +//! Project version string for Stripe3DS2. +FOUNDATION_EXPORT const unsigned char Stripe3DS2VersionString[]; + +#import "STDSConfigParameters.h" +#import "STDSThreeDS2Service.h" +#import "STDSUICustomization.h" +#import "STDSWarning.h" + +#import "STDSAlreadyInitializedException.h" +#import "STDSInvalidInputException.h" +#import "STDSNotInitializedException.h" +#import "STDSRuntimeException.h" + +#import "STDSErrorMessage.h" +#import "STDSProtocolErrorEvent.h" +#import "STDSRuntimeErrorEvent.h" +#import "STDSStripe3DS2Error.h" +#import "STDSThreeDSProtocolVersion.h" + +#import "STDSAuthenticationRequestParameters.h" +#import "STDSAuthenticationResponse.h" +#import "STDSChallengeParameters.h" +#import "STDSChallengeStatusReceiver.h" +#import "STDSCompletionEvent.h" +#import "STDSJSONDecodable.h" +#import "STDSJSONEncoder.h" +#import "STDSTransaction.h" + +#import "STDSButtonCustomization.h" +#import "STDSCustomization.h" +#import "STDSException.h" +#import "STDSFooterCustomization.h" +#import "STDSJSONEncodable.h" +#import "STDSLabelCustomization.h" +#import "STDSNavigationBarCustomization.h" +#import "STDSSelectionCustomization.h" +#import "STDSTextFieldCustomization.h" + +#import "STDSSwiftTryCatch.h" diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Info.plist b/Stripe3DS2/Stripe3DS2DemoUI/Info.plist new file mode 100644 index 00000000..80cd6052 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Resources/150-issuer.png b/Stripe3DS2/Stripe3DS2DemoUI/Resources/150-issuer.png new file mode 100644 index 0000000000000000000000000000000000000000..df12ebdabfa0cc654bb98a85a3d315f1043212c6 GIT binary patch literal 1135 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm2}o{Eezui?fyK9Q*k4 zK!6rgD5s&Lj#j4<_uv2j*~Ic{npvHeIJ$bRVC|~4wQFpyzAn^UA+<|DQZb*?4E$+Yqg-Ten`#F`It-g2~%Qj}Cp_6SjKm3=hri z+qdhU_EHi0|M>eyelxv?moHC#Y~iJ;=pu7`*0R{RI6a_E)-!!xecknXSAT#1)vVIg zpCwkZy>3^tLN!h0`gh-b_weDvQ%}Bp`BLtGK5g^qb*~Q_@L137du-9_^l&80>& z&lFhjG#x0M0}T42E-pp3X2;!k=be9EDB~Wevb01dJ#|s$taY#Vz3+EhtkJdX^2-Vv zGoHuiXJ0Jc8>44$V{_*D=l^ed{~vSv&-MM=H=Fajckc9D8Z<4HQ*qPHHx)Mf_U;Y! z(p7uyU2{KwYm}qFi(PSAQ;T-qIhFF!c$3cQZEyXSPxevkKDsDTV)Dr+j~%sIR$R@J z&!6YFy!&X<_S>=lrN1_Baa;WHa`giJn=hKD{No*ZIpsI2b!mFeyx=TFU_rv*>FVdQ I&MBb@0A?>9G5`Po literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Resources/150-payment.png b/Stripe3DS2/Stripe3DS2DemoUI/Resources/150-payment.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2c5dd15443e738afdd8bc127daf6b52c5d0772 GIT binary patch literal 1245 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm2}o{Eezui?fn|xOi(^Q|oVV8^{a^Vo9Q(LX zd&*i@M?uR6VuhYxe%G_GZbn|MX{_gzn)f_0}dK zded1ICyVW6_@$%Pa=hik3;{XeL&;7RJ{JIcU*&j(~fnwv$mRRoF z`SWLig@{wj;fFyhzkK*`;bloltiA}>!vYJ7W1sg#trgo7CZammZ~dAz9EwqE!&YCt zb^rc!m6zYXg#}L8y46&%<^AugS*5kLu^fTxuXi6^lsW6*ZSAREN)tVn1_34ZEeX&l zDJgkUdGG1d)RiHZOnf;m=9r1PZp^*?dC$*@U5jMoFMqq7xT6se_U8lt8?v_a+=QRWtn}) zAK%RR_L}Fj{jPP=oi0W*eRkg!b2_nU&iUv0d3jBhVd3G;hYvmcP+{|}HlE|+>#sVe zf1UoMG_hmd>$!fB3%*KA^{hl1N==IlyJCBsti^^~Q(ei%AGRd66iy0xIp^?*;U(Won z{$+{P{VzxKf8Q&ys;#TrmTN6>YEz7cNQ=O<)Lu8`*z5P}x>6&1-JAobMCg25&iP!$ z^VHL#^78Vw#G0#_bqnkD9$vmY`Sq@!HUFMIJ^KAGFr;(LCZ9~%fB(A4+qZMS_-()K zyENz^^XU>RVEC*Xy@3lk*T9$TcRr>|PYR(^FpP?x-Y9n1RW`{(?Y3)?LF7X~y*CjFm; z>c8C+xfH*B`*u%m{@FCy<3DeB)~*iN858$K>P4{Fe;2R$M}}SowP&37#{K0Qx0iar PVvE7k)z4*}Q$iB}f8I)R literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Resources/300-issuer.png b/Stripe3DS2/Stripe3DS2DemoUI/Resources/300-issuer.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f63b2d9b5d7e0434f8f8203403b7e57e86d6be GIT binary patch literal 2876 zcmeHJ`&ZK07T3Anve)F}j*?m2%!_N4sW4~_C0xCZuPG=iH5JH5YCgbM=m^M6mW`rv zUDTLqG7dSUC0brZMKmKMGjhsIk(bZRK=BDF5cfNO#;i3zoFDdCXP>?IXMaBXoZ{2y zpk3RYwp&@V|YZ_~D zSfG4b>a+O0`2KE-$SVQXKiMAmXW+FTK(=4R{bl@#F%Z4o`irfVp8%&N@q4sE?AJL6 zL2D~>aCQ-mhA=as59;kWbo_#4iW0$AYZ3ob__9H6jyiJt3L*ZLtf0vj0O2P?TUX%cl0a~#T~j;65vuk{cL)ff}~|-+0jK5 z715`9cQp;-EDnJ%n{k52y@v4xT>|V3`0d%~PHVn`3r&oVKe(SUgsdG%D1{D>-tWIn z;_oY}qw-(edWA}*5?~JWe=$RbRyR#ORC?Q|nW^7Aoo)yx0dy2U z78Vvtb6W;qp`v3!jk_lsn&h`KUM0XXvAn1z65?*8yKYKEpRWvLS3ut@?Z=VHTYtV2 z6XWjC>NrRHyuleskv@>;*3bwV{snPnSi{ZD$lCE2*lkJP)e<^=hhZ)O_C1AC6YTmM ze%I9}AbYxE6`ugEsr^95c+5^rOlUOj)>nsED11!Jz2W&ZpXz}GSk+4b{VJcWL)h+5j^2>PDDMW}44&{V$3vFLt!w~O6)ELI_xuNh20j)!jT_n~RG5*+lmss8-!Q4dBo zO$9C$V=5Jmz;ek89a2~RdfKI;#gj!K5HtsM`N=A+q>60c@o8pSG`+mAu)r$V93c3U z_zGh4CM8~(=`5Q(iaUboU86R$NS5y<)kmwVtA&$G8jZ#>H3GHM#pub;K86i=)LTs? z61C`8M{al^J3CN-tfs{blG%h4#l42WoxcQo5vAboNx3=Yrn>jKbG>Y#P?!MQW98Qw z^SM9PI2P{lR7EqJ04IGx#xsIF#xZ!Igx0HK?cH(p33~j;dWd6^upNBllMi_xj#Qw# z(DSH}$itXvvMm+CGao)Y&#VDjzPh^lmE0A67*wYSBeq3lH}`cZxW0}Yz3&(*uXa?u zp6I4t?}9>Zg`UDlL~f42z)CO8#OhNWd5Nuiit4Zt{Q+yCO*4nBWxaR9(^!F(*N%F6HeWVq(z#^&bc8at^t;Az3njZsZF zJYFWV1y~V>xhYLI0l_K<64D|7BE@**BKE2DLa+bnlZ+<4sx3-VkWIc6O;Q31dwlBi z`Y0Y?6QEh@SmZ=dq+F@26+rMf9CxDnKXbwxyFbR~r2um@tU-<~ zd!E=@A9h*iypJJHN=j-a{`ycL@a6NCe;6Pu$f#IQQCrkPs^eC1rcS3z+<_-4SEy#A zk<#Vt4NTyhq4m=1^?%p8!M{$eqsu!!bS-i)tWu~nO*b=>P@y#oBSl9_Vm$P^1z8~b zwN_76@s3W6QyV+a84|_G;UopfO42G{D@D)0s)N)`90#Y8D%kGpP%!N7J97x!H-M>}pymL!a zcq(w`w%WC-NYUW~OAjB)$^H|a2mJqCYHbO1v3mBCjm^Qoj6W3y8q`4i@;GV(Q49Qv OEkeFU2XOr_Uj09WsRnES literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Resources/300-payment.png b/Stripe3DS2/Stripe3DS2DemoUI/Resources/300-payment.png new file mode 100644 index 0000000000000000000000000000000000000000..a30a47d063a575c28ea3fcfe005a268546114f93 GIT binary patch literal 2828 zcmeHJjaSn58pr81a;}{1IyHx;W~%*O%(v@x z&YYpw%yEo?mLe)4`G&`9#+;X#A!0+sL_*^t3W4|6{)?Tn?>XP|eL3g*e4g*~c|Om1 z%1@zUK5%e%0D(Xs#KlG>fj~CUcN6>`aI@*@<_!Wl%)~_k{J=1El$q(-gZ zKGC`u`i%00>rGAyTc&-cCQ0;#{qqwO6Lt0kJP$pUP14^|hStU|ja7WSO|!17Q}&zdU8i;G}5Cf|!_?&UE1mdIdwYqi!kjb1paP6cN=*v(;eW1up;Z-MDg< zCQL8j%fxqD7^cIJ8oo?R5i({=8Z-l*NJbA5hr^i%TjJXngjD|LP{$D*WBXuZLOKQm zYnn=77tOx!o|XQ%c*ANjQ|;gKwdRCAG{?H7tUNgV_xm?B)@=op%f@%=U9Oq4n%{hS2AeU9h()y4VMg+)Z@;RSHWES~2D zcDQlkNPUplv24T(441Hzk8~ujE-(Lt=4?`gFNt?hEYIM@l=h@oTU*bq1fR7l;e)yz zi&4>!d8UV(*h=cXO0e;~yR(-l8{iRS!92#E*_0}z|Wl^a`E z!kH*mc~qu;D+h*#x&XUyWq>lSDuVQ@*iNsSK$(4Z`(#~~T z$WkZ@=1$*!bHT4}_sB0OdWw@>weooZkuQ_KO#`SeD=Vv;IIpkD?mo+w#``*tWnR`` zAI<&TpBLm<>IJDOsNeRTBar(sA?{TdMZ?W9BN$2BTHjEPPhF7Vc|qcz6MsE+?AUb9 z6%y(sYX(@cuK-xd4_z34t>+=Yb`bFS@~Gbz7c)Yd@-{D39&VVdjU6>w47f`nO>F4{ zjK40u79d5U@AY;Z=~C!qh?dXZy=(LW_aY#WOA4R)9*n<0&efTl)pI&kVgFr;dZU<@ zd)~V?73wmndNX8`XaWIVi0>HC|3%TsY`=!dM&_JU|2XquUn+DB5$|LtOxG%eW^ zlwtw-#7YzeII8v61aiopGIAVYP}dMzo#pd1yW2n!Ds%viQztu=N~IUL)QgA(G$K}R zq#2PMLWnG%PZcgu5}fQFrV=!(({JbsqeZS%6^6A)3O?y!U5{?NOs5M4&SJ~0bQcvKMBLIcHKt_Al0w7xQ zb+1(2N@{()O0Sk^PJUzljUV5h1aR`nOFVDYXcl}Gnb>-2g)9Jszp}Ct!woPxeQ0gN z`0vk3Vb|--I1vM=CAtM+H*O`sk#8>>z|8VQ0~eoUExgEo>RUH)qP1(V*Rk{o4R-^* zO&bId!8&9uk_S#?fRQzi$3F;Cl$qi`|9P*pL z(0OrKRk*F`6y-+-^MC}(`jjjf$&lO;g!y zDIN~NVX<>qs$ln}hHs_#BAEM)bD{24YhUJRES0-E_a^^WNuJ7=_pgq+I5_3?ls^8Bm%Yu^{c84N~#N$=HrW{Xv& zQc=F6qgA!G&3NHKk_L0;(~njS6xPf&@={}h__Bu?HjhngY!VclcFkHCq*EpPu8HsH z^?C)vTt&TE(=!{~8t7LWi@{))3j1?ltDz{UO9TQj*w^>`vB79EXl7<+QlZPCDEPao zwmH-@zPH$!8s3+i<3()g if3+-s`nK)ij?FZBuGQh}a2U|#g5thHMR8ALl>Q%GTH8wi literal 0 HcmV?d00001 diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Resources/450-issuer.png b/Stripe3DS2/Stripe3DS2DemoUI/Resources/450-issuer.png new file mode 100644 index 0000000000000000000000000000000000000000..136ec3ebd5a206860a141489a8528b101f28bf56 GIT binary patch literal 4455 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 k + + + + 3DS - One-Time Passcode - PA + + + + + + +
+ + + + +
+
+

Purchase Authentication

+

We have send you a text message with a code to your registered mobile number ending in ***.

+

You are paying Merchant ABC the amount of $xxx.xx on mm/dd/yy.

+
+ +
+

Enter your code below:

+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + Need some help? +

Help content will be displayed here.

+ +
+
+
+
+ + Learn more about authentication +

Authentication information will be displayed here.

+ +
+
+
+ + +
+ +
+
+ diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.h b/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.h new file mode 100644 index 00000000..0483838d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.h @@ -0,0 +1,15 @@ +// +// AppDelegate.h +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.m b/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.m new file mode 100644 index 00000000..0cf5fc19 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/AppDelegate.m @@ -0,0 +1,36 @@ +// +// AppDelegate.m +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +@import Stripe3DS2; + +#import "AppDelegate.h" +#import "STDSDemoViewController.h" +#import "STDSImageLoader.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + + STDSImageLoader *imageLoader = [[STDSImageLoader alloc] initWithURLSession:NSURLSession.sharedSession]; + STDSDemoViewController *demoViewController = [[STDSDemoViewController alloc] initWithImageLoader:imageLoader]; + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:demoViewController]; + + self.window.rootViewController = navigationController; + + [self.window makeKeyAndVisible]; + + return YES; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.h b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.h new file mode 100644 index 00000000..3a1ab053 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.h @@ -0,0 +1,23 @@ +// +// STDSChallengeResponseObject+TestObjects.h +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 3/7/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSChallengeResponseObject (TestObjects) + ++ (id)textChallengeResponseWithWhitelist:(BOOL)whitelist resendCode:(BOOL)resendCode; ++ (id)singleSelectChallengeResponse; ++ (id)multiSelectChallengeResponse; ++ (id)OOBChallengeResponse; ++ (id)HTMLChallengeResponse; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.m b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.m new file mode 100644 index 00000000..4e923545 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSChallengeResponseObject+TestObjects.m @@ -0,0 +1,198 @@ +// +// STDSChallengeResponseObject+TestObjects.m +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 3/7/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSChallengeResponseObject+TestObjects.h" +#import "STDSChallengeResponseSelectionInfoObject.h" +#import "STDSChallengeResponseImageObject.h" + +@implementation STDSChallengeResponseObject (TestObjects) + ++ (id)textChallengeResponseWithWhitelist:(BOOL)whitelist resendCode:(BOOL)resendCode { + return [[STDSChallengeResponseObject alloc] initWithThreeDSServerTransactionID:@"" + acsCounterACStoSDK:@"" + acsTransactionID:@"" + acsHTML:nil + acsHTMLRefresh:nil + acsUIType:STDSACSUITypeText + challengeCompletionIndicator:NO + challengeInfoHeader:@"Verify by phone" + challengeInfoLabel:@"Enter your 6 digit code:" + challengeInfoText:@"Great! We have sent you a text message with secure code to your registered mobile phone number.\n\nSent to a number ending in •••• •••• 4729." + challengeAdditionalInfoText:nil + showChallengeInfoTextIndicator:NO + challengeSelectInfo:nil + expandInfoLabel:@"Expand Info Label" + expandInfoText:@"This field displays expandable information text provided by the ACS." + issuerImage:[self issuerImage] + messageExtensions:nil + messageVersion:@"" + oobAppURL:nil + oobAppLabel:nil + oobContinueLabel:nil + paymentSystemImage:[self paymentImage] + resendInformationLabel:resendCode ? @"Resend code" : nil + sdkTransactionID:@"" + submitAuthenticationLabel:@"Submit" + whitelistingInfoText:whitelist ? @"Would you like to add this Merchant to your whitelist?" : nil + whyInfoLabel:@"Learn more about authentication" + whyInfoText:@"This is additional information about authentication. You are being provided extra information you wouldn't normally see, because you've tapped on the above label." + transactionStatus:nil]; +} + ++ (id)singleSelectChallengeResponse { + STDSChallengeResponseSelectionInfoObject *infoObject1 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Mobile" value:@"***-***-*321"]; + STDSChallengeResponseSelectionInfoObject *infoObject2 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Email" value:@"a******3@g****.com"]; + + return [[STDSChallengeResponseObject alloc] initWithThreeDSServerTransactionID:@"" + acsCounterACStoSDK:@"" + acsTransactionID:@"" + acsHTML:nil + acsHTMLRefresh:nil + acsUIType:STDSACSUITypeSingleSelect + challengeCompletionIndicator:NO + challengeInfoHeader:@"Payment Security" + challengeInfoLabel:nil + challengeInfoText:@"Hi Steve, your online payment is being secured using Card Network. Please select the location you would like to receive the code from YourBank." + challengeAdditionalInfoText:nil + showChallengeInfoTextIndicator:NO + challengeSelectInfo:@[infoObject1, infoObject2] + expandInfoLabel:@"Need some help?" + expandInfoText:@"You've indicated that you need help! We'd be happy to assist with that, by providing helpful text here that makes sense in context." + issuerImage:nil + messageExtensions:nil + messageVersion:@"" + oobAppURL:nil + oobAppLabel:nil + oobContinueLabel:nil + paymentSystemImage:nil + resendInformationLabel:nil + sdkTransactionID:@"" + submitAuthenticationLabel:@"Next" + whitelistingInfoText:nil + whyInfoLabel:@"Learn more about authentication" + whyInfoText:@"This is additional information about authentication. You are being provided extra information you wouldn't normally see, because you've tapped on the above label." + transactionStatus:nil]; +} + ++ (id)multiSelectChallengeResponse { + STDSChallengeResponseSelectionInfoObject *infoObject1 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Option1" value:@"Chicago, Illinois"]; + STDSChallengeResponseSelectionInfoObject *infoObject2 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Option2" value:@"Portland, Oregon"]; + STDSChallengeResponseSelectionInfoObject *infoObject3 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Option3" value:@"Dallas, Texas"]; + STDSChallengeResponseSelectionInfoObject *infoObject4 = [[STDSChallengeResponseSelectionInfoObject alloc] initWithName:@"Option4" value:@"St Louis, Missouri"]; + + return [[STDSChallengeResponseObject alloc] initWithThreeDSServerTransactionID:@"" + acsCounterACStoSDK:@"" + acsTransactionID:@"" + acsHTML:nil + acsHTMLRefresh:nil + acsUIType:STDSACSUITypeMultiSelect + challengeCompletionIndicator:NO + challengeInfoHeader:@"Payment Security" + challengeInfoLabel:@"Question 2: What cities have you lived in?" + challengeInfoText:@"Please answer 3 security questions from YourBank to complete your payment.\n\nSelect all that apply." + challengeAdditionalInfoText:nil + showChallengeInfoTextIndicator:NO + challengeSelectInfo:@[infoObject1, infoObject2, infoObject3, infoObject4] + expandInfoLabel:nil + expandInfoText:nil + issuerImage:nil + messageExtensions:nil + messageVersion:@"" + oobAppURL:nil + oobAppLabel:nil + oobContinueLabel:nil + paymentSystemImage:nil + resendInformationLabel:nil + sdkTransactionID:@"" + submitAuthenticationLabel:@"Next" + whitelistingInfoText:nil + whyInfoLabel:@"Learn more about authentication" + whyInfoText:@"This is additional information about authentication. You are being provided extra information you wouldn't normally see, because you've tapped on the above label." + transactionStatus:nil]; +} + ++ (id)OOBChallengeResponse { + return [[STDSChallengeResponseObject alloc] initWithThreeDSServerTransactionID:@"" + acsCounterACStoSDK:@"" + acsTransactionID:@"" + acsHTML:nil + acsHTMLRefresh:nil + acsUIType:STDSACSUITypeOOB + challengeCompletionIndicator:NO + challengeInfoHeader:@"Payment Security" + challengeInfoLabel:nil + challengeInfoText:@"For added security, you will be authenticated with YourBank application.\n\nStep 1 - Open your YourBank application directly from your phone and verify this payment.\n\nStep 2 - Tap continue after you have completed authentication with your YourBank application." + challengeAdditionalInfoText:nil + showChallengeInfoTextIndicator:YES + challengeSelectInfo:nil + expandInfoLabel:@"Need some help?" + expandInfoText:@"You've indicated that you need help! We'd be happy to assist with that, by providing helpful text here that makes sense in context." + issuerImage:[self issuerImage] + messageExtensions:nil + messageVersion:@"" + oobAppURL:nil + oobAppLabel:nil + oobContinueLabel:@"Continue" + paymentSystemImage:[self paymentImage] + resendInformationLabel:nil + sdkTransactionID:@"" + submitAuthenticationLabel:nil + whitelistingInfoText:nil + whyInfoLabel:@"Learn more about authentication" + whyInfoText:@"This is additional information about authentication. You are being provided extra information you wouldn't normally see, because you've tapped on the above label." + transactionStatus:nil]; +} + ++ (id)HTMLChallengeResponse { + NSString *htmlFilePath = [[NSBundle mainBundle] pathForResource:@"acs_challenge" ofType:@"html"]; + NSString *html = [NSString stringWithContentsOfFile:htmlFilePath encoding:NSUTF8StringEncoding error:nil]; + return [[STDSChallengeResponseObject alloc] initWithThreeDSServerTransactionID:@"" + acsCounterACStoSDK:@"" + acsTransactionID:@"" + acsHTML:html + acsHTMLRefresh:nil + acsUIType:STDSACSUITypeHTML + challengeCompletionIndicator:NO + challengeInfoHeader:nil + challengeInfoLabel:nil + challengeInfoText:nil + challengeAdditionalInfoText:nil + showChallengeInfoTextIndicator:NO + challengeSelectInfo:nil + expandInfoLabel:nil + expandInfoText:nil + issuerImage:nil + messageExtensions:nil + messageVersion:@"" + oobAppURL:nil + oobAppLabel:nil + oobContinueLabel:nil + paymentSystemImage:nil + resendInformationLabel:nil + sdkTransactionID:@"" + submitAuthenticationLabel:nil + whitelistingInfoText:nil + whyInfoLabel:nil + whyInfoText:nil + transactionStatus:nil]; +} + ++ (id)issuerImage { + return [[STDSChallengeResponseImageObject alloc] initWithMediumDensityURL: + [[NSBundle mainBundle] URLForResource:@"150-issuer" withExtension:@"png"] + highDensityURL:[[NSBundle mainBundle] URLForResource:@"300-issuer" withExtension:@"png"] + extraHighDensityURL:[[NSBundle mainBundle] URLForResource:@"450-issuer" withExtension:@"png"]]; +} + ++ (id)paymentImage { + return [[STDSChallengeResponseImageObject alloc] initWithMediumDensityURL:[[NSBundle mainBundle] URLForResource:@"150-payment" withExtension:@"png"] + highDensityURL:[[NSBundle mainBundle] URLForResource:@"300-payment" withExtension:@"png"] + extraHighDensityURL:[[NSBundle mainBundle] URLForResource:@"450-payment" withExtension:@"png"]]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.h b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.h new file mode 100644 index 00000000..db53f304 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.h @@ -0,0 +1,20 @@ +// +// STDSDemoViewController.h +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSImageLoader.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDemoViewController: UIViewController + +- (instancetype)initWithImageLoader:(STDSImageLoader *)imageLoader; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.m b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.m new file mode 100644 index 00000000..fa4ddeee --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/STDSDemoViewController.m @@ -0,0 +1,225 @@ +// +// STDSDemoViewController.m +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 3/11/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSDemoViewController.h" +#import "STDSChallengeResponseViewController.h" +#import "STDSChallengeResponseObject+TestObjects.h" +#import "STDSProgressViewController.h" +#import "STDSStackView.h" +#import "UIView+LayoutSupport.h" +#import "UIColor+ThirteenSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSDemoViewController () + +@property (nonatomic, strong) STDSImageLoader *imageLoader; +@property (nonatomic) BOOL shouldLoadSlowly; +@property (nonatomic) STDSUICustomization *customization; +@property (nonatomic) BOOL isDarkMode; + +@end + +@implementation STDSDemoViewController + +- (instancetype)initWithImageLoader:(STDSImageLoader *)imageLoader { + self = [super initWithNibName:nil bundle:nil]; + + if (self) { + _imageLoader = imageLoader; + _customization = [STDSUICustomization defaultSettings]; + } + + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor _stds_systemBackgroundColor]; + + STDSStackView *containerView = [[STDSStackView alloc] initWithAlignment:STDSStackViewLayoutAxisVertical]; + [self.view addSubview:containerView]; + [containerView _stds_pinToSuperviewBounds]; + + NSDictionary *buttonTitleToSelectorMapping = @{ + @"Toggle Dark Mode": NSStringFromSelector(@selector(toggleDarkMode)), + @"Present Text Challenge": NSStringFromSelector(@selector(presentTextChallenge)), + @"Present Text Challenge With Whitelist": NSStringFromSelector(@selector(presentTextChallengeWithWhitelist)), + @"Present Text Challenge With Resend": NSStringFromSelector(@selector(presentTextChallengeWithResendCode)), + @"Present Text Challenge With Whitelist and Resend": NSStringFromSelector(@selector(presentTextChallengeWithResendCodeAndWhitelist)), + @"Present Text Challenge (loads slowly w/ initial progressView)": NSStringFromSelector(@selector(presentTextChallengeLoadsSlowly)), + @"Present Single Select Challenge": NSStringFromSelector(@selector(presentSingleSelectChallenge)), + @"Present Multi Select Challenge": NSStringFromSelector(@selector(presentMultiSelectChallenge)), + @"Present OOB Challenge": NSStringFromSelector(@selector(presentOOBChallenge)), + @"Present HTML Challenge": NSStringFromSelector(@selector(presentHTMLChallenge)), + @"Present Progress View": NSStringFromSelector(@selector(presentProgressView)), + }; + for (NSString *key in [buttonTitleToSelectorMapping keysSortedByValueUsingComparator:^(NSString *a, NSString *b) { + return [a compare:b]; + }]) { + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button addTarget:self action:NSSelectorFromString(buttonTitleToSelectorMapping[key]) forControlEvents:UIControlEventTouchUpInside]; + button.titleLabel.numberOfLines = 0; + button.titleLabel.textAlignment = NSTextAlignmentCenter; + [button setTitle:key forState:UIControlStateNormal]; + [containerView addArrangedSubview:button]; + } + [containerView addArrangedSubview:[UIView new]]; +} + +- (void)toggleDarkMode { + if (self.isDarkMode) { + self.customization = [STDSUICustomization defaultSettings]; + self.isDarkMode = false; + } else { + self.customization = [STDSUICustomization defaultSettings]; + // Navigation bar + self.customization.navigationBarCustomization = [STDSNavigationBarCustomization new]; + self.customization.navigationBarCustomization.headerText = @"Authentication"; + self.customization.navigationBarCustomization.buttonText = @"Nope"; + self.customization.navigationBarCustomization.textColor = UIColor.whiteColor; + self.customization.navigationBarCustomization.barStyle = UIBarStyleBlack; + + // General + self.customization.backgroundColor = UIColor.blackColor; + self.customization.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; + self.customization.preferredStatusBarStyle = UIStatusBarStyleLightContent; + self.customization.footerCustomization = [STDSFooterCustomization new]; + self.customization.footerCustomization.backgroundColor = [UIColor colorWithRed:.08 green:.08 blue:.08 alpha:1]; + self.customization.footerCustomization.headingFont = [UIFont boldSystemFontOfSize:15]; + self.customization.footerCustomization.headingTextColor = [UIColor colorWithRed:.95 green:.95 blue:.95 alpha:1]; + self.customization.footerCustomization.textColor = [UIColor colorWithRed:.90 green:.90 blue:.90 alpha:1]; + + // Cancel button + STDSButtonCustomization *cancelButtonCustomization = [STDSButtonCustomization defaultSettingsForButtonType:STDSUICustomizationButtonTypeCancel]; + cancelButtonCustomization.textColor = UIColor.grayColor; + cancelButtonCustomization.titleStyle = STDSButtonTitleStyleUppercase; + [self.customization setButtonCustomization:cancelButtonCustomization forType:STDSUICustomizationButtonTypeCancel]; + + // Text + self.customization.labelCustomization.headingTextColor = UIColor.whiteColor; + self.customization.labelCustomization.textColor = UIColor.whiteColor; + + // Text field + self.customization.textFieldCustomization.keyboardAppearance = UIKeyboardAppearanceDark; + self.customization.textFieldCustomization.textColor = UIColor.whiteColor; + self.customization.textFieldCustomization.borderColor = UIColor.whiteColor; + + // Radio/Checkbox + self.customization.selectionCustomization.secondarySelectedColor = UIColor.lightGrayColor; + self.customization.selectionCustomization.unselectedBorderColor = UIColor.blackColor; + self.customization.selectionCustomization.unselectedBackgroundColor = UIColor.darkGrayColor; + + self.isDarkMode = true; + } +} + +- (void)presentTextChallenge { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:NO resendCode:NO]]; +} + +- (void)presentTextChallengeWithWhitelist { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:YES resendCode:NO]]; +} + +- (void)presentTextChallengeWithResendCode { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:NO resendCode:YES]]; +} + +- (void)presentTextChallengeWithResendCodeAndWhitelist { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:YES resendCode:YES]]; +} + +- (void)presentTextChallengeLoadsSlowly { + self.shouldLoadSlowly = YES; + STDSProgressViewController *progressVC = [[STDSProgressViewController alloc] initWithDirectoryServer:STDSDirectoryServerULTestEC uiCustomization:self.customization didCancel:^{}]; + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:progressVC]; + [self.navigationController presentViewController:navigationController animated:YES completion:nil]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:NO resendCode:NO]]; + }); + +} + +- (void)presentSingleSelectChallenge { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject singleSelectChallengeResponse]]; +} + +- (void)presentMultiSelectChallenge { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject multiSelectChallengeResponse]]; +} + +- (void)presentOOBChallenge { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject OOBChallengeResponse]]; +} + +- (void)presentHTMLChallenge { + [self presentChallengeForChallengeResponse:[STDSChallengeResponseObject HTMLChallengeResponse]]; +} + +- (void)presentProgressView { + __weak typeof(self) weakSelf = self; + UIViewController *vc = [[STDSProgressViewController alloc] initWithDirectoryServer:STDSDirectoryServerULTestEC uiCustomization:self.customization didCancel:^{ + [weakSelf dismissViewControllerAnimated:YES completion:nil]; + }]; + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + [self.navigationController presentViewController:navigationController animated:YES completion:nil]; +} + +- (void)presentChallengeForChallengeResponse:(id)challengeResponse { + STDSChallengeResponseViewController *challengeResponseViewController = [[STDSChallengeResponseViewController alloc] initWithUICustomization:self.customization imageLoader:self.imageLoader directoryServer:STDSDirectoryServerULTestEC]; + challengeResponseViewController.delegate = self; + + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:challengeResponseViewController]; + [self.navigationController presentViewController:navigationController animated:YES completion:nil]; + // Simulate what `STDSTransaction` does + [challengeResponseViewController setLoading]; + NSUInteger delay = self.shouldLoadSlowly ? 5 : 0; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [challengeResponseViewController setChallengeResponse:challengeResponse animated:YES]; + }); +} + +#pragma mark - STDSChallengeResponseViewControllerDelegate + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitHTMLForm:(nonnull NSString *)form { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitInput:(nonnull NSString *)userInput whitelistSelection:(nonnull id)whitelistSelection { + [viewController setLoading]; + NSUInteger delay = self.shouldLoadSlowly ? 5 : 0; + self.shouldLoadSlowly = NO; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [viewController setChallengeResponse:[STDSChallengeResponseObject OOBChallengeResponse] animated:YES]; + }); +} + +- (void)challengeResponseViewController:(nonnull STDSChallengeResponseViewController *)viewController didSubmitSelection:(nonnull NSArray> *)selection whitelistSelection:(nonnull id)whitelistSelection { + [viewController setLoading]; + [viewController setChallengeResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:YES resendCode:YES] animated:YES]; +} + +- (void)challengeResponseViewControllerDidCancel:(nonnull STDSChallengeResponseViewController *)viewController { + self.shouldLoadSlowly = NO; + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)challengeResponseViewControllerDidOOBContinue:(nonnull STDSChallengeResponseViewController *)viewController whitelistSelection:(nonnull id)whitelistSelection { + [viewController setLoading]; + [viewController setChallengeResponse:[STDSChallengeResponseObject singleSelectChallengeResponse] animated:YES]; +} + +- (void)challengeResponseViewControllerDidRequestResend:(nonnull STDSChallengeResponseViewController *)viewController { +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2DemoUI/Sources/main.m b/Stripe3DS2/Stripe3DS2DemoUI/Sources/main.m new file mode 100644 index 00000000..ca445d07 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUI/Sources/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Stripe3DS2DemoUI +// +// Created by Andrew Harrison on 2/27/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Stripe3DS2/Stripe3DS2DemoUITests/Info.plist b/Stripe3DS2/Stripe3DS2DemoUITests/Info.plist new file mode 100644 index 00000000..afb93bf7 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchStoryboardName + LaunchScreen + + diff --git a/Stripe3DS2/Stripe3DS2DemoUITests/STDSChallengeResponseViewControllerSnapshotTests.m b/Stripe3DS2/Stripe3DS2DemoUITests/STDSChallengeResponseViewControllerSnapshotTests.m new file mode 100644 index 00000000..94904843 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2DemoUITests/STDSChallengeResponseViewControllerSnapshotTests.m @@ -0,0 +1,117 @@ +// +// STDSChallengeResponseViewControllerSnapshotTests.m +// Stripe3DS2DemoUITests +// +// Created by Andrew Harrison on 3/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +@import iOSSnapshotTestCaseCore; + +#import + +#import "STDSChallengeResponseViewController.h" +#import "STDSChallengeResponseObject+TestObjects.h" + +/** + Calls FBSnapshotVerifyView with a default 2% per-pixel color differentiation, as M1 and Intel machines render shadows differently. + @param view The view to snapshot. + @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method. + */ +#define STPSnapshotVerifyView(view__, identifier__) \ +FBSnapshotVerifyViewWithPixelOptions(view__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0.02, 0) + +@interface STDSChallengeResponseViewControllerSnapshotTests: FBSnapshotTestCase + +@end + +@implementation STDSChallengeResponseViewControllerSnapshotTests + +- (void)setUp { + [super setUp]; + + /// Recorded on an iPhone 12 Mini running iOS 15.4 +// self.recordMode = YES; +} + +- (void)testVerifyTextChallengeDesign { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:[STDSChallengeResponseObject textChallengeResponseWithWhitelist:NO resendCode:NO] directoryServer:STDSDirectoryServerCustom]; + [challengeResponseViewController view]; + + [self waitForChallengeResponseTimer]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"TextChallengeResponse"); +} + +- (void)testVerifySingleSelectDesign { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:[STDSChallengeResponseObject singleSelectChallengeResponse] directoryServer:STDSDirectoryServerCustom]; + [challengeResponseViewController view]; + + [self waitForChallengeResponseTimer]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"SingleSelectResponse"); +} + +- (void)testVerifyMultiSelectDesign { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:[STDSChallengeResponseObject multiSelectChallengeResponse] directoryServer:STDSDirectoryServerCustom]; + [challengeResponseViewController view]; + + [self waitForChallengeResponseTimer]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"MultiSelectResponse"); +} + +- (void)testVerifyOOBDesign { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:[STDSChallengeResponseObject OOBChallengeResponse] directoryServer:STDSDirectoryServerCustom]; + [challengeResponseViewController view]; + + [self waitForChallengeResponseTimer]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"OOBResponse"); +} + +- (void)testLoadingAmex { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:nil directoryServer:STDSDirectoryServerAmex]; + [challengeResponseViewController view]; + [challengeResponseViewController setLoading]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"LoadingAmex"); +} + +- (void)testLoadingDiscover { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:nil directoryServer:STDSDirectoryServerDiscover]; + [challengeResponseViewController view]; + [challengeResponseViewController setLoading]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"LoadingDiscover"); +} + +- (void)testLoadingMastercard { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:nil directoryServer:STDSDirectoryServerMastercard]; + [challengeResponseViewController view]; + [challengeResponseViewController setLoading]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"LoadingMastercard"); +} + +- (void)testLoadingVisa { + STDSChallengeResponseViewController *challengeResponseViewController = [self challengeResponseViewControllerForResponse:nil directoryServer:STDSDirectoryServerVisa]; + [challengeResponseViewController view]; + [challengeResponseViewController setLoading]; + + STPSnapshotVerifyView(challengeResponseViewController.view, @"LoadingVisa"); +} + +- (STDSChallengeResponseViewController *)challengeResponseViewControllerForResponse:(id)response directoryServer:(STDSDirectoryServer)directoryServer { + STDSImageLoader *imageLoader = [[STDSImageLoader alloc] initWithURLSession:NSURLSession.sharedSession]; + + STDSChallengeResponseViewController *vc = [[STDSChallengeResponseViewController alloc] initWithUICustomization:[STDSUICustomization defaultSettings] imageLoader:imageLoader directoryServer:directoryServer]; + [vc setChallengeResponse:response animated:NO]; + return vc; +} + +- (void)waitForChallengeResponseTimer { + (void)[XCTWaiter waitForExpectations:@[[self expectationWithDescription:@""]] timeout:2.5]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Resources/Info.plist b/Stripe3DS2/Stripe3DS2Resources/Info.plist new file mode 100644 index 00000000..7607d027 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Resources/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSHumanReadableCopyright + Copyright © 2020 Stripe. All rights reserved. + NSPrincipalClass + + + diff --git a/Stripe3DS2/Stripe3DS2Tests/Info.plist b/Stripe3DS2/Stripe3DS2Tests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Stripe3DS2/Stripe3DS2Tests/JSON/ARes.json b/Stripe3DS2/Stripe3DS2Tests/JSON/ARes.json new file mode 100644 index 00000000..a1634721 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/JSON/ARes.json @@ -0,0 +1,16 @@ +{ + "dsTransID": "4e4750e7-6ab5-45a4-accf-9c668ed3b5a7", + "acsTransID": "fa695a82-a48c-455d-9566-a652058dda27", + "p_messageVersion": "1.0.5", + "acsOperatorID": "acsOperatorUL", + "sdkTransID": "D77EB83F-F317-4E29-9852-EBAAB55515B7", + "eci": "00", + "dsReferenceNumber": "3DS_LOA_DIS_PPFU_020100_00010", + "acsReferenceNumber": "3DS_LOA_ACS_PPFU_020100_00009", + "threeDSServerTransID": "fc7a39de-dc41-4b65-ba76-a322769b2efc", + "messageVersion": "2.1.0", + "authenticationValue": "AABBCCDDEEFFAABBCCDDEEFFAAA=", + "messageType": "pArs", + "transStatus": "C", + "acsChallengeMandated": "NO" +} diff --git a/Stripe3DS2/Stripe3DS2Tests/JSON/CRes.json b/Stripe3DS2/Stripe3DS2Tests/JSON/CRes.json new file mode 100644 index 00000000..98895661 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/JSON/CRes.json @@ -0,0 +1,31 @@ +{ + "threeDSServerTransID": "8a880dc0-d2d2-4067-bcb1-b08d1690b26e", + "acsTransID": "d7c1ee99-9478-44a6-b1f2-391e29c6b340", + "acsUiType": "01", + "challengeAddInfo": "Additional information to be shown.", + "challengeCompletionInd": "N", + "challengeInfoHeader": "Header information", + "challengeInfoLabel": "One-time-password", + "challengeInfoText": "Please enter the received one-time-password", + "challengeInfoTextIndicator": "N", + "expandInfoLabel": "Additional instructions", + "expandInfoText": "The issuer will send you via SMS a one-time password. Please enter the value in the designated input field above and press continue to complete the 3-D Secure authentication process.", + "issuerImage": { + "medium": "https://acs.com/medium_image.svg", + "high": "https://acs.com/high_image.svg", + "extraHigh": "https://acs.com/extraHigh_image.svg" + }, + "messageType": "CRes", + "messageVersion": "2.1.0", + "psImage": { + "medium": "https://ds.com/medium_image.svg", + "high": "https://ds.com/high_image.svg", + "extraHigh": "https://ds.com/extraHigh_image.svg" + }, + "resendInformationLabel": "Send new One-time-password", + "sdkTransID": "b2385523-a66c-4907-ac3c-91848e8c0067", + "submitAuthenticationLabel": "Continue", + "whyInfoLabel": "Why using 3-D Secure?", + "whyInfoText": "Some explanation about why using 3-D Secure is an excellent idea as part of an online payment transaction", + "acsCounterAtoS": "001" +} diff --git a/Stripe3DS2/Stripe3DS2Tests/JSON/ErrorMessage.json b/Stripe3DS2/Stripe3DS2Tests/JSON/ErrorMessage.json new file mode 100644 index 00000000..6acd63f1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/JSON/ErrorMessage.json @@ -0,0 +1,13 @@ +{ + "threeDSServerTransID": "6afa6072-9412-446b-9673-2f98b3ee98a2", + "acsTransID": "375d90ad-3873-498b-9133-380cbbc8d99d", + "dsTransID": "0b470d4f-fdf8-429f-9147-505b1a589883", + "errorCode": "203", + "errorComponent": "A", + "errorDescription": "Data element not in the required format. Not numeric or wrong length.", + "errorDetail": "billAddrCountry,billAddrPostCode,dsURL", + "errorMessageType": "AReq", + "messageType": "Erro", + "messageVersion": "2.1.0", + "sdkTransID": "b2385523-a66c-4907-ac3c-91848e8c0067" +} diff --git a/Stripe3DS2/Stripe3DS2Tests/NSDictionary+DecodingHelpersTest.m b/Stripe3DS2/Stripe3DS2Tests/NSDictionary+DecodingHelpersTest.m new file mode 100644 index 00000000..846c5268 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/NSDictionary+DecodingHelpersTest.m @@ -0,0 +1,312 @@ +// +// NSDictionary+DecodingHelpersTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSDictionary+DecodingHelpers.h" +#import "STDSStripe3DS2Error.h" +#import "NSError+Stripe3DS2.h" + +@interface JSONDecodableTestObject : NSObject +@property (nonatomic, copy) NSString *value; +@end + +@implementation JSONDecodableTestObject + ++ (instancetype)decodedObjectFromJSON:(NSDictionary *)json error:(NSError * _Nullable __autoreleasing *)outError { + NSString *value = [json _stds_stringForKey:@"key" required:YES error:outError]; + if (outError && *outError) { + return nil; + } + JSONDecodableTestObject *obj = [[self alloc] init]; + obj.value = value; + return obj; +} + +@end + +@interface NSDictionary_DecodingHelpersTest : XCTestCase + +@end + +@implementation NSDictionary_DecodingHelpersTest + +- (void)testMissingRequiredKey { + // Every getter should fail the same way if the key is not present + NSDictionary *json = @{}; + NSError *expectedError = [NSError _stds_missingJSONFieldError:@"key"]; + NSError *error; + id value; + + value = [json _stds_stringForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_stringForKey:@"key" validator:^BOOL (NSString *value) { + return NO; + } required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_boolForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_arrayForKey:@"key" arrayElementType:[JSONDecodableTestObject class] required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_urlForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_dictionaryForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); +} + +- (void)testInvalidType { + // Every getter should fail the same way if the value is not the expected type + NSDictionary *json = @{@"key": [NSObject new]}; + NSError *expectedError = [NSError _stds_invalidJSONFieldError:@"key"]; + NSError *error; + id value; + + value = [json _stds_stringForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_stringForKey:@"key" validator:^BOOL (NSString *value) { + return NO; + } required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_boolForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_arrayForKey:@"key" arrayElementType:[JSONDecodableTestObject class] required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_urlForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + value = [json _stds_dictionaryForKey:@"key" required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); +} + +#pragma mark NSString + +- (void)testString { + NSDictionary *json = [self _basicJSONDictionary]; + NSError *error; + NSString *value = [json _stds_stringForKey:@"key" required:YES error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(value); + XCTAssertEqualObjects(value, @"value"); +} + +- (void)testEmptyString { + NSDictionary *json = @{@"key": @""}; + NSError *expectedError = [NSError _stds_missingJSONFieldError:@"key"]; + NSError *error; + id value; + + // Required empty string should produce a missing error + value = [json _stds_stringForKey:@"key" required:YES error:&error]; + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); + + // Not required empty string should produce an invalid error + expectedError = [NSError _stds_invalidJSONFieldError:@"key"]; + value = [json _stds_stringForKey:@"key" required:NO error:&error]; + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); +} + +- (void)testInvalidValueString { + NSDictionary *json = [self _basicJSONDictionary]; + NSError *expectedError = [NSError _stds_invalidJSONFieldError:@"key"]; + NSError *error; + NSString *value = [json _stds_stringForKey:@"key" validator:^BOOL (NSString *value) { + return NO; + } required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); +} + +#pragma mark NSArray + +- (void)testArray { + NSDictionary *json = @{ + @"key": @[@{@"key": @"value"}] + }; + NSError *error; + NSArray *array = [json _stds_arrayForKey:@"key" arrayElementType:[JSONDecodableTestObject class] required:YES error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(array); + if (array.count == 1) { + XCTAssertEqualObjects([array[0] class], [JSONDecodableTestObject class]); + XCTAssertEqualObjects(array[0].value, @"value"); + } else { + XCTFail(@"Array was not populated"); + } +} + +- (void)testInvalidElementTypeArray { + NSDictionary *json = @{ + @"key": @[@"value1", @"value2"] + }; + NSError *expectedError = [NSError _stds_invalidJSONFieldError:@"key"]; + NSError *error; + NSArray *value = [json _stds_arrayForKey:@"key" arrayElementType:[JSONDecodableTestObject class] required:YES error:&error]; + XCTAssertNotNil(error); + if (error) { + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } else { + XCTFail(@"Error should have a value"); + } + XCTAssertNil(value); +} + +#pragma mark NSDictionary + +- (void)testDictionary { + NSDictionary *nestedJSON = [self _basicJSONDictionary]; + NSDictionary *json = @{ + @"key": nestedJSON + }; + NSError *error; + NSDictionary *value = [json _stds_dictionaryForKey:@"key" required:YES error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(value); + XCTAssertEqualObjects(value, nestedJSON); +} + +#pragma mark NSURL + +- (void)testURL { + NSDictionary *json = @{@"key": @"www.stripe.com"}; + NSError *error; + NSURL *value = [json _stds_urlForKey:@"key" required:YES error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(value); + XCTAssertEqualObjects(value, [NSURL URLWithString:@"www.stripe.com"]); +} + +#pragma mark BOOL + +- (void)testBOOL { + NSDictionary *json = @{@"key": @(YES)}; + NSError *error; + BOOL value = [json _stds_boolForKey:@"key" required:YES error:&error]; + XCTAssertNil(error); + XCTAssertEqual(value, YES); +} + +#pragma mark Helpers + +- (NSDictionary *)_basicJSONDictionary { + return @{@"key": @"value"}; +} + + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/NSString+EmptyCheckingTests.m b/Stripe3DS2/Stripe3DS2Tests/NSString+EmptyCheckingTests.m new file mode 100644 index 00000000..49024547 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/NSString+EmptyCheckingTests.m @@ -0,0 +1,32 @@ +// +// NSString+EmptyCheckingTests.m +// Stripe3DS2Tests +// +// Created by Andrew Harrison on 3/4/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSString+EmptyChecking.h" + +@interface NSString_EmptyCheckingTests : XCTestCase + +@end + +@implementation NSString_EmptyCheckingTests + +- (void)testStringIsEmpty { + XCTAssertTrue([NSString _stds_isStringEmpty:@""]); + XCTAssertTrue([NSString _stds_isStringEmpty:@" "]); + XCTAssertTrue([NSString _stds_isStringEmpty:@"\n"]); + XCTAssertTrue([NSString _stds_isStringEmpty:@"\t"]); +} + +- (void)testStringIsNotEmpty { + XCTAssertFalse([NSString _stds_isStringEmpty:@"Hello"]); + XCTAssertFalse([NSString _stds_isStringEmpty:@","]); + XCTAssertFalse([NSString _stds_isStringEmpty:@"\\n"]); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSACSNetworkingManagerTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSACSNetworkingManagerTest.m new file mode 100644 index 00000000..33fca13f --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSACSNetworkingManagerTest.m @@ -0,0 +1,54 @@ +// +// STDSACSNetworkingManagerTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 4/18/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSStripe3DS2Error.h" +#import "STDSACSNetworkingManager.h" +#import "STDSTestJSONUtils.h" +#import "STDSErrorMessage.h" +#import "STDSChallengeResponseObject.h" + +@interface STDSACSNetworkingManager (Private) +- (nullable id)decodeJSON:(NSDictionary *)dict error:(NSError * _Nullable *)outError; +@end + +@interface STDSACSNetworkingManagerTest : XCTestCase + +@end + +@implementation STDSACSNetworkingManagerTest + +- (void)testDecodeJSON { + STDSACSNetworkingManager *manager = [[STDSACSNetworkingManager alloc] init]; + NSError *error; + id decoded; + + // Unknown message type + NSDictionary *unknownMessageDict = @{@"messageType": @"foo"}; + decoded = [manager decodeJSON:unknownMessageDict error:&error]; + XCTAssertEqual(error.code, STDSErrorCodeUnknownMessageType); + XCTAssertNil(decoded); + error = nil; + + // Error Message type + NSDictionary *errorMessageDict = [STDSTestJSONUtils jsonNamed:@"ErrorMessage"]; + decoded = [manager decodeJSON:errorMessageDict error:&error]; + XCTAssertEqual(error.code, STDSErrorCodeReceivedErrorMessage); + XCTAssertTrue([error.userInfo[STDSStripe3DS2ErrorMessageErrorKey] isKindOfClass:[STDSErrorMessage class]]); + XCTAssertNil(decoded); + error = nil; + + // ChallengeResponse message type + NSDictionary *challengeResponseDict = [STDSTestJSONUtils jsonNamed:@"CRes"]; + decoded = [manager decodeJSON:challengeResponseDict error:&error]; + XCTAssertNil(error); + XCTAssertTrue([decoded isKindOfClass:[STDSChallengeResponseObject class]]); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationRequestParametersTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationRequestParametersTest.m new file mode 100644 index 00000000..7e701da8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationRequestParametersTest.m @@ -0,0 +1,41 @@ +// +// STDSAuthenticationRequestParametersTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSAuthenticationRequestParameters.h" +#import "STDSJSONEncoder.h" + +@interface STDSAuthenticationRequestParametersTest : XCTestCase + +@end + +@implementation STDSAuthenticationRequestParametersTest + +#pragma mark - STDSJSONEncodable + +- (void)testPropertyNamesToJSONKeysMapping { + STDSAuthenticationRequestParameters *params = [STDSAuthenticationRequestParameters new]; + + NSDictionary *mapping = [STDSAuthenticationRequestParameters propertyNamesToJSONKeysMapping]; + + for (NSString *propertyName in [mapping allKeys]) { + XCTAssertFalse([propertyName containsString:@":"]); + XCTAssert([params respondsToSelector:NSSelectorFromString(propertyName)]); + } + + for (NSString *formFieldName in [mapping allValues]) { + XCTAssert([formFieldName isKindOfClass:[NSString class]]); + XCTAssert([formFieldName length] > 0); + } + + XCTAssertEqual([[mapping allValues] count], [[NSSet setWithArray:[mapping allValues]] count]); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:[STDSJSONEncoder dictionaryForObject:params]]); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationResponseTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationResponseTests.m new file mode 100644 index 00000000..7dac3fd1 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSAuthenticationResponseTests.m @@ -0,0 +1,32 @@ +// +// STDSAuthenticationResponseTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 5/20/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSAuthenticationResponseObject.h" +#import "STDSTestJSONUtils.h" + +@interface STDSAuthenticationResponseTests : XCTestCase + +@end + +@implementation STDSAuthenticationResponseTests + +- (void)testInitWithJSON { + NSError *error = nil; + STDSAuthenticationResponseObject *ares = [STDSAuthenticationResponseObject decodedObjectFromJSON:[STDSTestJSONUtils jsonNamed:@"ARes"] error:&error]; + + XCTAssertNil(error); + XCTAssertNotNil(ares, @"Failed to create an ares parsed from JSON"); + + id authResponse = STDSAuthenticationResponseFromJSON([STDSTestJSONUtils jsonNamed:@"ARes"]); + XCTAssertNotNil(authResponse, @"Failed to create an ares parsed from JSON"); + XCTAssert(authResponse.isChallengeRequired, @"ares did not indicate that a challenge was required"); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSBase64URLEncodingTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSBase64URLEncodingTests.m new file mode 100644 index 00000000..25642a5d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSBase64URLEncodingTests.m @@ -0,0 +1,59 @@ +// +// STDSBase64URLEncodingTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSData+JWEHelpers.h" +#import "NSString+JWEHelpers.h" + +@interface STDSBase64URLEncodingTests : XCTestCase + +@end + +@implementation STDSBase64URLEncodingTests + +// test cases from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41 + +- (void)testEncodingDataToString { + { + Byte bytes[5] = {3, 236, 255, 224, 193}; + NSData *data = [NSData dataWithBytes:bytes length:5]; + XCTAssertEqualObjects([data _stds_base64URLEncodedString], @"A-z_4ME"); + } + + { + Byte bytes[30] = {123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125}; + NSData *data = [NSData dataWithBytes:bytes length:30]; + XCTAssertEqualObjects([data _stds_base64URLEncodedString], @"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"); + } + + { + Byte bytes[70] = {123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, + 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, + 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, + 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, + 111, 116, 34, 58, 116, 114, 117, 101, 125}; + NSData *data = [NSData dataWithBytes:bytes length:70]; + XCTAssertEqualObjects([data _stds_base64URLEncodedString], @"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"); + } + +} + +- (void)testEncodingString { + XCTAssertEqualObjects([@"{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" _stds_base64URLEncodedString], @"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"); + + XCTAssertEqualObjects([@"{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}" _stds_base64URLEncodedString], @"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"); +} + +- (void)testDecodingString { + XCTAssertEqualObjects([@"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" _stds_base64URLDecodedString], @"{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}"); + + XCTAssertEqualObjects([ @"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" _stds_base64URLDecodedString], @"{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}"); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSChallengeParametersTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeParametersTests.m new file mode 100644 index 00000000..8b430d10 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeParametersTests.m @@ -0,0 +1,56 @@ +// +// STDSChallengeParametersTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 2/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSAuthenticationResponseObject.h" +#import "STDSChallengeParameters.h" + +@interface TestAuthResponse: STDSAuthenticationResponseObject + +@end + +@interface STDSChallengeParametersTests : XCTestCase + +@end + +@implementation STDSChallengeParametersTests + +- (void)testInitWithAuthResponse { + STDSChallengeParameters *params = [[STDSChallengeParameters alloc] initWithAuthenticationResponse:[[TestAuthResponse alloc] init]]; + + XCTAssertEqual(params.threeDSServerTransactionID, @"test_threeDSServerTransactionID", @"Failed to set test_threeDSServerTransactionID"); + XCTAssertEqual(params.acsTransactionID, @"test_acsTransactionID", @"Failed to set test_acsTransactionID"); + XCTAssertEqual(params.acsReferenceNumber, @"test_acsReferenceNumber", @"Failed to set test_acsReferenceNumber"); + XCTAssertEqual(params.acsSignedContent, @"test_acsSignedContent", @"Failed to set test_acsSignedContent"); + XCTAssertNil(params.threeDSRequestorAppURL, @"Should not have set threeDSRequestorAppURL"); +} + +@end + +#pragma mark - TestAuthResponse + +@implementation TestAuthResponse + +- (NSString *)threeDSServerTransactionID { + return @"test_threeDSServerTransactionID"; +} + +- (NSString *)acsTransactionID { + return @"test_acsTransactionID"; +} + +- (NSString *)acsReferenceNumber { + return @"test_acsReferenceNumber"; +} + +- (NSString *)acsSignedContent { + return @"test_acsSignedContent"; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSChallengeRequestParametersTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeRequestParametersTest.m new file mode 100644 index 00000000..e97b030e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeRequestParametersTest.m @@ -0,0 +1,71 @@ +// +// STDSChallengeRequestParametersTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 4/1/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSChallengeRequestParameters.h" +#import "STDSJSONEncoder.h" + +@interface STDSChallengeRequestParametersTest : XCTestCase + +@end + +@implementation STDSChallengeRequestParametersTest + +#pragma mark - STDSJSONEncodable + +- (void)testPropertyNamesToJSONKeysMapping { + STDSChallengeRequestParameters *params = [[STDSChallengeRequestParameters alloc] initWithThreeDSServerTransactionIdentifier:@"server id" + acsTransactionIdentifier:@"acs id" + messageVersion:@"message version" + sdkTransactionIdentifier:@"sdk id" + requestorAppUrl:@"requestor app url" + sdkCounterStoA:0]; + + NSDictionary *mapping = [STDSChallengeRequestParameters propertyNamesToJSONKeysMapping]; + + for (NSString *propertyName in [mapping allKeys]) { + XCTAssertFalse([propertyName containsString:@":"]); + XCTAssert([params respondsToSelector:NSSelectorFromString(propertyName)]); + } + + for (NSString *formFieldName in [mapping allValues]) { + XCTAssert([formFieldName isKindOfClass:[NSString class]]); + XCTAssert([formFieldName length] > 0); + } + + XCTAssertEqual([[mapping allValues] count], [[NSSet setWithArray:[mapping allValues]] count]); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:[STDSJSONEncoder dictionaryForObject:params]]); +} + +- (void)testNextChallengeRequestParametersIncrementsCounter { + STDSChallengeRequestParameters *params = [[STDSChallengeRequestParameters alloc] initWithThreeDSServerTransactionIdentifier:@"server id" + acsTransactionIdentifier:@"acs id" + messageVersion:@"message version" + sdkTransactionIdentifier:@"sdk id" + requestorAppUrl:@"requestor app url" + sdkCounterStoA:0]; + for (NSInteger i = 0; i < 1000; i++) { + XCTAssertEqual(params.sdkCounterStoA.length, 3); + XCTAssertEqual(params.sdkCounterStoA.integerValue, i); + params = [params nextChallengeRequestParametersByIncrementCounter]; + } +} + +- (void)testEmptyChallengeDataEntryField { + STDSChallengeRequestParameters *params = [[STDSChallengeRequestParameters alloc] initWithThreeDSServerTransactionIdentifier:@"server id" + acsTransactionIdentifier:@"acs id" + messageVersion:@"message version" + sdkTransactionIdentifier:@"sdk id" + requestorAppUrl:@"requestor app url" + sdkCounterStoA:0]; + params.challengeDataEntry = @""; + XCTAssertNil(params.challengeDataEntry); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSChallengeResponseObjectTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeResponseObjectTest.m new file mode 100644 index 00000000..7ffdd95a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSChallengeResponseObjectTest.m @@ -0,0 +1,79 @@ +// +// STDSChallengeResponseObjectTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSChallengeResponseObject.h" +#import "STDSTestJSONUtils.h" +#import "NSError+Stripe3DS2.h" + +@interface STDSChallengeResponseObjectTest : XCTestCase + +@end + +@implementation STDSChallengeResponseObjectTest + +- (void)testSuccessfulDecode { + NSDictionary *json = [STDSTestJSONUtils jsonNamed:@"CRes"]; + NSError *error; + STDSChallengeResponseObject *cr = [STDSChallengeResponseObject decodedObjectFromJSON:json error:&error]; + + XCTAssertNil(error); + XCTAssertNotNil(cr); + + XCTAssertEqualObjects(cr.threeDSServerTransactionID, @"8a880dc0-d2d2-4067-bcb1-b08d1690b26e"); + XCTAssertEqualObjects(cr.acsTransactionID, @"d7c1ee99-9478-44a6-b1f2-391e29c6b340"); + XCTAssertEqual(cr.acsUIType, STDSACSUITypeText); + XCTAssertEqual(cr.challengeCompletionIndicator, NO); + XCTAssertEqualObjects(cr.challengeInfoHeader, @"Header information"); + XCTAssertEqualObjects(cr.challengeInfoLabel, @"One-time-password"); + XCTAssertEqualObjects(cr.challengeInfoText, @"Please enter the received one-time-password"); + XCTAssertEqual(cr.showChallengeInfoTextIndicator, NO); + XCTAssertEqualObjects(cr.expandInfoLabel, @"Additional instructions"); + XCTAssertEqualObjects(cr.expandInfoText, @"The issuer will send you via SMS a one-time password. Please enter the value in the designated input field above and press continue to complete the 3-D Secure authentication process."); + XCTAssertEqualObjects(cr.issuerImage.mediumDensityURL, [NSURL URLWithString:@"https://acs.com/medium_image.svg"]); + XCTAssertEqualObjects(cr.issuerImage.highDensityURL, [NSURL URLWithString:@"https://acs.com/high_image.svg"]); + XCTAssertEqualObjects(cr.issuerImage.extraHighDensityURL, [NSURL URLWithString:@"https://acs.com/extraHigh_image.svg"]); + XCTAssertEqualObjects(cr.messageType, @"CRes"); + XCTAssertEqualObjects(cr.messageVersion, @"2.1.0"); + XCTAssertEqualObjects(cr.paymentSystemImage.mediumDensityURL, [NSURL URLWithString:@"https://ds.com/medium_image.svg"]); + XCTAssertEqualObjects(cr.paymentSystemImage.highDensityURL, [NSURL URLWithString:@"https://ds.com/high_image.svg"]); + XCTAssertEqualObjects(cr.paymentSystemImage.extraHighDensityURL, [NSURL URLWithString:@"https://ds.com/extraHigh_image.svg"]); + XCTAssertEqualObjects(cr.resendInformationLabel, @"Send new One-time-password"); + XCTAssertEqualObjects(cr.sdkTransactionID, @"b2385523-a66c-4907-ac3c-91848e8c0067"); + XCTAssertEqualObjects(cr.submitAuthenticationLabel, @"Continue"); + XCTAssertEqualObjects(cr.whyInfoLabel, @"Why using 3-D Secure?"); + XCTAssertEqualObjects(cr.whyInfoText, @"Some explanation about why using 3-D Secure is an excellent idea as part of an online payment transaction"); + XCTAssertEqualObjects(cr.acsCounterACStoSDK, @"001"); +} + +- (void)testMissingFields { + NSArray *requiredFields = @[ + @"threeDSServerTransID", + @"acsCounterAtoS", + @"acsTransID", + @"acsUiType", + @"challengeCompletionInd", + @"messageType", + @"messageVersion", + @"sdkTransID", + ]; + + for (NSString *field in requiredFields) { + NSMutableDictionary *response = [[STDSTestJSONUtils jsonNamed:@"CRes"] mutableCopy]; + [response removeObjectForKey:field]; + + NSError *error; + NSError *expectedError = [NSError _stds_missingJSONFieldError:field]; + XCTAssertNil([STDSChallengeResponseObject decodedObjectFromJSON:response error:&error]); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, expectedError.code); + XCTAssertEqualObjects(error.userInfo, expectedError.userInfo); + } +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSConfigParametersTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSConfigParametersTests.m new file mode 100644 index 00000000..28114985 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSConfigParametersTests.m @@ -0,0 +1,67 @@ +// +// STDSConfigParametersTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 2/13/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSConfigParameters.h" +#import "STDSInvalidInputException.h" + +@interface STDSConfigParametersTests : XCTestCase + +@end + +@implementation STDSConfigParametersTests + +- (void)testStandardParameters { + STDSConfigParameters *defaultParameters = [[STDSConfigParameters alloc] initWithStandardParameters]; + XCTAssertNotNil(defaultParameters, @"Should return a non-nil instance"); +} + +- (void)testAddRead { + STDSConfigParameters *parameters = [[STDSConfigParameters alloc] init]; + XCTAssertNoThrow([parameters addParameterNamed:@"testName" withValue:@"testValue"], @"Should not throw with non-nil name and value."); + NSString *paramValue = nil; + XCTAssertNoThrow(paramValue = [parameters parameterValue:@"testName"], @"Should not throw with non-nil name."); + XCTAssertEqual(paramValue, @"testValue", @"Returned value does not match expectation."); +} + +- (void)testDefaultGroup { + STDSConfigParameters *parameters = [[STDSConfigParameters alloc] init]; + XCTAssertNoThrow([parameters addParameterNamed:@"testName" withValue:@"testValue"], @"Should not throw with non-nil name and value."); + XCTAssertNoThrow([parameters addParameterNamed:@"testName" withValue:@"testValue2" toGroup:@"otherGroup"], @"Should not throw with non-nil name, value, and group."); + NSString *paramValue = nil; + XCTAssertNoThrow(paramValue = [parameters parameterValue:@"testName"], @"Should not throw with non-nil name."); + XCTAssertEqual(paramValue, @"testValue", @"Returned value does not match expectation. Should default to default group's value."); + XCTAssertNoThrow(paramValue = [parameters parameterValue:@"testName" inGroup:@"otherGroup"], @"Should not throw with non-nil name and group name."); + XCTAssertEqual(paramValue, @"testValue2", @"Returned value does not match expectation. Should read from custom group."); +} + +- (void)testExceptions { + STDSConfigParameters *parameters = [[STDSConfigParameters alloc] init]; + XCTAssertNoThrow([parameters addParameterNamed:@"testParam" withValue:@"testValue" toGroup:nil], @"Should not throw with nil group."); + + XCTAssertThrowsSpecific([parameters addParameterNamed:@"testParam" withValue:@"value2"], STDSInvalidInputException, @"Should throw STDSInvalidInputException if trying to override testParam value."); + [parameters addParameterNamed:@"testParam" withValue:@"testValue" toGroup:@"otherGroup"]; + XCTAssertThrowsSpecific([parameters addParameterNamed:@"testParam" withValue:@"value2" toGroup:@"otherGroup"], STDSInvalidInputException, @"Should throw STDSInvalidInputException if trying to override testParam value in non-default group."); +} + +- (void)testRemove { + STDSConfigParameters *parameters = [[STDSConfigParameters alloc] init]; + + [parameters addParameterNamed:@"testParam" withValue:@"testValue"]; + [parameters addParameterNamed:@"testParam" withValue:@"testValue2" toGroup:@"otherGroup"]; + XCTAssertEqual([parameters removeParameterNamed:@"testParam"], @"testValue", @"Should return testValue when removing."); + XCTAssertNil([parameters parameterValue:@"testParam"]); + XCTAssertNotNil([parameters parameterValue:@"testParam" inGroup:@"otherGroup"], @"Should only remove param in specified group."); + + XCTAssertNil([parameters removeParameterNamed:@"testParam"], @"Should return nil if removing a non-existant parameter."); + + XCTAssertEqual([parameters removeParameterNamed:@"testParam" fromGroup:@"otherGroup"], @"testValue2", @"Should return group-specific value when removing."); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationManagerTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationManagerTests.m new file mode 100644 index 00000000..d86c2b9e --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationManagerTests.m @@ -0,0 +1,33 @@ +// +// STDSDeviceInformationManagerTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/24/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDeviceInformation.h" +#import "STDSDeviceInformationManager.h" +#import "STDSWarning.h" + +@interface STDSDeviceInformationManagerTests : XCTestCase + +@end + +@implementation STDSDeviceInformationManagerTests + +- (void)testDeviceInformation { + STDSDeviceInformation *deviceInformation = [STDSDeviceInformationManager deviceInformationWithWarnings:@[] ignoringRestrictions:NO]; + XCTAssertEqualObjects(deviceInformation.dictionaryValue[@"DV"], @"1.4", @"Device data version check."); + XCTAssertNotNil(deviceInformation.dictionaryValue[@"DD"], @"Device data should be non-nil"); + XCTAssertNotNil(deviceInformation.dictionaryValue[@"DPNA"], @"Param not available should be non-nil in simulator"); + XCTAssertNil(deviceInformation.dictionaryValue[@"SW"]); + + deviceInformation = [STDSDeviceInformationManager deviceInformationWithWarnings:@[[[STDSWarning alloc] initWithIdentifier:@"WARNING_1" message:@"" severity:STDSWarningSeverityMedium], [[STDSWarning alloc] initWithIdentifier:@"WARNING_2" message:@"" severity:STDSWarningSeverityMedium], ] ignoringRestrictions:NO]; + NSArray *warningIDs = @[@"WARNING_1", @"WARNING_2"]; + XCTAssertEqualObjects(deviceInformation.dictionaryValue[@"SW"], warningIDs, @"Failed to set warning identifiers correctly"); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationParameterTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationParameterTests.m new file mode 100644 index 00000000..c550a7eb --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSDeviceInformationParameterTests.m @@ -0,0 +1,214 @@ +// +// STDSDeviceInformationParameterTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/24/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDeviceInformationParameter+Private.h" + +@interface STDSDeviceInformationParameterTests : XCTestCase + +@end + +@implementation STDSDeviceInformationParameterTests + +- (void)testNoPermissions { + STDSDeviceInformationParameter *noPermissionParam = [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"NoPermissionID" + permissionCheck:^BOOL{ + return NO; + } + valueCheck:^id _Nullable{ + XCTFail(@"Should not try to collect value if we don't have permission for it"); + return @"fail"; + }]; + [noPermissionParam collectIgnoringRestrictions:YES withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + XCTAssertFalse(collected, @"Should not have collected a param we don't have permission for."); + XCTAssertTrue([value isKindOfClass:[NSString class]], @"No permission value should be a string."); + XCTAssertEqualObjects(value, @"RE03", @"Returned value should be 'RE03' for param with missing permissions."); + }]; +} + +- (void)testNoValue { + STDSDeviceInformationParameter *noValueParam = [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"NoValueID" + permissionCheck:^BOOL{ + return YES; + } + valueCheck:^id _Nullable{ + return nil; + }]; + [noValueParam collectIgnoringRestrictions:YES withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + XCTAssertFalse(collected, @"Should not have collected a param we don't have a value for."); + XCTAssertTrue([value isKindOfClass:[NSString class]], @"No value value should be a string."); + XCTAssertEqualObjects(value, @"RE02", @"Returned value should be 'RE02' for param with unavailable value."); + }]; +} + +- (void)testCollect { + __block BOOL permissionCheckCalled = NO; + __block BOOL valueCheckCalled = NO; + __block BOOL collectedHandlerCalled = NO; + + STDSDeviceInformationParameter *param = [[STDSDeviceInformationParameter alloc] initWithIdentifier:@"ParamID" + permissionCheck:^BOOL{ + XCTAssertFalse(valueCheckCalled); + permissionCheckCalled = YES; + return YES; + } + valueCheck:^id _Nullable{ + XCTAssertTrue(permissionCheckCalled); + valueCheckCalled = YES; + return @"param_val"; + }]; + [param collectIgnoringRestrictions:YES withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + XCTAssertTrue(collected, @"Should have marked value as collected."); + XCTAssertEqualObjects(value, @"param_val", @"Inaccurate returned value."); + XCTAssertTrue(permissionCheckCalled); + XCTAssertTrue(valueCheckCalled); + collectedHandlerCalled = YES; + }]; + + // This check tests that collect is synchronous for now + XCTAssertTrue(collectedHandlerCalled); + + // reset so the permission before value check doesn't fail on the second call + permissionCheckCalled = NO; + valueCheckCalled = NO; + + // make sure the ignoreRestrictions param is respected + [param collectIgnoringRestrictions:NO withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + XCTAssertFalse(collected, @"Should not have marked value as collected."); + XCTAssertFalse(permissionCheckCalled, @"Restrictions shouldn't even check the runtime permission."); + XCTAssertFalse(valueCheckCalled, @"Should not have tried to get the value."); + XCTAssertEqualObjects(value, @"RE01", @"Should return market restricted code as the value."); + }]; +} + +- (void)testAllParameters { + NSArray *allParams = [STDSDeviceInformationParameter allParameters]; + XCTAssertEqual(allParams.count, 29, @"iOS should collect 29 separate parameters."); + NSMutableSet *allParamIdentifiers = [[NSMutableSet alloc] init]; + for (STDSDeviceInformationParameter *param in allParams) { + [param collectIgnoringRestrictions:YES withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + [allParamIdentifiers addObject:identifier]; + }]; + } + XCTAssertEqual(allParamIdentifiers.count, allParams.count, @"Sanity check that there are not duplicate identifiers."); + NSArray *expectedIdentifiers = @[ + @"C001", + @"C002", + @"C003", + @"C004", + @"C005", + @"C006", + @"C007", + @"C008", + @"C009", + @"C010", + @"C011", + @"C012", + @"C013", + @"C014", + @"C015", + @"I001", + @"I002", + @"I003", + @"I004", + @"I005", + @"I006", + @"I007", + @"I008", + @"I009", + @"I010", + @"I011", + @"I012", + @"I013", + @"I014", + ]; + for (NSString *identifier in expectedIdentifiers) { + XCTAssertTrue([allParamIdentifiers containsObject:identifier], @"Missing identifier %@", identifier); + } +} + +- (void)testOnlyApprovedIdentifiers { + NSArray *allParams = [STDSDeviceInformationParameter allParameters]; + NSMutableSet *collectedParameterIdentifiers = [[NSMutableSet alloc] init]; + for (STDSDeviceInformationParameter *param in allParams) { + [param collectIgnoringRestrictions:NO withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + + if (collected) { + [collectedParameterIdentifiers addObject:identifier]; + } + }]; + } + NSArray *expectedIdentifiers = @[ + @"C001", + @"C002", + @"C003", + @"C004", + @"C005", + @"C006", + @"C007", + @"C008", + ]; + XCTAssertEqual(collectedParameterIdentifiers.count, expectedIdentifiers.count, @"Should only have collected the expected amount."); + + for (NSString *identifier in expectedIdentifiers) { + XCTAssertTrue([collectedParameterIdentifiers containsObject:identifier], @"Missing identifier %@", identifier); + } +} + +- (void)testIdentifiersAccurate { + NSDictionary *expectedIdentifiers = @{ + @"C001": [STDSDeviceInformationParameter platform], + @"C002": [STDSDeviceInformationParameter deviceModel], + @"C003": [STDSDeviceInformationParameter OSName], + @"C004": [STDSDeviceInformationParameter OSVersion], + @"C005": [STDSDeviceInformationParameter locale], + @"C006": [STDSDeviceInformationParameter timeZone], + @"C007": [STDSDeviceInformationParameter advertisingID], + @"C008": [STDSDeviceInformationParameter screenResolution], + @"C009": [STDSDeviceInformationParameter deviceName], + @"C010": [STDSDeviceInformationParameter IPAddress], + @"C011": [STDSDeviceInformationParameter latitude], + @"C012": [STDSDeviceInformationParameter longitude], + @"I001": [STDSDeviceInformationParameter identiferForVendor], + @"I002": [STDSDeviceInformationParameter userInterfaceIdiom], + @"I003": [STDSDeviceInformationParameter familyNames], + @"I004": [STDSDeviceInformationParameter fontNamesForFamilyName], + @"I005": [STDSDeviceInformationParameter systemFont], + @"I006": [STDSDeviceInformationParameter labelFontSize], + @"I007": [STDSDeviceInformationParameter buttonFontSize], + @"I008": [STDSDeviceInformationParameter smallSystemFontSize], + @"I009": [STDSDeviceInformationParameter systemFontSize], + @"I010": [STDSDeviceInformationParameter systemLocale], + @"I011": [STDSDeviceInformationParameter availableLocaleIdentifiers], + @"I012": [STDSDeviceInformationParameter preferredLanguages], + @"I013": [STDSDeviceInformationParameter defaultTimeZone], + }; + + [expectedIdentifiers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, STDSDeviceInformationParameter * _Nonnull obj, BOOL * _Nonnull stop) { + [obj collectIgnoringRestrictions:YES withHandler:^(BOOL collected, NSString * _Nonnull identifier, id _Nonnull value) { + XCTAssertEqualObjects(key, identifier); + }]; + }]; +} + +#pragma mark - App ID + +- (void)testSDKAppIdentifier { + // xctest in Xcode 13+ uses the Xcode version for the current app id string, previous versions are empty + NSString *appIdentifierKeyPrefix = @"STDSStripe3DS2AppIdentifierKey"; + NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @""; + NSString *appIdentifierUserDefaultsKey = [appIdentifierKeyPrefix stringByAppendingString:appVersion]; + + [[NSUserDefaults standardUserDefaults] removeObjectForKey:appIdentifierUserDefaultsKey]; + NSString *appId = [STDSDeviceInformationParameter sdkAppIdentifier]; + XCTAssertNotNil(appId); + XCTAssertEqualObjects(appId, [[NSUserDefaults standardUserDefaults] stringForKey:appIdentifierUserDefaultsKey]); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSDirectoryServerCertificateTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSDirectoryServerCertificateTests.m new file mode 100644 index 00000000..43e7bab5 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSDirectoryServerCertificateTests.m @@ -0,0 +1,811 @@ +// +// STDSDirectoryServerCertificateTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 3/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServerCertificate+Internal.h" +#import "STDSJSONWebSignature.h" +#import "NSString+JWEHelpers.h" + +@interface STDSDirectoryServerCertificateTests : XCTestCase + +@end + +@implementation STDSDirectoryServerCertificateTests + +- (void)testCertificateForDirectoryServer { + NSArray *directoryServers = @[ + @(STDSDirectoryServerULTestRSA), + @(STDSDirectoryServerULTestEC), + @(STDSDirectoryServerSTPTestRSA), + @(STDSDirectoryServerSTPTestEC), + @(STDSDirectoryServerAmex), + @(STDSDirectoryServerDiscover), + @(STDSDirectoryServerMastercard), + @(STDSDirectoryServerVisa), + @(STDSDirectoryServerCustom), + @(STDSDirectoryServerUnknown), + ]; + + for (NSNumber *directoryServerNum in directoryServers) { + STDSDirectoryServer directoryServer = (STDSDirectoryServer)[directoryServerNum integerValue]; + switch (directoryServer) { + case STDSDirectoryServerULTestRSA: + case STDSDirectoryServerULTestEC: + case STDSDirectoryServerSTPTestRSA: + case STDSDirectoryServerSTPTestEC: + case STDSDirectoryServerAmex: + case STDSDirectoryServerCartesBancaires: + case STDSDirectoryServerDiscover: + case STDSDirectoryServerMastercard: + case STDSDirectoryServerVisa: + XCTAssertNotNil([STDSDirectoryServerCertificate certificateForDirectoryServer:directoryServer], @"Failed creating certificate for type %@", directoryServerNum); + break; + + case STDSDirectoryServerCustom: + case STDSDirectoryServerUnknown: + XCTAssertNil([STDSDirectoryServerCertificate certificateForDirectoryServer:directoryServer], @"Should return nil for STDSDirectoryServerUnknown"); + break; + } + } +} + +- (void)testKeyType { + NSArray *directoryServers = @[ + @(STDSDirectoryServerULTestRSA), + @(STDSDirectoryServerULTestEC), + @(STDSDirectoryServerSTPTestRSA), + @(STDSDirectoryServerSTPTestEC), + @(STDSDirectoryServerAmex), + @(STDSDirectoryServerCartesBancaires), + @(STDSDirectoryServerDiscover), + @(STDSDirectoryServerMastercard), + @(STDSDirectoryServerVisa), + @(STDSDirectoryServerCustom), + @(STDSDirectoryServerUnknown), + ]; + + for (NSNumber *directoryServerNum in directoryServers) { + STDSDirectoryServer directoryServer = (STDSDirectoryServer)[directoryServerNum integerValue]; + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate certificateForDirectoryServer:directoryServer]; + switch (directoryServer) { + case STDSDirectoryServerULTestRSA: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerULTestRSA"); + break; + + case STDSDirectoryServerULTestEC: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeEC, @"Incorrect key type for STDSDirectoryServerULTestEC"); + break; + + case STDSDirectoryServerSTPTestRSA: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerSTPTestRSA"); + break; + + case STDSDirectoryServerSTPTestEC: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeEC, @"Incorrect key type for STDSDirectoryServerSTPTestEC"); + break; + + case STDSDirectoryServerAmex: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerAmex"); + break; + + case STDSDirectoryServerCartesBancaires: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerCartesBancaires"); + break; + + case STDSDirectoryServerDiscover: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerDiscover"); + break; + + case STDSDirectoryServerMastercard: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerMastercard"); + break; + + case STDSDirectoryServerVisa: + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Incorrect key type for STDSDirectoryServerVisa"); + break; + + case STDSDirectoryServerCustom: + case STDSDirectoryServerUnknown: + // asserts + break; + } + } +} + +- (void)testRSA_OAEP_SHA256Encryption { + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate certificateForDirectoryServer:STDSDirectoryServerSTPTestRSA]; + if (certificate != nil) { + + XCTAssertNotNil([certificate encryptDataUsingRSA_OAEP_SHA256:[@"In nature's infinite book of secrecy a little I can read." dataUsingEncoding:NSUTF8StringEncoding]]); + } else { + XCTFail(@"Failed loading certificate for %@", NSStringFromSelector(_cmd)); + } +} + +- (void)testCustomCertificate { + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate customCertificateWithData:[[NSData alloc] initWithBase64EncodedString:certificateString options:0]]; + XCTAssertNotNil(certificate, @"Failed to create certificate from string."); + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Parsed incorrect key type from custom certificate"); +} + +- (void)testCustomCertificateWithString { + // Using ds-amex.pm.PEM + NSString *certificateString = @"MIIE0TCCA7mgAwIBAgIUXbeqM1duFcHk4dDBwT8o7Ln5wX8wDQYJKoZIhvcNAQEL" + "BQAwXjELMAkGA1UEBhMCVVMxITAfBgNVBAoTGEFtZXJpY2FuIEV4cHJlc3MgQ29t" + "cGFueTEsMCoGA1UEAxMjQW1lcmljYW4gRXhwcmVzcyBTYWZla2V5IElzc3Vpbmcg" + "Q0EwHhcNMTgwMjIxMjM0OTMxWhcNMjAwMjIxMjM0OTMwWjCB0DELMAkGA1UEBhMC" + "VVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazE/MD0GA1UE" + "ChM2QW1lcmljYW4gRXhwcmVzcyBUcmF2ZWwgUmVsYXRlZCBTZXJ2aWNlcyBDb21w" + "YW55LCBJbmMuMTkwNwYDVQQLEzBHbG9iYWwgTmV0d29yayBUZWNobm9sb2d5IC0g" + "TmV0d29yayBBUEkgUGxhdGZvcm0xHzAdBgNVBAMTFlNESy5TYWZlS2V5LkVuY3J5" + "cHRLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSFF9kTYbwRrxX" + "C6WcJJYio5TZDM62+CnjQRfggV3GMI+xIDtMIN8LL/jbWBTycu97vrNjNNv+UPhI" + "WzhFDdUqyRfrY337A39uE8k1xhdDI3dNeZz6xgq8r9hn2NBou78YPBKidpN5oiHn" + "TxcFq1zudut2fmaldaa9a4ZKgIQo+02heiJfJ8XNWkoWJ17GcjJ59UU8C1KF/y1G" + "ymYO5ha2QRsVZYI17+ZFsqnpcXwK4Mr6RQKV6UimmO0nr5++CgvXfekcWAlLV6Xq" + "juACWi3kw0haepaX/9qHRu1OSyjzWNcSVZ0On6plB5Lq6Y9ylgmxDrv+zltz3MrT" + "K7txIAFFAgMBAAGjggESMIIBDjAMBgNVHRMBAf8EAjAAMCEGA1UdEQQaMBiCFlNE" + "Sy5TYWZlS2V5LkVuY3J5cHRLZXkwRQYJKwYBBAGCNxQCBDgeNgBBAE0ARQBYAF8A" + "UwBBAEYARQBLAEUAWQAyAF8ARABTAF8ARQBOAEMAUgBZAFAAVABJAE8ATjAOBgNV" + "HQ8BAf8EBAMCBJAwHwYDVR0jBBgwFoAU7k/rXuVMhTBxB1zSftPgmLFuDIgwRAYD" + "VR0fBD0wOzA5oDegNYYzaHR0cDovL2FtZXhzay5jcmwuY29tLXN0cm9uZy1pZC5u" + "ZXQvYW1leHNhZmVrZXkuY3JsMB0GA1UdDgQWBBQHclVTo5nwZGH8labJ2F2P45xi" + "fDANBgkqhkiG9w0BAQsFAAOCAQEAWY6b77VBoGLs3k5vOqSU7QRqT+4v6y77T8LA" + "BKrSZ58DiVZWVyDSxyftQUiRRgFHt2gTN0yfJTP50Fyp84nCEWC0tugZ4iIhgPss" + "HzL+4/u4eG/MTzK2ESxvPgr6YHajyuU+GXA89u8+bsFrFmojOjhTgFKli7YUeV/0" + "xoiYZf2utlns800ofJrcrfiFoqE6PvK4Od0jpeMgfSKv71nK5ihA1+wTk76ge1fs" + "PxL23hEdRpWW11ofaLfJGkLFXMM3/LHSXWy7HhsBgDELdzLSHU4VkSv8yTOZxsRO" + "ByxdC5v3tXGcK56iQdtKVPhFGOOEBugw7AcuRzv3f1GhvzAQZg=="; + STDSDirectoryServerCertificate *certificate = certificate = [STDSDirectoryServerCertificate customCertificateWithString:certificateString]; + XCTAssertNotNil(certificate, @"Failed to create certificate from string."); + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Parsed incorrect key type from custom certificate"); + + // 3ds2.rsa.encryption.crt + certificateString = @"-----BEGIN CERTIFICATE-----" + "MIIFrjCCBJagAwIBAgIQB2rJmsHVwbONd36WP9QPrTANBgkqhkiG9w0BAQsFADBx" + "MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl" + "cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xIjAgBgNVBAMTGVZpc2EgZUNv" + "bW1lcmNlIElzc3VpbmcgQ0EwHhcNMTcxMTAyMjIyMzEwWhcNMjAxMTAzMDAyMzEw" + "WjCBoTEYMBYGA1UEBxMPSGlnaGxhbmRzIFJhbmNoMREwDwYDVQQIEwhDb2xvcmFk" + "bzELMAkGA1UEBhMCVVMxDTALBgNVBAoTBFZJU0ExLzAtBgNVBAsTJlZpc2EgSW50" + "ZXJuYXRpb25hbCBTZXJ2aWNlIEFzc29jaWF0aW9uMSUwIwYDVQQDExwzZHMyLnJz" + "YS5lbmNyeXB0aW9uLnZpc2EuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" + "CgKCAQEAst+HGfPPsX3p6HHEQ9YzourlQj16Nscmm13Cp7cZe4dZB2oWnJqZ7oh/" + "pEoEoOAxBw1x4NFgXKTKdHAeu3VBNVw8SwMTdIC+X16VV+3VIyPbUvJXFp3QoR8W" + "UwPB3F1Lb9SMFNS95boYDZKIOdPW0cP1dRi7pFugsBUZDCP/H3nFfBFHMCBoga+P" + "3AHGj5y8RVpv0hS9jaIsYjX+i58B61OGCB7D0AiADNZJuFzw2+xpNkt6NJJF66FP" + "O8qIh8xR2xGVDf7TtCbss/CugLRgSqKab9YRB8/TBTcy5bxj6O8HD6aL2zGLcMY9" + "dCobXxCodLEtMjJdVL8N+iZrsI2gtwIDAQABo4ICDzCCAgswEwYDVR0lBAwwCgYI" + "KwYBBQUHAwEwZQYIKwYBBQUHAQEEWTBXMCUGCCsGAQUFBzABhhlodHRwOi8vb2Nz" + "cC52aXNhLmNvbS9vY3NwMC4GCCsGAQUFBzAChiJodHRwOi8vZW5yb2xsLnZpc2Fj" + "YS5jb20vZWNvbW0uY2VyMB8GA1UdIwQYMBaAFN/DKlUuL0I6ekCdkqD3R3nXj4eK" + "MAwGA1UdEwEB/wQCMAAwgcoGA1UdHwSBwjCBvzAooCagJIYiaHR0cDovL0Vucm9s" + "bC52aXNhY2EuY29tL2VDb21tLmNybDCBkqCBj6CBjIaBiWxkYXA6Ly9FbnJvbGwu" + "dmlzYWNhLmNvbTozODkvY249VmlzYSBlQ29tbWVyY2UgSXNzdWluZyBDQSxjPVVT" + "LG91PVZpc2EgSW50ZXJuYXRpb25hbCBTZXJ2aWNlIEFzc29jaWF0aW9uLG89VklT" + "QT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MA4GA1UdDwEB/wQEAwIFoDAnBgNV" + "HREEIDAeghwzZHMyLnJzYS5lbmNyeXB0aW9uLnZpc2EuY29tMB0GA1UdDgQWBBT8" + "m2pDtUtY13f/3NCOmexHavP5zDA5BgNVHSAEMjAwMC4GBWeBAwEBMCUwIwYIKwYB" + "BQUHAgEWF2h0dHA6Ly93d3cudmlzYS5jb20vcGtpMA0GCSqGSIb3DQEBCwUAA4IB" + "AQCcCUhU7KnHUDuLXqwSuzC8lWWCcEqPRgPPzY3mgBUg9ya0p+v7QF2BG77tpygK" + "E2yDPkOE8trzYeMi7TCuvKgZvUXDSOka8SId9QleMBlo2pzNi0vKKBG8+E7qmGaf" + "etQHVaoFvhg24/e7y8q89VYNKfLXn8TWMUOJdTQoNP+4bHcCnBvWWUcI2LlyEog1" + "2FDSG8hgP3cpw+0B2Hace9BQGR7ZgTIJAANEHZ54QGOYdxZEcDS5IEpKZlN8INs/" + "NKJyCkqP09VA4NO/WHaGFAXtgoLmjlA9Kal+4ieJPKijVDxcHVv/uPSfVQJ0/vCa" + "udJGOXV9q4VteupwLxfOGW8w" + "-----END CERTIFICATE-----"; + + certificateString = @"-----BEGIN CERTIFICATE-----\n" + "MIIFrjCCBJagAwIBAgIQB2rJmsHVwbONd36WP9QPrTANBgkqhkiG9w0BAQsFADBx\n" + "MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl\n" + "cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xIjAgBgNVBAMTGVZpc2EgZUNv\n" + "bW1lcmNlIElzc3VpbmcgQ0EwHhcNMTcxMTAyMjIyMzEwWhcNMjAxMTAzMDAyMzEw\n" + "WjCBoTEYMBYGA1UEBxMPSGlnaGxhbmRzIFJhbmNoMREwDwYDVQQIEwhDb2xvcmFk\n" + "bzELMAkGA1UEBhMCVVMxDTALBgNVBAoTBFZJU0ExLzAtBgNVBAsTJlZpc2EgSW50\n" + "ZXJuYXRpb25hbCBTZXJ2aWNlIEFzc29jaWF0aW9uMSUwIwYDVQQDExwzZHMyLnJz\n" + "YS5lbmNyeXB0aW9uLnZpc2EuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" + "CgKCAQEAst+HGfPPsX3p6HHEQ9YzourlQj16Nscmm13Cp7cZe4dZB2oWnJqZ7oh/\n" + "pEoEoOAxBw1x4NFgXKTKdHAeu3VBNVw8SwMTdIC+X16VV+3VIyPbUvJXFp3QoR8W\n" + "UwPB3F1Lb9SMFNS95boYDZKIOdPW0cP1dRi7pFugsBUZDCP/H3nFfBFHMCBoga+P\n" + "3AHGj5y8RVpv0hS9jaIsYjX+i58B61OGCB7D0AiADNZJuFzw2+xpNkt6NJJF66FP\n" + "O8qIh8xR2xGVDf7TtCbss/CugLRgSqKab9YRB8/TBTcy5bxj6O8HD6aL2zGLcMY9\n" + "dCobXxCodLEtMjJdVL8N+iZrsI2gtwIDAQABo4ICDzCCAgswEwYDVR0lBAwwCgYI\n" + "KwYBBQUHAwEwZQYIKwYBBQUHAQEEWTBXMCUGCCsGAQUFBzABhhlodHRwOi8vb2Nz\n" + "cC52aXNhLmNvbS9vY3NwMC4GCCsGAQUFBzAChiJodHRwOi8vZW5yb2xsLnZpc2Fj\n" + "YS5jb20vZWNvbW0uY2VyMB8GA1UdIwQYMBaAFN/DKlUuL0I6ekCdkqD3R3nXj4eK\n" + "MAwGA1UdEwEB/wQCMAAwgcoGA1UdHwSBwjCBvzAooCagJIYiaHR0cDovL0Vucm9s\n" + "bC52aXNhY2EuY29tL2VDb21tLmNybDCBkqCBj6CBjIaBiWxkYXA6Ly9FbnJvbGwu\n" + "dmlzYWNhLmNvbTozODkvY249VmlzYSBlQ29tbWVyY2UgSXNzdWluZyBDQSxjPVVT\n" + "LG91PVZpc2EgSW50ZXJuYXRpb25hbCBTZXJ2aWNlIEFzc29jaWF0aW9uLG89VklT\n" + "QT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MA4GA1UdDwEB/wQEAwIFoDAnBgNV\n" + "HREEIDAeghwzZHMyLnJzYS5lbmNyeXB0aW9uLnZpc2EuY29tMB0GA1UdDgQWBBT8\n" + "m2pDtUtY13f/3NCOmexHavP5zDA5BgNVHSAEMjAwMC4GBWeBAwEBMCUwIwYIKwYB\n" + "BQUHAgEWF2h0dHA6Ly93d3cudmlzYS5jb20vcGtpMA0GCSqGSIb3DQEBCwUAA4IB\n" + "AQCcCUhU7KnHUDuLXqwSuzC8lWWCcEqPRgPPzY3mgBUg9ya0p+v7QF2BG77tpygK\n" + "E2yDPkOE8trzYeMi7TCuvKgZvUXDSOka8SId9QleMBlo2pzNi0vKKBG8+E7qmGaf\n" + "etQHVaoFvhg24/e7y8q89VYNKfLXn8TWMUOJdTQoNP+4bHcCnBvWWUcI2LlyEog1\n" + "2FDSG8hgP3cpw+0B2Hace9BQGR7ZgTIJAANEHZ54QGOYdxZEcDS5IEpKZlN8INs/\n" + "NKJyCkqP09VA4NO/WHaGFAXtgoLmjlA9Kal+4ieJPKijVDxcHVv/uPSfVQJ0/vCa\n" + "udJGOXV9q4VteupwLxfOGW8w\n" + "-----END CERTIFICATE-----\n"; + certificate = [STDSDirectoryServerCertificate customCertificateWithString:certificateString]; + XCTAssertNotNil(certificate, @"Failed to create certificate from string."); + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Parsed incorrect key type from custom certificate"); + + certificateString = @"-----BEGIN CERTIFICATE-----" + "-----END CERTIFICATE-----"; + certificate = [STDSDirectoryServerCertificate customCertificateWithString:certificateString]; + XCTAssertNil(certificate, @"Should not return a valid value with only anchors"); + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Parsed incorrect key type from custom certificate"); + certificateString = @"-----END CERTIFICATE-----"; + + certificate = [STDSDirectoryServerCertificate customCertificateWithString:certificateString]; + XCTAssertNil(certificate, @"Should not return a valid value with only suffix anchor"); + XCTAssertEqual(certificate.keyType, STDSDirectoryServerKeyTypeRSA, @"Parsed incorrect key type from custom certificate"); +} + +- (void)testCertificateChainValidation { + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-string-concatenation" + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSArray *certChain = @[@"MIIDeTCCAmGgAwIBAgIQbS4C4BSig7uuJ5uDpeT4WDANB" + "gkqhkiG9w0BAQsFADBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBG" + "RYHZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE1NDAyW" + "hcNMjcxMjMxMTMzMDAwWjBIMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyL" + "GQBGRYHZXhhbXBsZTEYMBYGA1UEAwwPUlNBIEV4YW1wbGUgQUNTMIIBIjANBgkqhkiG9" + "w0BAQEFAAOCAQ8AMIIBCgKCAQEAkNrPIBDXMU6fcyv5i+QHQAQ+K8gsC3HJb7FYhYaw8" + "hXbNJa+t8q0lDKwLZgQXYV+ffWxXJv5GGrlZE4GU52lfMEegTDzYTrRQ3tepgKFjMGg6" + "Iy6fkl1ZNsx2gEonsnlShfzA9GJwRTmtKPbk1s+hwx1IU5AT+AIelNqBgcF2vE5W25/S" + "GGBoaROVdUYxqETDggM1z5cKV4ZjDZ8+lh4oVB07bkac6LQdHpJUUySH/Er20DXx30Ky" + "i97PciXKTS+QKXnmm8ivyRCmux22ZoPUind2BKC5OiG4MwALhaL2Z2k8CsRdfy+7dg7z" + "41Rp6D0ZeEvtaUp4bX4aKraL4rTfwIDAQABo2AwXjAMBgNVHRMBAf8EAjAAMA4GA1UdD" + "wEB/wQEAwIHgDAdBgNVHQ4EFgQUktwf6ZpTCxjYKw/BLW6PeiNX4swwHwYDVR0jBBgwF" + "oAUw4MCnbwD6m2wpnoQ2sND8GryPN4wDQYJKoZIhvcNAQELBQADggEBAGuNHxv/BR6j7" + "lCPysm1uhrbjBOqdrhJMR/Id4dB2GtdEScl3irGPmXyQ2SncTWhNfsgsKDZWp5Bk7+Ot" + "nty0eNUMk3hZEqgYjxhzau048XHbsfGvoJaMGZZNTwUvTUz2hkkhgpx9yQAKIA2LzFKc" + "gYhelPu4GW5rtEuxu3IS6WYy3D1GtF3naEWkjUra8hQOhOl2S+CYHmRd6lGkXykVDajM" + "gd2AJFzXdKLxTt0OYrWDGlUSzGACRBCd5xbRmATIldtccaGqDN1cNWv0I/bPN8EpKS6B" + "0WaZcPasItKWpDC85Jw1GrDxdhwoKHoxtSG+odiTwB5zLbrn2OsRE5bV7E="]; +#pragma clang diagnostic pop + + XCTAssertTrue([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:@[certificateString]], @"Failed to verify certificate chain."); + certChain = @[@"junk_data", @"more_junk"]; + XCTAssertFalse([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:@[certificateString]], @"Verified certificate chain with invalid certificates."); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-string-concatenation" + // Test with valid certificates in the chain, but that are not related to the root certificate + // ref. https://tools.ietf.org/html/rfc7515 + certChain = @[ + @"MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVM" + "xITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR2" + "8gRGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExM" + "TYwMTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UE" + "CBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWR" + "keS5jb20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYW" + "RkeS5jb20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlc" + "nRpZmljYXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJ" + "KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTt" + "wY6vj3D3HKrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqV" + "Tr9vcyOdQmVZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aL" + "GbqGmu75RpRSgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo" + "7RJlbmr2EkRTcDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgW" + "JCJjPOq8lh8BJ6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAw" + "EAAaOCATIwggEuMB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVH" + "SMEGDAWgBTSxLDSkdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEA" + "MDMGCCsGAQUFBwEBBCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWR" + "keS5jb20wRgYDVR0fBD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2" + "RhZGR5LmNvbS9yZXBvc2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVH" + "SAAMDgwNgYIKwYBBQUHAgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j" + "b20vcmVwb3NpdG9yeTAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggE" + "BANKGwOy9+aG2Z+5mC6IGOgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPI" + "UyIXvJxwqoJKSQ3kbTJSMUA2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL" + "5CkKSkB2XIsKd83ASe8T+5o0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9" + "p0iRFEUOOjZv2kWzRaJBydTXRE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsx" + "uxN89txJx9OjxUUAiKEngHUuHqDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZ" + "EjYx8WnM25sgVjOuH0aBsXBTWVU+4=", + @"MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Z" + "hbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIE" + "luYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb" + "24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8x" + "IDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDY" + "yMFoXDTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZS" + "BHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgM" + "iBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN" + "ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XC" + "APVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux" + "6wwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLO" + "tXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWo" + "riMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZ" + "Eewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ" + "4EFgQU0sSw0pHUTBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBu" + "zEkMCIGA1UEBxMbVmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQK" + "Ew5WYWxpQ2VydCwgSW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2x" + "pY3kgVmFsaWRhdGlvbiBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudm" + "FsaWNlcnQuY29tLzEgMB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CA" + "QEwDwYDVR0TAQH/BAUwAwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGG" + "F2h0dHA6Ly9vY3NwLmdvZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA" + "6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybD" + "BLBgNVHSAERDBCMEAGBFUdIAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZ" + "mljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjAN" + "BgkqhkiG9w0BAQUFAAOBgQC1QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+" + "Sn1eocSxI0YGyeR+sBjUZsE4OWBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgM" + "QLARzLrUc+cb53S8wGd9D0VmsfSxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j" + "09VZw==", + @"MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ" + "0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNT" + "AzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0a" + "G9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkq" + "hkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE" + "5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTm" + "V0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZ" + "XJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQD" + "ExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9" + "AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5a" + "vIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zf" + "N1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwb" + "P7RfZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQU" + "AA4GBADt/UG9vUJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQ" + "C1u+mNr0HZDzTuIYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMM" + "j4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd", + @"MIIDeTCCAmGgAwIBAgIQbS4C4BSig7uuJ5uDpeT4WDANB" + "gkqhkiG9w0BAQsFADBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBG" + "RYHZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE1NDAyW" + "hcNMjcxMjMxMTMzMDAwWjBIMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyL" + "GQBGRYHZXhhbXBsZTEYMBYGA1UEAwwPUlNBIEV4YW1wbGUgQUNTMIIBIjANBgkqhkiG9" + "w0BAQEFAAOCAQ8AMIIBCgKCAQEAkNrPIBDXMU6fcyv5i+QHQAQ+K8gsC3HJb7FYhYaw8" + "hXbNJa+t8q0lDKwLZgQXYV+ffWxXJv5GGrlZE4GU52lfMEegTDzYTrRQ3tepgKFjMGg6" + "Iy6fkl1ZNsx2gEonsnlShfzA9GJwRTmtKPbk1s+hwx1IU5AT+AIelNqBgcF2vE5W25/S" + "GGBoaROVdUYxqETDggM1z5cKV4ZjDZ8+lh4oVB07bkac6LQdHpJUUySH/Er20DXx30Ky" + "i97PciXKTS+QKXnmm8ivyRCmux22ZoPUind2BKC5OiG4MwALhaL2Z2k8CsRdfy+7dg7z" + "41Rp6D0ZeEvtaUp4bX4aKraL4rTfwIDAQABo2AwXjAMBgNVHRMBAf8EAjAAMA4GA1UdD" + "wEB/wQEAwIHgDAdBgNVHQ4EFgQUktwf6ZpTCxjYKw/BLW6PeiNX4swwHwYDVR0jBBgwF" + "oAUw4MCnbwD6m2wpnoQ2sND8GryPN4wDQYJKoZIhvcNAQELBQADggEBAGuNHxv/BR6j7" + "lCPysm1uhrbjBOqdrhJMR/Id4dB2GtdEScl3irGPmXyQ2SncTWhNfsgsKDZWp5Bk7+Ot" + "nty0eNUMk3hZEqgYjxhzau048XHbsfGvoJaMGZZNTwUvTUz2hkkhgpx9yQAKIA2LzFKc" + "gYhelPu4GW5rtEuxu3IS6WYy3D1GtF3naEWkjUra8hQOhOl2S+CYHmRd6lGkXykVDajM" + "gd2AJFzXdKLxTt0OYrWDGlUSzGACRBCd5xbRmATIldtccaGqDN1cNWv0I/bPN8EpKS6B" + "0WaZcPasItKWpDC85Jw1GrDxdhwoKHoxtSG+odiTwB5zLbrn2OsRE5bV7E=",]; + XCTAssertFalse([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:@[certificateString]], @"Verified invalid certificate chain."); + + // stripe.com's cert chain retrieved by https://whatsmychaincert.com/ + // If below test cases start failing, the certs may have expired and should be replaced with newer ones + // from whatsmychaincert.com, select Generate Cert Chain, include root for stripe.com + // print the entire certificate chain with `openssl crl2pkcs7 -nocrl -certfile | openssl pkcs7 -print_certs -text` + // The root is the last printed certificate and the cert chain should be ordered with the top most at the end of certChain + + certChain = @[ + @"MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3" + "d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j" + "ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL" + "MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3" + "LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW" + "YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC" + "ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY" + "uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/" + "LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy" + "/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh" + "cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k" + "8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB" + "Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF" + "BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp" + "Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy" + "dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2" + "MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j" + "b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW" + "gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh" + "hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg" + "4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa" + "2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs" + "1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1" + "oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn" + "8TUoE6smftX3eg==", + + @"MIIHQDCCBiigAwIBAgIQD2ygPziYarLZojJGDizjjzANBgkqhkiG9w0BAQsFADB1" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3" + "d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk" + "IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTIxMTAyMDAwMDAwMFoXDTIyMDIwMjIz" + "NTk1OVowgcYxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB" + "BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF" + "Ewc0Njc1NTA2MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG" + "A1UEBxMNU2FuIEZyYW5jaXNjbzEUMBIGA1UEChMLU3RyaXBlLCBJbmMxEzARBgNV" + "BAMTCnN0cmlwZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1" + "ZyRTERjtA28e16WX4AUBBbXE8YTA6F5RNp5NEy2px6GF2LbjR8TobVfJoj63+CXR" + "W4uox0TQV526ZnPKbvYpAilYxEc9/fJTPS3lDJ4EgKRZlZ97UrGY8dzOR2aebpw4" + "xYnN8gYXFHeISG1ieQnnyZUck5HUF1FX3rcWh8y7doNLL9cvIt5+R6yLqeTZkCX3" + "eIAvANuhdN++nqTM7JoFSViZa8VNQ18wviB5Hw+VWdpZXF9SQmWpP4M7pgDUYTVa" + "xIsywBiisQBExh5NXEhSAboTtqxmt5IADSQx9E2tp5u3oY2Ql3YRGmYnfe1y7QKD" + "PdEVMRa4aFod+w9qe2OfAgMBAAGjggN4MIIDdDAfBgNVHSMEGDAWgBQ901Cl1qCt" + "7vNKYApl0yHU+PjWDzAdBgNVHQ4EFgQUzjnGrO9r9NbeN6cRkzoAtaKNZLowJQYD" + "VR0RBB4wHIIKc3RyaXBlLmNvbYIOd3d3LnN0cmlwZS5jb20wDgYDVR0PAQH/BAQD" + "AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSg" + "MqAwhi5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzMu" + "Y3JsMDSgMqAwhi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2" + "ZXItZzMuY3JsMEoGA1UdIARDMEEwCwYJYIZIAYb9bAIBMDIGBWeBDAEBMCkwJwYI" + "KwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBiAYIKwYBBQUH" + "AQEEfDB6MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wUgYI" + "KwYBBQUHMAKGRmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNI" + "QTJFeHRlbmRlZFZhbGlkYXRpb25TZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADCC" + "AX4GCisGAQQB1nkCBAIEggFuBIIBagFoAHYAKXm+8J45OSHwVnOfY6V35b5XfZxg" + "Cvj5TV0mXCVdx4QAAAF8n1z3KwAABAMARzBFAiAWda1cPqRI3YujJxhh3ahOlUoN" + "L+bpZFA30lL+L3UB1AIhAIAX2FUC4uyshmrtgnnq7OcmIzsOEVW7LDlFUlfokAbW" + "AHUAUaOw9f0BeZxWbbg3eI8MpHrMGyfL956IQpoN/tSLBeUAAAF8n1z3JAAABAMA" + "RjBEAiAPAv+vPLXXB1hB5S+MIrWXIIIsa0u+cPIARVuMEQPz5gIgLbrsj2bNmxYF" + "mfvwsORC61rCPouzzFkvQGy81lz0L38AdwBByMqx3yJGShDGoToJQodeTjGLGwPr" + "60vHaPCQYpYG9gAAAXyfXPahAAAEAwBIMEYCIQCdieTsguvk9YFGdpMPsSp4CJYY" + "1cn1lTiYQQBwzKSqogIhAOyyQKx4fhNqtWOiXUYhPaQtYLS7Ey4TeOlmm7U3OktO" + "MA0GCSqGSIb3DQEBCwUAA4IBAQBmZNhqh9q/u4hNPX7fPhIXTkHJD+B30kefLTOK" + "OA/ZlMzGdvrGaJAJNzQzZL0gTEf3x+D9yjOv8shBIfblugTTa/+ulDbSzjM7qrn6" + "nfojxtLdJ90UvkDCeJ30+Mn+FRahAeLN1jVAgXAUKTgU8QoAM0URZuLh/yWSoMzh" + "kx9PiDtEJR6DMoVVOPTf9Yk3iUBr//KDzVMbz5aJFDO+2fUjGqVmW+3rapYfealY" + "LNz/SmaO8S5U4z9A3xkwXSWxlEKkboKvngSdhNgYyPeHTmNKhcmFnnA9q2LvAl9u" + "UUhp5kZA9EKInRZiVl6i7z1RNhxe2RISbq1MlZi/raZYACt8", + ]; +#pragma clang diagnostic pop + + // root + NSString *rootCertificateString = @"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3" + "d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j" + "ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL" + "MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3" + "LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug" + "RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm" + "+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW" + "PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM" + "xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB" + "Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3" + "hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg" + "EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF" + "MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA" + "FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec" + "nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z" + "eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF" + "hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2" + "Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe" + "vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep" + "+OkuE6N36B9K"; + + // mastercard DS (from mastercard.der) + NSString *dsCertificateString = @"MIIFtTCCA52gAwIBAgIQJqSRaPua/6cpablmVDHWUDANBgkqhkiG9w0BAQsFADB6" + "MQswCQYDVQQGEwJVUzETMBEGA1UEChMKTWFzdGVyQ2FyZDEoMCYGA1UECxMfTWFz" + "dGVyQ2FyZCBJZGVudGl0eSBDaGVjayBHZW4gMzEsMCoGA1UEAxMjUFJEIE1hc3Rl" + "ckNhcmQgM0RTMiBBY3F1aXJlciBTdWIgQ0EwHhcNMTgxMTIwMTQ1MzIzWhcNMjEx" + "MTIwMTQ1MzIzWjBxMQswCQYDVQQGEwJVUzEdMBsGA1UEChMUTWFzdGVyQ2FyZCBX" + "b3JsZHdpZGUxGzAZBgNVBAsTEmdhdGV3YXktZW5jcnlwdGlvbjEmMCQGA1UEAxMd" + "M2RzMi5kaXJlY3RvcnkubWFzdGVyY2FyZC5jb20wggEiMA0GCSqGSIb3DQEBAQUA" + "A4IBDwAwggEKAoIBAQCFlZjqbbL9bDKOzZFawdbyfQcezVEUSDCWWsYKw/V6co9A" + "GaPBUsGgzxF6+EDgVj3vYytgSl8xFvVPsb4ZJ6BJGvimda8QiIyrX7WUxQMB3hyS" + "BOPf4OB72CP+UkaFNR6hdlO5ofzTmB2oj1FdLGZmTN/sj6ZoHkn2Zzums8QAHFjv" + "FjspKUYCmms91gpNpJPUUztn0N1YMWVFpFMytahHIlpiGqTDt4314F7sFABLxzFr" + "Dmcqhf623SPV3kwQiLVWOvewO62ItYUFgHwle2dq76YiKrUv1C7vADSk2Am4gqwv" + "7dcCnFeM2AHbBFBa1ZBRQXosuXVw8ZcQqfY8m4iNAgMBAAGjggE+MIIBOjAOBgNV" + "HQ8BAf8EBAMCAygwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBSakqJUx4CN/s5W4wMU" + "/17uSLhFuzBIBggrBgEFBQcBAQQ8MDowOAYIKwYBBQUHMAGGLGh0dHA6Ly9vY3Nw" + "LnBraS5pZGVudGl0eWNoZWNrLm1hc3RlcmNhcmQuY29tMCgGA1UdEQQhMB+CHTNk" + "czIuZGlyZWN0b3J5Lm1hc3RlcmNhcmQuY29tMGkGA1UdHwRiMGAwXqBcoFqGWGh0" + "dHA6Ly9jcmwucGtpLmlkZW50aXR5Y2hlY2subWFzdGVyY2FyZC5jb20vOWE5MmEy" + "NTRjNzgwOGRmZWNlNTZlMzAzMTRmZjVlZWU0OGI4NDViYi5jcmwwHQYDVR0OBBYE" + "FHxN6+P0r3+dFWmi/+pDQ8JWaCbuMA0GCSqGSIb3DQEBCwUAA4ICAQAtwW8siyCi" + "mhon1WUAUmufZ7bbegf3cTOafQh77NvA0xgVeloELUNCwsSSZgcOIa4Zgpsa0xi5" + "fYxXsPLgVPLM0mBhTOD1DnPu1AAm32QVelHe6oB98XxbkQlHGXeOLs62PLtDZd94" + "7pm08QMVb+MoCnHLaBLV6eKhKK+SNrfcxr33m0h3v2EMoiJ6zCvp8HgIHEhVpleU" + "8H2Uo5YObatb/KUHgtp2z0vEfyGhZR7hrr48vUQpfVGBABsCV0aqUkPxtAXWfQo9" + "1N9B7H3EIcSjbiUz5vkj9YeDSyJIi0Y/IZbzuNMsz2cRi1CWLl37w2fe128qWxYq" + "Y/k+Y4HX7uYchB8xPaZR4JczCvg1FV2JrkOcFvElVXWSMpBbe2PS6OMr3XxrHjzp" + "DyM9qvzge0Ai9+rq8AyGoG1dP2Ay83Ndlgi42X3yl1uEUW2feGojCQQCFFArazEj" + "LUkSlrB2kA12SWAhsqqQwnBLGSTp7PqPZeWkluQVXS0sbj0878kTra6TjG3U+KqO" + "JCj8v6G380qIkAXe1xMHHNQ6GS59HZMeBPYkK2y5hmh/JVo4bRfK7Ya3blBSBfB8" + "AVWQ5GqVWklvXZsQLN7FH/fMIT3y8iE1W19Ua4whlhvn7o/aYWOkHr1G2xyh8BHj" + "7H63A2hjcPlW/ZAJSTuBZUClAhsNohH2Jg=="; + + XCTAssertTrue([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:@[rootCertificateString]], @"Failed to verify w/ root certificate."); + XCTAssertFalse([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:@[dsCertificateString]], @"Should not verify w/ DS certificate."); + NSArray *bothCertificateStrings = @[dsCertificateString, rootCertificateString]; + XCTAssertTrue([STDSDirectoryServerCertificate _verifyCertificateChain:certChain withRootCertificates:bothCertificateStrings], @"Failed to verify w/ root certificate and ds certificate."); +} + +- (void)testVerifyPS256Signature { + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *jwsString = @"eyJhbGciOiJQUzI1NiIsIng1YyI6WyJNSUlEZVRDQ0FtR2dBd0lCQWdJUWJTNEM0QlNp" + "Zzd1dUo1dURwZVQ0V0RBTkJna3Foa2lHOXcwQkFRc0ZBREJITVJNd0VRWUtDWkltaVpQ" + "eUxHUUJHUllEWTI5dE1SY3dGUVlLQ1pJbWlaUHlMR1FCR1JZSFpYaGhiWEJzWlRFWE1C" + "VUdBMVVFQXd3T1VsTkJJRVY0WVcxd2JHVWdSRk13SGhjTk1UY3hNVEl4TVRFMU5EQXlX" + "aGNOTWpjeE1qTXhNVE16TURBd1dqQklNUk13RVFZS0NaSW1pWlB5TEdRQkdSWURZMjl0" + "TVJjd0ZRWUtDWkltaVpQeUxHUUJHUllIWlhoaGJYQnNaVEVZTUJZR0ExVUVBd3dQVWxO" + "QklFVjRZVzF3YkdVZ1FVTlRNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1J" + "SUJDZ0tDQVFFQWtOclBJQkRYTVU2ZmN5djVpK1FIUUFRK0s4Z3NDM0hKYjdGWWhZYXc4" + "aFhiTkphK3Q4cTBsREt3TFpnUVhZVitmZld4WEp2NUdHcmxaRTRHVTUybGZNRWVnVER6" + "WVRyUlEzdGVwZ0tGak1HZzZJeTZma2wxWk5zeDJnRW9uc25sU2hmekE5R0p3UlRtdEtQ" + "Ymsxcytod3gxSVU1QVQrQUllbE5xQmdjRjJ2RTVXMjUvU0dHQm9hUk9WZFVZeHFFVERn" + "Z00xejVjS1Y0WmpEWjgrbGg0b1ZCMDdia2FjNkxRZEhwSlVVeVNIL0VyMjBEWHgzMEt5" + "aTk3UGNpWEtUUytRS1hubW04aXZ5UkNtdXgyMlpvUFVpbmQyQktDNU9pRzRNd0FMaGFM" + "MloyazhDc1JkZnkrN2RnN3o0MVJwNkQwWmVFdnRhVXA0Ylg0YUtyYUw0clRmd0lEQVFB" + "Qm8yQXdYakFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBZEJn" + "TlZIUTRFRmdRVWt0d2Y2WnBUQ3hqWUt3L0JMVzZQZWlOWDRzd3dId1lEVlIwakJCZ3dG" + "b0FVdzRNQ25id0Q2bTJ3cG5vUTJzTkQ4R3J5UE40d0RRWUpLb1pJaHZjTkFRRUxCUUFE" + "Z2dFQkFHdU5IeHYvQlI2ajdsQ1B5c20xdWhyYmpCT3FkcmhKTVIvSWQ0ZEIyR3RkRVNj" + "bDNpckdQbVh5UTJTbmNUV2hOZnNnc0tEWldwNUJrNytPdG50eTBlTlVNazNoWkVxZ1lq" + "eGh6YXUwNDhYSGJzZkd2b0phTUdaWk5Ud1V2VFV6Mmhra2hncHg5eVFBS0lBMkx6Rktj" + "Z1loZWxQdTRHVzVydEV1eHUzSVM2V1l5M0QxR3RGM25hRVdralVyYThoUU9oT2wyUytD" + "WUhtUmQ2bEdrWHlrVkRhak1nZDJBSkZ6WGRLTHhUdDBPWXJXREdsVVN6R0FDUkJDZDV4" + "YlJtQVRJbGR0Y2NhR3FETjFjTld2MEkvYlBOOEVwS1M2QjBXYVpjUGFzSXRLV3BEQzg1" + "SncxR3JEeGRod29LSG94dFNHK29kaVR3QjV6TGJybjJPc1JFNWJWN0U9Il19" + "." + "eyJBQ1MgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFUKSI6eyJrdHkiOiJFQyIsImNydiI6" + "IlAtMjU2IiwieCI6Im1QVUtUX2JBV0dISWhnMFRwampxVnNQMXJYV1F1X3Z3Vk9ISHRO" + "a2RZb0EiLCJ5IjoiOEJRQXNJbUdlQVM0NmZ5V3c1TWhmR1RUMElqQnBGdzJTUzM0RHY0" + "SXJzIix9LCJTREsgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFDKSI6eyJrdHkiOiJFQyIs" + "ImNydiI6IlAtMjU2IiwieCI6IlplMmxvU1Yzd3Jyb0tVTl80emh3R2hDcW8zWGh1MXRk" + "NFFqZVE1d0lWUjAiLCJ5IjoiSGxMdGRYQVJZX2Y1NUEzZm56UWJQY202aGdyMzRNcDhw" + "LW51elFDRTBadyIsfSwiQUNTIFVSTCI6Imh0dHA6Ly9hY3NzZXJ2ZXIuZG9tYWlubmFt" + "ZS5jb20ifQ" + "." + "OiiD5pbwe_0wrG_j61LmUxidBzTbUHyZXrKE9efRylEgMJy0axYJLOUshIloiKMexPdo" + "yDdpZV5pMQR588q-" + "uuUfssKpDXj3dCrIlUCEwgs4yfIBAUwd2NHoDg20w25ek0NPH9RV_T867DAXIjDWyd2I" + "y0ZFvJeHSrRskyPY75PA_mAUIpahaW20rxJHHMyGuWx2byKxnIx6G14vXCOPp1xEv49K" + "xKWrgFLS3_GtVYuNDqQC5pleHd4drLKSbq1bjwqEM1osYEZvw9y-" + "f1vQYxAQ8GJEti9F_309GVCZSWe1oyNDY51mo-" + "BwiyojoCDPxQDOTp6g4lp656tUQNW16g"; + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:jwsString]; + + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + + XCTAssertTrue([STDSDirectoryServerCertificate verifyJSONWebSignature:jws withRootCertificates:@[certificateString]], @"Failed to validate correct PS256 signature."); +} + +- (void)testVerifyPS256SignatureFail { + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *jwsString = @"eyJhbGciOiJQUzI1NiIsIng1YyI6WyJNSUlEZVRDQ0FtR2dBd0lCQWdJUWJTNEM0QlNp" + "Zzd1dUo1dURwZVQ0V0RBTkJna3Foa2lHOXcwQkFRc0ZBREJITVJNd0VRWUtDWkltaVpQ" + "eUxHUUJHUllEWTI5dE1SY3dGUVlLQ1pJbWlaUHlMR1FCR1JZSFpYaGhiWEJzWlRFWE1C" + "VUdBMVVFQXd3T1VsTkJJRVY0WVcxd2JHVWdSRk13SGhjTk1UY3hNVEl4TVRFMU5EQXlX" + "aGNOTWpjeE1qTXhNVE16TURBd1dqQklNUk13RVFZS0NaSW1pWlB5TEdRQkdSWURZMjl0" + "TVJjd0ZRWUtDWkltaVpQeUxHUUJHUllIWlhoaGJYQnNaVEVZTUJZR0ExVUVBd3dQVWxO" + "QklFVjRZVzF3YkdVZ1FVTlRNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1J" + "SUJDZ0tDQVFFQWtOclBJQkRYTVU2ZmN5djVpK1FIUUFRK0s4Z3NDM0hKYjdGWWhZYXc4" + "aFhiTkphK3Q4cTBsREt3TFpnUVhZVitmZld4WEp2NUdHcmxaRTRHVTUybGZNRWVnVER6" + "WVRyUlEzdGVwZ0tGak1HZzZJeTZma2wxWk5zeDJnRW9uc25sU2hmekE5R0p3UlRtdEtQ" + "Ymsxcytod3gxSVU1QVQrQUllbE5xQmdjRjJ2RTVXMjUvU0dHQm9hUk9WZFVZeHFFVERn" + "Z00xejVjS1Y0WmpEWjgrbGg0b1ZCMDdia2FjNkxRZEhwSlVVeVNIL0VyMjBEWHgzMEt5" + "aTk3UGNpWEtUUytRS1hubW04aXZ5UkNtdXgyMlpvUFVpbmQyQktDNU9pRzRNd0FMaGFM" + "MloyazhDc1JkZnkrN2RnN3o0MVJwNkQwWmVFdnRhVXA0Ylg0YUtyYUw0clRmd0lEQVFB" + "Qm8yQXdYakFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBZEJn" + "TlZIUTRFRmdRVWt0d2Y2WnBUQ3hqWUt3L0JMVzZQZWlOWDRzd3dId1lEVlIwakJCZ3dG" + "b0FVdzRNQ25id0Q2bTJ3cG5vUTJzTkQ4R3J5UE40d0RRWUpLb1pJaHZjTkFRRUxCUUFE" + "Z2dFQkFHdU5IeHYvQlI2ajdsQ1B5c20xdWhyYmpCT3FkcmhKTVIvSWQ0ZEIyR3RkRVNj" + "bDNpckdQbVh5UTJTbmNUV2hOZnNnc0tEWldwNUJrNytPdG50eTBlTlVNazNoWkVxZ1lq" + "eGh6YXUwNDhYSGJzZkd2b0phTUdaWk5Ud1V2VFV6Mmhra2hncHg5eVFBS0lBMkx6Rktj" + "Z1loZWxQdTRHVzVydEV1eHUzSVM2V1l5M0QxR3RGM25hRVdralVyYThoUU9oT2wyUytD" + "WUhtUmQ2bEdrWHlrVkRhak1nZDJBSkZ6WGRLTHhUdDBPWXJXREdsVVN6R0FDUkJDZDV4" + "YlJtQVRJbGR0Y2NhR3FETjFjTld2MEkvYlBOOEVwS1M2QjBXYVpjUGFzSXRLV3BEQzg1" + "SncxR3JEeGRod29LSG94dFNHK29kaVR3QjV6TGJybjJPc1JFNWJWN0U9Il19" + "." + "eyJBQ1MgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFUKSI6eyJrdHkiOiJFQyIsImNydiI6" + "IlAtMjU2IiwieCI6Im1QVUtUX2JBV0dISWhnMFRwampxVnNQMXJYV1F1X3Z3Vk9ISHRO" + "a2RZb0EiLCJ5IjoiOEJRQXNJbUdlQVM0NmZ5V3c1TWhmR1RUMElqQnBGdzJTUzM0RHY0" + "SXJzIix9LCJTREsgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFDKSI6eyJrdHkiOiJFQyIs" + "ImNydiI6IlAtMjU2IiwieCI6IlplMmxvU1Yzd3Jyb0tVTl80emh3R2hDcW8zWGh1MXRk" + "NFFqZVE1d0lWUjAiLCJ5IjoiSGxMdGRYQVJZX2Y1NUEzZm56UWJQY202aGdyMzRNcDhw" + "LW51elFDRTBadyIsfSwiQUNTIFVSTCI6Imh0dHA6Ly9hY3NzZXJ2ZXIuZG9tYWlubmFt" + "ZS5jb20ifQ" + "." + "OiiD5pbwe_0wrG_j61LmUxidBzTbUHyZXrKE9efRylEgMJy0axYJLOUshIloiKMexPdo" + "yDdpZV5pMQR588q-" + "uuUfssKpDXj3dCrIlUCEwgs4yfIBAUwd2NHoDg20w25ek0NPH9RV_T867DAXIjDWyd2I" + "y0ZFvJeHSrRskyPY75PA_mAUIpahaW20rxJHHMyGuWx2byKxnIx6G14vXCOPp1xEv49K" + "xKWrgFLS3_GtVYuNDqQC5pleHd4drLKSbq1bjwqEM1osYEZvw9y-" + "f1vQYxAQ8GJEti9F_309GVCZSWe1oyNEY51mo-" // This is all the same as testVerifyPS256Signature, except this line where "NDY51mo-" became "NEY51mo-" + "BwiyojoCDPxQDOTp6g4lp656tUQNW16g"; + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:jwsString]; + + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + + XCTAssertFalse([STDSDirectoryServerCertificate verifyJSONWebSignature:jws withRootCertificates:@[certificateString]], @"Incorrectly validated PS256 signature."); +} + +- (void)testVerifyES256Signature { + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *jwsString = @"eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlDclRDQ0FaV2dBd0lCQWdJUWJTNEM0QlNp" + "Zzd1dUo1dURwZVQ0V1RBTkJna3Foa2lHOXcwQkFRc0ZBREJITVJNd0VRWUtDWkltaVpQ" + "eUxHUUJHUllEWTI5dE1SY3dGUVlLQ1pJbWlaUHlMR1FCR1JZSFpYaGhiWEJzWlRFWE1C" + "VUdBMVVFQXd3T1VsTkJJRVY0WVcxd2JHVWdSRk13SGhjTk1UY3hNVEl4TVRVME16STNX" + "aGNOTWpjeE1qTXhNVE16TURBd1dqQkhNUk13RVFZS0NaSW1pWlB5TEdRQkdSWURZMjl0" + "TVJjd0ZRWUtDWkltaVpQeUxHUUJHUllIWlhoaGJYQnNaVEVYTUJVR0ExVUVBd3dPUlVN" + "Z1JYaGhiWEJzWlNCQlExTXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FB" + "VGZvZml3YzZBaXUxWWc1dkc5ZUtYSGVEQ1ZoOWgzVk1xTjIveUoxQ1dHVWlwOEJqOHEr" + "ZXJPbzc0dHQ2akVUTXpBYVRwekxaNW9HRFpjZVpmR21NN2RvMkF3WGpBTUJnTlZIUk1C" + "QWY4RUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQWRCZ05WSFE0RUZnUVUwQVd0REhS" + "L3ZsUXJSQXo0YUtnSkJsbkZqRXN3SHdZRFZSMGpCQmd3Rm9BVXc0TUNuYndENm0yd3Bu" + "b1Eyc05EOEdyeVBONHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRXFsRVJld1VDZUV0" + "dEFrQzBGMTZIamp4ZnYxV2E4bmFEbWFSTDk5UTAvcXFVTjh3MHF3cEFQRjd3bjJhZkxm" + "YUdkKzV1WkViMVROWXdWOUF3OUwvczNCY1NURVJJbDZPRVduK3g3Y3RPbUh5MnZ2N21p" + "dGFVcmlsZUdvZGVubS9mYURkeTVWZ0tZaitLc01WTTJzTlZhZWtYK1Qwc3dBQ1g5Qjkw" + "dW5aeGE2MjU2dDJPSjJRVjV6dTNzWU8xTjBqOXY3K3lGK0ZneDAxNE5ydzcvWHQ4SUxH" + "RjU4TnhiUWhraGtmV1NmSHRhRTVtb0JBYldSdUZURmJrQmY0NVNLZTBVTWlVNUxhYzl4" + "STBPN1hDRCt6TkI1bXdzNE5PMkFZdnl4SHE5WCthNjRJaFhjbFhuZ1BRTXJVcU1vTFdJ" + "MTY2Z1JKU3ZRRVdzSUxJVXR4MndzaVlzPSJdfQ" + "." + "eyJBQ1MgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFUKSI6eyJrdHkiOiJFQyIsImNydiI6" + "IlAtMjU2IiwieCI6Im1QVUtUX2JBV0dISWhnMFRwampxVnNQMXJYV1F1X3Z3Vk9ISHRO" + "a2RZb0EiLCJ5IjoiOEJRQXNJbUdlQVM0NmZ5V3c1TWhmR1RUMElqQnBGdzJTUzM0RHY0" + "SXJzIix9LCJTREsgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFDKSI6eyJrdHkiOiJFQyIs" + "ImNydiI6IlAtMjU2IiwieCI6IlplMmxvU1Yzd3Jyb0tVTl80emh3R2hDcW8zWGh1MXRk" + "NFFqZVE1d0lWUjAiLCJ5IjoiSGxMdGRYQVJZX2Y1NUEzZm56UWJQY202aGdyMzRNcDhw" + "LW51elFDRTBadyIsfSwiQUNTIFVSTCI6Imh0dHA6Ly9hY3NzZXJ2ZXIuZG9tYWlubmFt" + "ZS5jb20ifQ" + "." + "KMNtxy3eZMDnVRK_UZvFtRY8fDuAAHJXHCaKncoViB3dy56u5VOT0XnB8K-" + "0nWwFhwWmwU1xGVwimBcfxNplCA"; + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:jwsString]; + + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + + XCTAssertTrue([STDSDirectoryServerCertificate verifyJSONWebSignature:jws withRootCertificates:@[certificateString]], @"Failed to verify valid ES256 signature."); +} + +- (void)testVerifyES256SignatureFail { + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *jwsString = @"eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlDclRDQ0FaV2dBd0lCQWdJUWJTNEM0QlNp" + "Zzd1dUo1dURwZVQ0V1RBTkJna3Foa2lHOXcwQkFRc0ZBREJITVJNd0VRWUtDWkltaVpQ" + "eUxHUUJHUllEWTI5dE1SY3dGUVlLQ1pJbWlaUHlMR1FCR1JZSFpYaGhiWEJzWlRFWE1C" + "VUdBMVVFQXd3T1VsTkJJRVY0WVcxd2JHVWdSRk13SGhjTk1UY3hNVEl4TVRVME16STNX" + "aGNOTWpjeE1qTXhNVE16TURBd1dqQkhNUk13RVFZS0NaSW1pWlB5TEdRQkdSWURZMjl0" + "TVJjd0ZRWUtDWkltaVpQeUxHUUJHUllIWlhoaGJYQnNaVEVYTUJVR0ExVUVBd3dPUlVN" + "Z1JYaGhiWEJzWlNCQlExTXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FB" + "VGZvZml3YzZBaXUxWWc1dkc5ZUtYSGVEQ1ZoOWgzVk1xTjIveUoxQ1dHVWlwOEJqOHEr" + "ZXJPbzc0dHQ2akVUTXpBYVRwekxaNW9HRFpjZVpmR21NN2RvMkF3WGpBTUJnTlZIUk1C" + "QWY4RUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQWRCZ05WSFE0RUZnUVUwQVd0REhS" + "L3ZsUXJSQXo0YUtnSkJsbkZqRXN3SHdZRFZSMGpCQmd3Rm9BVXc0TUNuYndENm0yd3Bu" + "b1Eyc05EOEdyeVBONHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRXFsRVJld1VDZUV0" + "dEFrQzBGMTZIamp4ZnYxV2E4bmFEbWFSTDk5UTAvcXFVTjh3MHF3cEFQRjd3bjJhZkxm" + "YUdkKzV1WkViMVROWXdWOUF3OUwvczNCY1NURVJJbDZPRVduK3g3Y3RPbUh5MnZ2N21p" + "dGFVcmlsZUdvZGVubS9mYURkeTVWZ0tZaitLc01WTTJzTlZhZWtYK1Qwc3dBQ1g5Qjkw" + "dW5aeGE2MjU2dDJPSjJRVjV6dTNzWU8xTjBqOXY3K3lGK0ZneDAxNE5ydzcvWHQ4SUxH" + "RjU4TnhiUWhraGtmV1NmSHRhRTVtb0JBYldSdUZURmJrQmY0NVNLZTBVTWlVNUxhYzl4" + "STBPN1hDRCt6TkI1bXdzNE5PMkFZdnl4SHE5WCthNjRJaFhjbFhuZ1BRTXJVcU1vTFdJ" + "MTY2Z1JKU3ZRRVdzSUxJVXR4MndzaVlzPSJdfQ" + "." + "eyJBQ1MgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFUKSI6eyJrdHkiOiJFQyIsImNydiI6" + "IlAtMjU2IiwieCI6Im1QVUtUX2JBV0dISWhnMFRwampxVnNQMXJYV1F1X3Z3Vk9ISHRO" + "a2RZb0EiLCJ5IjoiOEJRQXNJbUdlQVM0NmZ5V3c1TWhmR1RUMElqQnBGdzJTUzM0RHY0" + "SXJzIix9LCJTREsgRXBoZW1lcmFsIFB1YmxpYyBLZXkgKFFDKSI6eyJrdHkiOiJFQyIs" + "ImNydiI6IlAtMjU2IiwieCI6IlplMmxvU1Yzd3Jyb0tVTl80emh3R2hDcW8zWGh1MXRk" + "NFFqZVE1d0lWUjAiLCJ5IjoiSGxMdGRYQVJZX2Y1NUEzZm56UWJQY202aGdyMzRNcDhw" + "LW51elFDRTBadyIsfSwiQUNTIFVSTCI6Imh0dHA6Ly9hY3NzZXJ2ZXIuZG9tYWlubMFt" // This is all the same as testVerifyPS256Signature, except this line where "bmFt" became "bMFt" + "ZS5jb20ifQ" + "." + "KMNtxy3eZMDnVRK_UZvFtRY8fDuAAHJXHCaKncoViB3dy56u5VOT0XnB8K-" + "0nWwFhwWmwU1xGVwimBcfxNplCA"; + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:jwsString]; + + // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSString *certificateString = @"MIIDXTCCAkWgAwIBAgIQbS4C4BSig7uuJ5uDpeT4VjANBgkqhkiG9w0BAQsFADBH" + "MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEX" + "MBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwHhcNMTcxMTIxMTE0ODQ5WhcNMjcxMjMx" + "MTQwMDAwWjBHMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYH" + "ZXhhbXBsZTEXMBUGA1UEAwwOUlNBIEV4YW1wbGUgRFMwggEiMA0GCSqGSIb3DQEB" + "AQUAA4IBDwAwggEKAoIBAQCfgQ+0A4Jz0CWR5Ac/MdK2ABuCzttNkvBQFl1Hz8q4" + "o8Qct3isdVN5P475dXaNGiN02HElZMO813uepDRUSJlAfP8AmZIKkxokxEFIUqsp" + "vbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0CmvnJ1AuseqQceKDdEGit1pnoCP6gEe" + "oUQdik97tOl7459V8d3UTpxLozUVlwPU00tgPmUUek8j1tPAmWx17e6EaoLRkK4Q" + "eDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++eJQUArC4IuoqdLYFjB2r+bNKdstj" + "uH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzmAd2s0/nPAgMBAAGjRTBDMBIGA1Ud" + "EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTDgwKdvAPq" + "bbCmehDaw0PwavI83jANBgkqhkiG9w0BAQsFAAOCAQEAOUcKqpzNQ6lr0PbDSsns" + "D6onfi+8j3TD0xG0zBSf+8G4zs8Zb6vzzQ5qHKgfr4aeen8Pw0cw2KKUJ2dFaBqj" + "n3/6/MIZbgaBvXKUbmY8xCxKQ+tOFc3KWIu4pSaO50tMPJjU/lP35bv19AA9vs9M" + "TKY2qLf88bmoNYT3W8VSDcB58KBHa7HVIPx7BUUtSyb2N2Jqx5AOiYy4NarhB3hV" + "ftkZBmCzi2Qw50KWIgTFYcIVeRTx3Js/F0IuEdgZHBK2gmO7fdM7+QKYm83401vl" + "YRNCXfIZ0H9E1V3NddqJuqIutdUajckSzMhXdNCJqfI4FAQAymTWGL3/lZyr/30x" + "Fg=="; + + XCTAssertFalse([STDSDirectoryServerCertificate verifyJSONWebSignature:jws withRootCertificates:@[certificateString]], @"Verified invalid ES256 signature."); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSEllipticCurvePointTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSEllipticCurvePointTests.m new file mode 100644 index 00000000..a315dcd8 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSEllipticCurvePointTests.m @@ -0,0 +1,42 @@ +// +// STDSEllipticCurvePointTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 4/5/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSString+JWEHelpers.h" +#import "STDSEllipticCurvePoint.h" + +@interface STDSEllipticCurvePointTests : XCTestCase + +@end + +@implementation STDSEllipticCurvePointTests + +- (void)testInitWithJWK { + + STDSEllipticCurvePoint *ecPoint = [[STDSEllipticCurvePoint alloc] initWithJWK:@{ // ref. EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + @"kty":@"EC", + @"crv":@"P-256", + @"x":@"mPUKT_bAWGHIhg0TpjjqVsP1rXWQu_vwVOHHtNkdYoA", + @"y":@"8BQAsImGeAS46fyWw5MhYfGTT0IjBpFw2SS34Dv4Irs", + }]; + + XCTAssertNotNil(ecPoint, @"Failed to create point with valid jwk"); + XCTAssertEqualObjects(ecPoint.x, [@"mPUKT_bAWGHIhg0TpjjqVsP1rXWQu_vwVOHHtNkdYoA" _stds_base64URLDecodedData], @"Parsed incorrect x-coordinate"); + XCTAssertEqualObjects(ecPoint.y, [@"8BQAsImGeAS46fyWw5MhYfGTT0IjBpFw2SS34Dv4Irs" _stds_base64URLDecodedData], @"Parsed incorrect y-coordinate"); + + ecPoint = [[STDSEllipticCurvePoint alloc] initWithJWK:@{ + @"kty":@"EC", + @"crv":@"P-128", + @"x":@"mPUKT_bAWGHIhg0TpjjqVsP1rXWQu_vwVOHHtNkdYoA", + @"y":@"8BQAsImGeAS46fyWw5MhYfGTT0IjBpFw2SS34Dv4Irs", + }]; + XCTAssertNil(ecPoint, @"Shoud return nil for non P-256 curve."); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSEphemeralKeyPairTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSEphemeralKeyPairTests.m new file mode 100644 index 00000000..5d8cb194 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSEphemeralKeyPairTests.m @@ -0,0 +1,41 @@ +// +// STDSEphemeralKeyPairTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 3/26/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSDirectoryServerCertificate.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSEphemeralKeyPair+Testing.h" +#import "STDSSecTypeUtilities.h" + +@interface STDSEphemeralKeyPairTests : XCTestCase + +@end + +@implementation STDSEphemeralKeyPairTests + +- (void)testCreateEpehemeralKeyPair { + STDSEphemeralKeyPair *keyPair = [STDSEphemeralKeyPair ephemeralKeyPair]; + XCTAssertNotNil(keyPair.publicKeyJWK, @"Failed to create a valid public key JWK"); + STDSEphemeralKeyPair *keyPair2 = [STDSEphemeralKeyPair ephemeralKeyPair]; + XCTAssertNotEqual(keyPair.publicKeyJWK, keyPair2.publicKeyJWK, @"Failed sanity check that two different ephemeral key pairs don't have the same public key JWK."); +} + +- (void)testDiffieHellmanSharedSecret { + // values from EMVCo_3DS_-AppBased_CryptoExamples_082018.pdf + NSDictionary *jwk = [NSJSONSerialization JSONObjectWithData:[@"{\"kty\":\"EC\",\"crv\":\"P-256\",\"kid\":\"UUIDkeyidentifierforDS-EC\", \"x\":\"2_v-MuNZccqwM7PXlakW9oHLP5XyrjMG1UVS8OxYrgA\", \"y\":\"rm1ktLmFIsP2R0YyJGXtsCbaTUesUK31Xc04tHJRolc\"}" dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL]; + + STDSEllipticCurvePoint *publicKey = [[STDSEllipticCurvePoint alloc] initWithJWK:jwk]; + STDSEphemeralKeyPair *keyPair = [STDSEphemeralKeyPair testKeyPair]; + NSData *secret = [keyPair createSharedSecretWithEllipticCurveKey:publicKey]; + const unsigned char expectedSecretBytes[] = {0x5C, 0x32, 0xBC, 0x13, 0xF8, 0xEC, 0xEB, 0x14, 0x8A, 0xBA, 0xF2, 0xA6, 0xB9, 0xDD, 0x1F, 0x68, 0x91, 0xBB, 0x2A, 0x80, 0xAB, 0x09, 0x34, 0x7C, 0x64, 0x06, 0x82, 0x31, 0xA5, 0x9E, 0x8C, 0xA2}; + NSData *expectedSecret = [[NSData alloc] initWithBytes:expectedSecretBytes length:32]; + XCTAssertEqualObjects(secret, expectedSecret, @"Generated incorrect shared secret value"); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSErrorMessageTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSErrorMessageTest.m new file mode 100644 index 00000000..b55af754 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSErrorMessageTest.m @@ -0,0 +1,61 @@ +// +// STDSErrorMessageTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSErrorMessage.h" +#import "STDSJSONEncoder.h" +#import "STDSTestJSONUtils.h" + +@interface STDSErrorMessageTest : XCTestCase + +@end + +@implementation STDSErrorMessageTest + +#pragma mark - STDSJSONDecodable + +- (void)testSuccessfulDecode { + NSDictionary *json = [STDSTestJSONUtils jsonNamed:@"ErrorMessage"]; + NSError *error; + STDSErrorMessage *errorMessage = [STDSErrorMessage decodedObjectFromJSON:json error:&error]; + + XCTAssertNil(error); + XCTAssertNotNil(errorMessage); + XCTAssertEqualObjects(errorMessage.errorCode, @"203"); + XCTAssertEqualObjects(errorMessage.errorComponent, @"A"); + XCTAssertEqualObjects(errorMessage.errorDescription, @"Data element not in the required format. Not numeric or wrong length."); + XCTAssertEqualObjects(errorMessage.errorDetails, @"billAddrCountry,billAddrPostCode,dsURL"); + XCTAssertEqualObjects(errorMessage.errorMessageType, @"AReq"); + XCTAssertEqualObjects(errorMessage.messageVersion, @"2.1.0"); + +} + +#pragma mark - STDSJSONEncodable + +- (void)testPropertyNamesToJSONKeysMapping { + STDSErrorMessage *params = [STDSErrorMessage new]; + + NSDictionary *mapping = [STDSErrorMessage propertyNamesToJSONKeysMapping]; + + for (NSString *propertyName in [mapping allKeys]) { + XCTAssertFalse([propertyName containsString:@":"]); + XCTAssert([params respondsToSelector:NSSelectorFromString(propertyName)]); + } + + for (NSString *formFieldName in [mapping allValues]) { + XCTAssert([formFieldName isKindOfClass:[NSString class]]); + XCTAssert([formFieldName length] > 0); + } + + XCTAssertEqual([[mapping allValues] count], [[NSSet setWithArray:[mapping allValues]] count]); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:[STDSJSONEncoder dictionaryForObject:params]]); +} + + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSJSONEncoderTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSJSONEncoderTest.m new file mode 100644 index 00000000..a42c70d9 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSJSONEncoderTest.m @@ -0,0 +1,201 @@ +// +// STDSJSONEncoderTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/25/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSJSONEncoder.h" + +#pragma mark - STDSJSONEncodableObject + +@interface STDSJSONEncodableObject : NSObject +@property (nonatomic, copy) NSString *testProperty; +@property (nonatomic, copy) NSArray *testArrayProperty; +@property (nonatomic, copy) NSDictionary *testDictionaryProperty; +@property (nonatomic) STDSJSONEncodableObject *testNestedObjectProperty; +@end + +@implementation STDSJSONEncodableObject + ++ (NSDictionary *)propertyNamesToJSONKeysMapping { + return @{ + NSStringFromSelector(@selector(testProperty)): @"test_property", + NSStringFromSelector(@selector(testArrayProperty)): @"test_array_property", + NSStringFromSelector(@selector(testDictionaryProperty)): @"test_dictionary_property", + NSStringFromSelector(@selector(testNestedObjectProperty)): @"test_nested_property", + }; +} + +@end + +#pragma mark - STDSJSONEncoderTest + +@interface STDSJSONEncoderTest : XCTestCase +@end + +@implementation STDSJSONEncoderTest + +- (void)testEmptyEncodableObject { + STDSJSONEncodableObject *object = [STDSJSONEncodableObject new]; + XCTAssertEqualObjects([STDSJSONEncoder dictionaryForObject:object], @{}); +} + +- (void)testNestedObject { + STDSJSONEncodableObject *object = [STDSJSONEncodableObject new]; + STDSJSONEncodableObject *nestedObject = [STDSJSONEncodableObject new]; + nestedObject.testProperty = @"nested_object_property"; + object.testProperty = @"object_property"; + object.testNestedObjectProperty = nestedObject; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:object]; + NSDictionary *expected = @{ + @"test_property": @"object_property", + @"test_nested_property": @{ + @"test_property": @"nested_object_property", + } + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); +} + +- (void)testSerializeDeserialize { + STDSJSONEncodableObject *object = [STDSJSONEncodableObject new]; + object.testProperty = @"test"; + NSDictionary *expected = @{@"test_property": @"test"}; + NSData *data = [NSJSONSerialization dataWithJSONObject:[STDSJSONEncoder dictionaryForObject:object] options:0 error:nil]; + id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + XCTAssertEqualObjects(expected, jsonObject); +} + +- (void)testBoolAndNumbers { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + testObject.testArrayProperty = @[@0, + @1, + [NSNumber numberWithBool:NO], + [[NSNumber alloc] initWithBool:YES], + @YES]; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_array_property": @[ + @0, + @1, + [NSNumber numberWithBool:NO], + [[NSNumber alloc] initWithBool:YES], + @YES], + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); + +} + +#pragma mark NSArray + +- (void)testArrayValueEmpty { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + testObject.testProperty = @"success"; + testObject.testArrayProperty = @[]; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_property": @"success", + @"test_array_property": @[] + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); +} + +- (void)testArrayValue { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + testObject.testProperty = @"success"; + testObject.testArrayProperty = @[@1, @2, @3]; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_property": @"success", + @"test_array_property": @[@1, @2, @3] + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); + +} + +- (void)testArrayOfEncodable { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + + STDSJSONEncodableObject *inner1 = [STDSJSONEncodableObject new]; + inner1.testProperty = @"inner1"; + STDSJSONEncodableObject *inner2 = [STDSJSONEncodableObject new]; + inner2.testArrayProperty = @[@"inner2"]; + + testObject.testArrayProperty = @[inner1, inner2]; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_array_property": @[ + @{ + @"test_property": @"inner1" + }, + @{ + @"test_array_property": @[@"inner2"] + } + ] + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); +} + +#pragma mark NSDictionary + +- (void)testDictionaryValueEmpty { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + testObject.testProperty = @"success"; + testObject.testDictionaryProperty = @{}; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_property": @"success", + @"test_dictionary_property": @{} + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); +} + +- (void)testDictionaryValue { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + testObject.testProperty = @"success"; + testObject.testDictionaryProperty = @{@"foo": @"bar"}; + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_property": @"success", + @"test_dictionary_property": @{@"foo": @"bar"} + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); + +} + +- (void)testDictionaryOfEncodable { + STDSJSONEncodableObject *testObject = [STDSJSONEncodableObject new]; + + STDSJSONEncodableObject *inner1 = [STDSJSONEncodableObject new]; + inner1.testProperty = @"inner1"; + STDSJSONEncodableObject *inner2 = [STDSJSONEncodableObject new]; + inner2.testArrayProperty = @[@"inner2"]; + + testObject.testDictionaryProperty = @{@"one": inner1, @"two": inner2}; + + NSDictionary *jsonDictionary = [STDSJSONEncoder dictionaryForObject:testObject]; + NSDictionary *expected = @{ + @"test_dictionary_property": @{ + @"one": @{ + @"test_property": @"inner1" + }, + @"two": @{ + @"test_array_property": @[@"inner2"] + } + } + }; + XCTAssertEqualObjects(jsonDictionary, expected); + XCTAssertTrue([NSJSONSerialization isValidJSONObject:jsonDictionary]); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebEncryptionTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebEncryptionTests.m new file mode 100644 index 00000000..9e7cf928 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebEncryptionTests.m @@ -0,0 +1,119 @@ +// +// STDSJSONWebEncryptionTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSStripe3DS2Error.h" +#import "NSString+JWEHelpers.h" +#import "STDSDirectoryServer.h" +#import "STDSDirectoryServerCertificate.h" +#import "STDSJSONWebEncryption.h" +#import "STDSSecTypeUtilities.h" + +@interface STDSJSONWebEncryptionTests : XCTestCase + +@end + +@implementation STDSJSONWebEncryptionTests + +- (void)testEncryption { + NSDictionary *json = @{@"a": @(0), @"b": @[@(0), @(1), @(2)], @"c": @{@"d": @"val"}}; + NSError *error = nil; + + NSString *encrypted = [STDSJSONWebEncryption encryptJSON:json forDirectoryServer:STDSDirectoryServerSTPTestRSA error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with ul_test cert."); + XCTAssertNil(error, @"Successful encryption should not populate error."); + + encrypted = [STDSJSONWebEncryption encryptJSON:json forDirectoryServer:STDSDirectoryServerSTPTestEC error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with ec_test cert."); + XCTAssertNil(error, @"Successful encryption should not populate error."); + + encrypted = [STDSJSONWebEncryption encryptJSON:json forDirectoryServer:STDSDirectoryServerULTestRSA error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with STDSDirectoryServerULTestRSA."); + XCTAssertNil(error, @"Successful encryption should not populate error."); + + encrypted = [STDSJSONWebEncryption encryptJSON:json forDirectoryServer:STDSDirectoryServerULTestEC error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with STDSDirectoryServerULTestEC."); + XCTAssertNil(error, @"Successful encryption should not populate error."); + + encrypted = [STDSJSONWebEncryption encryptJSON:json forDirectoryServer:STDSDirectoryServerUnknown error:&error]; + XCTAssertNil(encrypted, @"Invalid server ID should return nil."); + XCTAssertEqual(error.code, STDSErrorCodeDecryptionVerification, @"Failed encryption should provide a STDSErrorCodeDecryptionVerification error."); +} + +- (void)testEncryptionWithCustomCertificate { + NSDictionary *json = @{@"a": @(0), @"b": @[@(0), @(1), @(2)], @"c": @{@"d": @"val"}}; + NSError *error = nil; + // Using ds-amex.pm.PEM + NSString *certificateString = @"MIIE0TCCA7mgAwIBAgIUXbeqM1duFcHk4dDBwT8o7Ln5wX8wDQYJKoZIhvcNAQEL" + "BQAwXjELMAkGA1UEBhMCVVMxITAfBgNVBAoTGEFtZXJpY2FuIEV4cHJlc3MgQ29t" + "cGFueTEsMCoGA1UEAxMjQW1lcmljYW4gRXhwcmVzcyBTYWZla2V5IElzc3Vpbmcg" + "Q0EwHhcNMTgwMjIxMjM0OTMxWhcNMjAwMjIxMjM0OTMwWjCB0DELMAkGA1UEBhMC" + "VVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazE/MD0GA1UE" + "ChM2QW1lcmljYW4gRXhwcmVzcyBUcmF2ZWwgUmVsYXRlZCBTZXJ2aWNlcyBDb21w" + "YW55LCBJbmMuMTkwNwYDVQQLEzBHbG9iYWwgTmV0d29yayBUZWNobm9sb2d5IC0g" + "TmV0d29yayBBUEkgUGxhdGZvcm0xHzAdBgNVBAMTFlNESy5TYWZlS2V5LkVuY3J5" + "cHRLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSFF9kTYbwRrxX" + "C6WcJJYio5TZDM62+CnjQRfggV3GMI+xIDtMIN8LL/jbWBTycu97vrNjNNv+UPhI" + "WzhFDdUqyRfrY337A39uE8k1xhdDI3dNeZz6xgq8r9hn2NBou78YPBKidpN5oiHn" + "TxcFq1zudut2fmaldaa9a4ZKgIQo+02heiJfJ8XNWkoWJ17GcjJ59UU8C1KF/y1G" + "ymYO5ha2QRsVZYI17+ZFsqnpcXwK4Mr6RQKV6UimmO0nr5++CgvXfekcWAlLV6Xq" + "juACWi3kw0haepaX/9qHRu1OSyjzWNcSVZ0On6plB5Lq6Y9ylgmxDrv+zltz3MrT" + "K7txIAFFAgMBAAGjggESMIIBDjAMBgNVHRMBAf8EAjAAMCEGA1UdEQQaMBiCFlNE" + "Sy5TYWZlS2V5LkVuY3J5cHRLZXkwRQYJKwYBBAGCNxQCBDgeNgBBAE0ARQBYAF8A" + "UwBBAEYARQBLAEUAWQAyAF8ARABTAF8ARQBOAEMAUgBZAFAAVABJAE8ATjAOBgNV" + "HQ8BAf8EBAMCBJAwHwYDVR0jBBgwFoAU7k/rXuVMhTBxB1zSftPgmLFuDIgwRAYD" + "VR0fBD0wOzA5oDegNYYzaHR0cDovL2FtZXhzay5jcmwuY29tLXN0cm9uZy1pZC5u" + "ZXQvYW1leHNhZmVrZXkuY3JsMB0GA1UdDgQWBBQHclVTo5nwZGH8labJ2F2P45xi" + "fDANBgkqhkiG9w0BAQsFAAOCAQEAWY6b77VBoGLs3k5vOqSU7QRqT+4v6y77T8LA" + "BKrSZ58DiVZWVyDSxyftQUiRRgFHt2gTN0yfJTP50Fyp84nCEWC0tugZ4iIhgPss" + "HzL+4/u4eG/MTzK2ESxvPgr6YHajyuU+GXA89u8+bsFrFmojOjhTgFKli7YUeV/0" + "xoiYZf2utlns800ofJrcrfiFoqE6PvK4Od0jpeMgfSKv71nK5ihA1+wTk76ge1fs" + "PxL23hEdRpWW11ofaLfJGkLFXMM3/LHSXWy7HhsBgDELdzLSHU4VkSv8yTOZxsRO" + "ByxdC5v3tXGcK56iQdtKVPhFGOOEBugw7AcuRzv3f1GhvzAQZg=="; + STDSDirectoryServerCertificate *certificate = [STDSDirectoryServerCertificate customCertificateWithData:[[NSData alloc] initWithBase64EncodedString:certificateString options:0]]; + + NSString *encrypted = [STDSJSONWebEncryption encryptJSON:json + withCertificate:certificate + directoryServerID:@"custom_test" + serverKeyID:nil + error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with custom cert."); + XCTAssertNil(error, @"Successful encryption should not populate error."); +} + +- (void)testDirectEncryptionWithKey { + NSDictionary *json = @{@"a": @(0), @"b": @[@(0), @(1), @(2)], @"c": @{@"d": @"val"}}; + NSError *error = nil; + NSData *contentEncryptionKey = STDSCryptoRandomData(32); + NSString *encrypted = [STDSJSONWebEncryption directEncryptJSON:json + withContentEncryptionKey:contentEncryptionKey + forACSTransactionID:@"acs_id" + error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with direct encryption key."); + XCTAssertNil(error, @"Successful encryption should not populate error."); +} + +- (void)testDecryption { + NSDictionary *json = @{@"a": @(0), @"b": @[@(0), @(1), @(2)], @"c": @{@"d": @"val"}}; + NSError *error = nil; + NSData *contentEncryptionKey = STDSCryptoRandomData(32); + NSString *encrypted = [STDSJSONWebEncryption directEncryptJSON:json + withContentEncryptionKey:contentEncryptionKey + forACSTransactionID:@"acs_id" + error:&error]; + XCTAssertNotNil(encrypted, @"Should successfully encrypt with direct encryption key."); + XCTAssertNil(error, @"Successful encryption should not populate error."); + + NSDictionary *decrypted = [STDSJSONWebEncryption decryptData:[encrypted dataUsingEncoding:NSUTF8StringEncoding] + withContentEncryptionKey:contentEncryptionKey + error:&error]; + XCTAssertEqualObjects(decrypted, json, @"Decrypted is not equal to the encrypted json"); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebSignatureTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebSignatureTests.m new file mode 100644 index 00000000..c4587102 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSJSONWebSignatureTests.m @@ -0,0 +1,78 @@ +// +// STDSJSONWebSignatureTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 4/2/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSString+JWEHelpers.h" +#import "STDSEllipticCurvePoint.h" +#import "STDSJSONWebSignature.h" + +@interface STDSJSONWebSignatureTests : XCTestCase + +@end + +@implementation STDSJSONWebSignatureTests + +- (void)testInitES256 { + + // generated a private ec key and certificate, plugged into jwt.io with default sample payload. + // This certificate will expire in 2030 but as this test doesn't cover certificate validity + // it shouldn't start failing + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:@"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUh3TUlHV0Fna0ErdEM1LzJxV1RqRXdDZ1lJS29aSXpqMEVBd0l3QURBZUZ3MHlNVEF4TURReU1UQTBNamxhRncwek1UQXhNREl5TVRBME1qbGFNQUF3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFSV3oram42NUJ0T012ZHlIS2N2akJlQlNEWkgycjFSVHdqbVlTaTlSL3pwQm51UTRFaU1uQ3FmTVBXaVpxQjRRZGJBZDBFN29INTBWcHVaMVAwODdHTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDSVFETTVRbHRDTFhEeEpvTG1EVXRqREgxZEJQVHBUVG1jS2pjOHlodVp1VHU2UUloQVBEU0cvN3plV09NdkhxNUpaWk8zd3JQeVBhTFlVNHBCcGpWTS95YzQ5MDciXX0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.71MhQ7FJavv1nQ7Boujfp7K0iBEYFGSGLZ3osnL9KAY9scF95Hf7ZMQ8I1JSgnGl227UY96is80MlbTijOOxsg"]; + + XCTAssertNotNil(jws, @"Failed to create jws object"); + + XCTAssertEqual(jws.algorithm, STDSJSONWebSignatureAlgorithmES256, @"Parsed incorrect algorithm"); + + NSData *digest = [@"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUh3TUlHV0Fna0ErdEM1LzJxV1RqRXdDZ1lJS29aSXpqMEVBd0l3QURBZUZ3MHlNVEF4TURReU1UQTBNamxhRncwek1UQXhNREl5TVRBME1qbGFNQUF3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFSV3oram42NUJ0T012ZHlIS2N2akJlQlNEWkgycjFSVHdqbVlTaTlSL3pwQm51UTRFaU1uQ3FmTVBXaVpxQjRRZGJBZDBFN29INTBWcHVaMVAwODdHTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDSVFETTVRbHRDTFhEeEpvTG1EVXRqREgxZEJQVHBUVG1jS2pjOHlodVp1VHU2UUloQVBEU0cvN3plV09NdkhxNUpaWk8zd3JQeVBhTFlVNHBCcGpWTS95YzQ5MDciXX0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(jws.digest, digest, @"Parsed payload incorrectly."); + NSData *signature = [@"71MhQ7FJavv1nQ7Boujfp7K0iBEYFGSGLZ3osnL9KAY9scF95Hf7ZMQ8I1JSgnGl227UY96is80MlbTijOOxsg" _stds_base64URLDecodedData]; + XCTAssertEqualObjects(jws.signature, signature, @"Parsed signature incorrectly."); + NSData *payload = [@"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0" _stds_base64URLDecodedData]; + XCTAssertEqualObjects(jws.payload, payload, @"Parsed payload incorrectly."); + + XCTAssertNotNil(jws.ellipticCurvePoint, @"Failed to parse elliptic curve point."); + XCTAssertNotNil(jws.certificateChain, @"Must have certificate chain."); + + const unsigned char keyBytes[] = {0x11, 0x5b, 0x3f, 0xa3, 0x9f, 0xae, 0x41, 0xb4, 0xe3, 0x2f, 0x77, 0x21, 0xca, 0x72, + 0xf8, 0xc1, 0x78, 0x14, 0x83, 0x64, 0x7d, 0xab, 0xd5, 0x14, 0xf0, 0x8e, 0x66, 0x12, 0x8b, + 0xd4, 0x7f, 0xce, 0x90, 0x67, 0xb9, 0x0e, 0x04, 0x88, 0xc9, 0xc2, 0xa9, 0xf3, 0x0f, 0x5a, + 0x26, 0x6a, 0x07, 0x84, 0x1d, 0x6c, 0x07, 0x74, 0x13, 0xba, 0x07, 0xe7, 0x45, 0x69, 0xb9, + 0x9d, 0x4f, 0xd3, 0xce, 0xc6}; + size_t keyLength = sizeof(keyBytes)/2; + NSData *coordinateX = [NSData dataWithBytes:keyBytes length:keyLength]; + NSData *coordinateY = [NSData dataWithBytes:keyBytes + keyLength length:keyLength]; + + XCTAssertEqualObjects(jws.ellipticCurvePoint.x, coordinateX, @"Incorrect x-point."); + XCTAssertEqualObjects(jws.ellipticCurvePoint.y, coordinateY, @"Incorrect y-point."); +} + +- (void)testInitPS256 { + + // test jws strings generated from jwt.io + STDSJSONWebSignature *jws = [[STDSJSONWebSignature alloc] initWithString:@"eyJhbGciOiJQUzI1NiIsIng1YyI6WyJNSUkiLCJNSUkyIl19.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.CDB63_lCSBlmrIfZBvn6w4rDKJgkmKhFe4mfR-xUnfxf9N0g4vZa0R9lFG5pjVkThq9CX-p-vM_64wG4bAC53VlXOk6DhjzN0LTCo1nB81rd8DgqMH4SkLFy3wP-Xe0akRmXE8iHmv63ip7d2LGQVCD38xwXOnoBUVANCrcsC0Iur1TTEXaEfT6ACwg3V1YTu-vygNdbhYZOC_Q9ESbaoPxOQfumXnD44m1EN_FV3d-uQJx1Rn6w3AkDw34P3KunLrwOMJt1mbkWzb66VDVsIxegc4N8TjJTzvxmCk841wUae3kZ97_HPIEfil3ewv80hZstEE2hcEXJbdBfsxsSqg"]; + + XCTAssertNotNil(jws, @"Failed to create jws object"); + + XCTAssertEqual(jws.algorithm, STDSJSONWebSignatureAlgorithmPS256, @"Parsed incorrect algorithm"); + + NSData *digest = [@"eyJhbGciOiJQUzI1NiIsIng1YyI6WyJNSUkiLCJNSUkyIl19.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(jws.digest, digest, @"Parsed payload incorrectly."); + NSData *signature = [@"CDB63_lCSBlmrIfZBvn6w4rDKJgkmKhFe4mfR-xUnfxf9N0g4vZa0R9lFG5pjVkThq9CX-p-vM_64wG4bAC53VlXOk6DhjzN0LTCo1nB81rd8DgqMH4SkLFy3wP-Xe0akRmXE8iHmv63ip7d2LGQVCD38xwXOnoBUVANCrcsC0Iur1TTEXaEfT6ACwg3V1YTu-vygNdbhYZOC_Q9ESbaoPxOQfumXnD44m1EN_FV3d-uQJx1Rn6w3AkDw34P3KunLrwOMJt1mbkWzb66VDVsIxegc4N8TjJTzvxmCk841wUae3kZ97_HPIEfil3ewv80hZstEE2hcEXJbdBfsxsSqg" _stds_base64URLDecodedData]; + XCTAssertEqualObjects(jws.signature, signature, @"Parsed signature incorrectly."); + NSData *payload = [@"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0" _stds_base64URLDecodedData]; + XCTAssertEqualObjects(jws.payload, payload, @"Parsed payload incorrectly."); + + XCTAssertNil(jws.ellipticCurvePoint, @"Should not create elliptic curve point."); + + NSArray *certChain = @[@"MII", @"MII2"]; + XCTAssertEqualObjects(jws.certificateChain, certChain, @"Failed to parse x5c correctly."); + + +} +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSSecTypeUtilitiesTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSSecTypeUtilitiesTests.m new file mode 100644 index 00000000..9c4c3b2d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSSecTypeUtilitiesTests.m @@ -0,0 +1,142 @@ +// +// STDSSecTypeUtilitiesTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/28/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "NSString+JWEHelpers.h" +#import "STDSSecTypeUtilities.h" + +@interface STDSSecTypeUtilitiesTests : XCTestCase + +@end + +@implementation STDSSecTypeUtilitiesTests + +- (void)testDirectoryServerForID { + XCTAssertEqual(STDSDirectoryServerForID(@"ul_test"), STDSDirectoryServerSTPTestRSA, @"ul_test should map to STDSDirectoryServerSTPTestRSA."); + XCTAssertEqual(STDSDirectoryServerForID(@"ec_test"), STDSDirectoryServerSTPTestEC, @"ec_test should map to STDSDirectoryServerSTPTestEC."); + XCTAssertEqual(STDSDirectoryServerForID(@"F055545342"), STDSDirectoryServerULTestRSA, @"F055545342 should map to STDSDirectoryServerULTestRSA."); + XCTAssertEqual(STDSDirectoryServerForID(@"F155545342"), STDSDirectoryServerULTestEC, @"F155545342 should map to STDSDirectoryServerULTestEC."); + + XCTAssertEqual(STDSDirectoryServerForID(@"junk"), STDSDirectoryServerUnknown, @"junk server ID should map to STDSDirectoryServerUnknown."); +} + +- (void)testCertificateForServer { + SecCertificateRef certificate = NULL; + + certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestRSA); + XCTAssertTrue(certificate != NULL, @"Unable to load STDSDirectoryServerSTPTestRSA certificate."); + if (certificate != NULL) { + CFRelease(certificate); + } + certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestEC); + XCTAssertTrue(certificate != NULL, @"Unable to load STDSDirectoryServerSTPTestEC certificate."); + if (certificate != NULL) { + CFRelease(certificate); + } + certificate = STDSCertificateForServer(STDSDirectoryServerUnknown); + if (certificate != NULL) { + XCTFail(@"Should not have an unknown certificate."); + CFRelease(certificate); + } +} + +- (void)testCopyPublicRSAKey { + SecCertificateRef certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestRSA); + if (certificate != NULL) { + SecKeyRef publicKey = SecCertificateCopyKey(certificate); + if (publicKey != NULL) { + CFRelease(publicKey); + } else { + XCTFail(@"Unable to load public key from certificate"); + } + + CFRelease(certificate); + } else { + XCTFail(@"Failed loading certificate for %@", NSStringFromSelector(_cmd)); + } +} + +- (void)testCopyPublicECKey { + SecCertificateRef certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestEC); + if (certificate != NULL) { + SecKeyRef publicKey = SecCertificateCopyKey(certificate); + if (publicKey != NULL) { + CFRelease(publicKey); + } else { + XCTFail(@"Unable to load public key from certificate"); + } + + CFRelease(certificate); + } else { + XCTFail(@"Failed loading certificate for %@", NSStringFromSelector(_cmd)); + } +} + +- (void)testCopyKeyTypeRSA { + SecCertificateRef certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestRSA); + if (certificate != NULL) { + CFStringRef keyType = STDSSecCertificateCopyPublicKeyType(certificate); + if (keyType != NULL) { + XCTAssertTrue(CFStringCompare(keyType, kSecAttrKeyTypeRSA, 0) == kCFCompareEqualTo, @"Key type is incorrect"); + CFRelease(keyType); + } else { + XCTFail(@"Failed to copy key type."); + } + CFRelease(certificate); + } else { + XCTFail(@"Failed loading certificate for %@", NSStringFromSelector(_cmd)); + } +} + +- (void)testCopyKeyTypeEC { + SecCertificateRef certificate = STDSCertificateForServer(STDSDirectoryServerSTPTestEC); + if (certificate != NULL) { + CFStringRef keyType = STDSSecCertificateCopyPublicKeyType(certificate); + if (keyType != NULL) { + XCTAssertTrue(CFStringCompare(keyType, kSecAttrKeyTypeECSECPrimeRandom, 0) == kCFCompareEqualTo, @"Key type is incorrect"); + CFRelease(keyType); + } else { + XCTFail(@"Failed to copy key type."); + } + CFRelease(certificate); + } else { + XCTFail(@"Failed loading certificate for %@", NSStringFromSelector(_cmd)); + } +} + +- (void)testRandomData { + // We're not actually going to implement randomness tests... just sanity + NSData *data1 = STDSCryptoRandomData(32); + NSData *data2 = STDSCryptoRandomData(32); + + XCTAssertNotNil(data1); + XCTAssertTrue(data1.length == 32, @"Random data is not correct length."); + XCTAssertNotEqualObjects(data1, data2, @"Sanity check: two random data's should not equate to equal (unless you get reeeeallly unlucky."); + XCTAssertTrue(STDSCryptoRandomData(12).length == 12, @"Random data is not correct length."); + XCTAssertNotNil(STDSCryptoRandomData(0), @"Empty random data should return empty data."); + XCTAssertTrue(STDSCryptoRandomData(0).length == 0, @"Empty random data should have length 0"); +} + +- (void)testConcatKDFWithSHA256 { + NSData *data = STDSCreateConcatKDFWithSHA256(STDSCryptoRandomData(32), 256, @"acs_identifier"); + XCTAssertNotNil(data, @"Failed to concat KDF and hash."); + XCTAssertEqual(data.length, 256, @"Concat returned data of incorrect length"); +} + + +- (void)testVerifyEllipticCurveP256 { + NSData *payload = [@"eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *signature = [@"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" _stds_base64URLDecodedData]; + + NSData *coordinateX = [@"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU" _stds_base64URLDecodedData]; + NSData *coordinateY = [@"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" _stds_base64URLDecodedData]; + + XCTAssertTrue(STDSVerifyEllipticCurveP256Signature(coordinateX, coordinateY, payload, signature)); +} +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSSynchronousLocationManagerTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSSynchronousLocationManagerTests.m new file mode 100644 index 00000000..65e2f9a2 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSSynchronousLocationManagerTests.m @@ -0,0 +1,28 @@ +// +// STDSSynchronousLocationManagerTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/24/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSSynchronousLocationManager.h" + +@interface STDSSynchronousLocationManagerTests : XCTestCase + +@end + +@implementation STDSSynchronousLocationManagerTests + +- (void)testLocationFetchIsSynchronous { + id originalLocation = [[NSObject alloc] init]; + id location = originalLocation; + + location = [[STDSSynchronousLocationManager sharedManager] deviceLocation]; + // tests that location gets synchronously updated (even if it's to nil due to permissions while running tests) + XCTAssertNotEqual(originalLocation, location); +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.h b/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.h new file mode 100644 index 00000000..c05e729a --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.h @@ -0,0 +1,19 @@ +// +// STDSTestJSONUtils.h +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STDSTestJSONUtils : NSObject + ++ (NSDictionary *)jsonNamed:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.m b/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.m new file mode 100644 index 00000000..f742e1fc --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSTestJSONUtils.m @@ -0,0 +1,54 @@ +// +// STDSTestJSONUtils.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/29/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import "STDSTestJSONUtils.h" + +@implementation STDSTestJSONUtils + ++ (NSDictionary *)jsonNamed:(NSString *)name { + NSData *data = [self dataFromJSONFile:name]; + if (data != nil) { + return [NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)kNilOptions error:nil]; + } + return nil; +} + ++ (NSBundle *)testBundle { + return [NSBundle bundleForClass:[STDSTestJSONUtils class]]; +} + ++ (NSData *)dataFromJSONFile:(NSString *)name { + NSBundle *bundle = [self testBundle]; + NSString *path = [bundle pathForResource:name ofType:@"json"]; + + if (!path) { + // Missing JSON file + return nil; + } + + NSError *error = nil; + NSString *jsonString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + + if (!jsonString) { + // File read error + return nil; + } + + // Strip all lines that begin with `//` + NSMutableArray *jsonLines = [[NSMutableArray alloc] init]; + + for (NSString *line in [jsonString componentsSeparatedByString:@"\n"]) { + if (![line hasPrefix:@"//"]) { + [jsonLines addObject:line]; + } + } + + return [[jsonLines componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSThreeDS2ServiceTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSThreeDS2ServiceTests.m new file mode 100644 index 00000000..237b5127 --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSThreeDS2ServiceTests.m @@ -0,0 +1,62 @@ +// +// STDSThreeDS2ServiceTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 1/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSAlreadyInitializedException.h" +#import "STDSConfigParameters.h" +#import "STDSInvalidInputException.h" +#import "STDSThreeDS2Service.h" +#import "STDSNotInitializedException.h" + +@interface STDSThreeDS2ServiceTests : XCTestCase + +@end + +@implementation STDSThreeDS2ServiceTests + +- (void)testInitialize { + STDSThreeDS2Service *service = [[STDSThreeDS2Service alloc] init]; + XCTAssertNoThrow([service initializeWithConfig:[[STDSConfigParameters alloc] init] + locale:nil + uiSettings:nil], + @"Should not throw with valid input and first call to initialize"); + + XCTAssertThrowsSpecific([service initializeWithConfig:[[STDSConfigParameters alloc] init] + locale:nil + uiSettings:nil], + STDSAlreadyInitializedException, + @"Should throw STDSAlreadyInitializedException if called again with valid input."); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrowsSpecific([service initializeWithConfig:nil + locale:nil + uiSettings:nil], + STDSInvalidInputException, + @"Should throw STDSInvalidInputException for nil config even if already initialized."); + + service = [[STDSThreeDS2Service alloc] init]; + XCTAssertThrowsSpecific([service initializeWithConfig:nil + locale:nil + uiSettings:nil], + STDSInvalidInputException, + @"Should throw STDSInvalidInputException for nil config on first initialize."); +#pragma clang diagnostic pop + + XCTAssertNoThrow([service initializeWithConfig:[[STDSConfigParameters alloc] init] + locale:nil + uiSettings:nil], + @"Should not throw with valid input and first call to initialize even after invalid input"); + + service = [[STDSThreeDS2Service alloc] init]; + XCTAssertThrowsSpecific(service.warnings, STDSNotInitializedException); + +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSTransactionTest.m b/Stripe3DS2/Stripe3DS2Tests/STDSTransactionTest.m new file mode 100644 index 00000000..3dd1187c --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSTransactionTest.m @@ -0,0 +1,157 @@ +// +// STDSTransactionTest.m +// Stripe3DS2Tests +// +// Created by Yuki Tokuhiro on 3/22/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSTransaction+Private.h" +#import "STDSInvalidInputException.h" +#import "STDSChallengeParameters.h" +#import "STDSChallengeStatusReceiver.h" +#import "STDSRuntimeErrorEvent.h" +#import "STDSProtocolErrorEvent.h" +#import "STDSStripe3DS2Error.h" +#import "STDSTransaction.h" +#import "STDSErrorMessage.h" +#import "NSError+Stripe3DS2.h" + +@interface STDSTransaction (Private) +@property (nonatomic, weak) id challengeStatusReceiver; + +- (void)_handleError:(NSError *)error; +- (NSString *)_sdkAppIdentifier; +@end + +@interface STDSTransactionTest : XCTestCase +@property (nonatomic, copy) void (^didErrorWithProtocolErrorEvent)(STDSProtocolErrorEvent *); +@property (nonatomic, copy) void (^didErrorWithRuntimeErrorEvent)(STDSRuntimeErrorEvent *); +@end + +@implementation STDSTransactionTest + +- (void)tearDown { + self.didErrorWithRuntimeErrorEvent = nil; + self.didErrorWithProtocolErrorEvent = nil; + [super tearDown]; +} + +#pragma mark - Timeout + +- (void)testTimeoutBelow5Throws { + STDSTransaction *transaction = [STDSTransaction new]; + STDSChallengeParameters *challengeParameters = [[STDSChallengeParameters alloc] init]; + XCTAssertThrowsSpecific([transaction doChallengeWithViewController:[UIViewController new] + challengeParameters:challengeParameters + challengeStatusReceiver:self + timeout:4 * 60], STDSInvalidInputException); +} + +- (void)testTimeoutFires { + STDSTransaction *transaction = [STDSTransaction new]; + STDSChallengeParameters *challengeParameters = [[STDSChallengeParameters alloc] init]; + + // Assert timer is scheduled to fire 5 minutes from now, give or take 1 second + NSInteger timeout = 300; + [transaction doChallengeWithViewController:[UIViewController new] + challengeParameters:challengeParameters + challengeStatusReceiver:self + timeout:timeout]; + XCTAssertTrue(transaction.timeoutTimer.isValid); + NSTimeInterval secondsIntoTheFutureTimerWillFire = [transaction.timeoutTimer.fireDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(secondsIntoTheFutureTimerWillFire - (timeout)), 1); +} + +#pragma mark - Error Handling + +- (void)testHandleUnknownMessageTypeError { + NSError *error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain code:STDSErrorCodeUnknownMessageType userInfo:nil]; + [self _expectProtocolErrorEventForError:error validator:^(STDSProtocolErrorEvent *protocolErrorEvent) { + XCTAssertEqualObjects(protocolErrorEvent.errorMessage.errorCode, @"101"); + }]; +} + +- (void)testHandleInvalidJSONError { + NSError *error = [NSError _stds_invalidJSONFieldError:@"invalid field"]; + [self _expectProtocolErrorEventForError:error validator:^(STDSProtocolErrorEvent *protocolErrorEvent) { + XCTAssertEqualObjects(protocolErrorEvent.errorMessage.errorCode, @"203"); + XCTAssertEqualObjects(protocolErrorEvent.errorMessage.errorDetails, @"invalid field"); + }]; +} + +- (void)testHandleMissingJSONError { + NSError *error = [NSError _stds_missingJSONFieldError:@"missing field"]; + [self _expectProtocolErrorEventForError:error validator:^(STDSProtocolErrorEvent *protocolErrorEvent) { + XCTAssertEqualObjects(protocolErrorEvent.errorMessage.errorCode, @"201"); + XCTAssertEqualObjects(protocolErrorEvent.errorMessage.errorDetails, @"missing field"); + }]; +} + +- (void)testHandleReceivedErrorMessage { + STDSErrorMessage *receivedErrorMessage = [[STDSErrorMessage alloc] initWithErrorCode:@"" errorComponent:@"" errorDescription:@"" errorDetails:nil messageVersion:@"" acsTransactionIdentifier:@"" errorMessageType:@""]; + NSError *error = [NSError errorWithDomain:STDSStripe3DS2ErrorDomain + code:STDSErrorCodeReceivedErrorMessage + userInfo:@{STDSStripe3DS2ErrorMessageErrorKey: receivedErrorMessage}]; + + [self _expectProtocolErrorEventForError:error validator:^(STDSProtocolErrorEvent *protocolErrorEvent) { + XCTAssertEqualObjects(protocolErrorEvent.errorMessage, receivedErrorMessage); + }]; +} + +- (void)testHandleNetworkConnectionLostError { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorNetworkConnectionLost + userInfo:nil]; + [self _expectRuntimeErrorEventForError:error validator:^(STDSRuntimeErrorEvent *runtimeErrorEvent) { + XCTAssertEqualObjects(runtimeErrorEvent.errorCode, [@(NSURLErrorNetworkConnectionLost) stringValue]); + }]; +} + +- (void)_expectProtocolErrorEventForError:(NSError *)error validator:(void (^)(STDSProtocolErrorEvent *))protocolErrorEventChecker { + STDSTransaction *transaction = [STDSTransaction new]; + transaction.challengeStatusReceiver = self; + XCTestExpectation *expectation = [self expectationWithDescription:@"Call didErrorWithProtocolErrorEvent"]; + self.didErrorWithProtocolErrorEvent = ^(STDSProtocolErrorEvent *protocolErrorEvent) { + protocolErrorEventChecker(protocolErrorEvent); + [expectation fulfill]; + }; + [transaction _handleError:error]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)_expectRuntimeErrorEventForError:(NSError *)error validator:(void (^)(STDSRuntimeErrorEvent *))runtimeErrorEventChecker { + STDSTransaction *transaction = [STDSTransaction new]; + transaction.challengeStatusReceiver = self; + XCTestExpectation *expectation = [self expectationWithDescription:@"Call didErrorWithRuntimeErrorEvent"]; + self.didErrorWithRuntimeErrorEvent = ^(STDSRuntimeErrorEvent *runtimeErrorEvent) { + runtimeErrorEventChecker(runtimeErrorEvent); + [expectation fulfill]; + }; + [transaction _handleError:error]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - STDSChallengeStatusReceiver + +- (void)transaction:(nonnull STDSTransaction *)transaction didCompleteChallengeWithCompletionEvent:(nonnull STDSCompletionEvent *)completionEvent {} + +- (void)transaction:(nonnull STDSTransaction *)transaction didErrorWithProtocolErrorEvent:(nonnull STDSProtocolErrorEvent *)protocolErrorEvent { + if (self.didErrorWithProtocolErrorEvent) { + self.didErrorWithProtocolErrorEvent(protocolErrorEvent); + } +} + +- (void)transaction:(nonnull STDSTransaction *)transaction didErrorWithRuntimeErrorEvent:(nonnull STDSRuntimeErrorEvent *)runtimeErrorEvent { + if (self.didErrorWithRuntimeErrorEvent) { + self.didErrorWithRuntimeErrorEvent(runtimeErrorEvent); + } +} + +- (void)transactionDidCancel:(nonnull STDSTransaction *)transaction {} + +- (void)transactionDidTimeOut:(nonnull STDSTransaction *)transaction {} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSUICustomizationTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSUICustomizationTests.m new file mode 100644 index 00000000..9d231acf --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSUICustomizationTests.m @@ -0,0 +1,249 @@ +// +// STDSUICustomizationTests.m +// Stripe3DS2Tests +// +// Created by Andrew Harrison on 3/14/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import +#import "STDSUICustomization.h" + +@interface STDSUICustomizationTests : XCTestCase + +@end + +@implementation STDSUICustomizationTests + +// The following helper methods return customization objects with properties different than the default. + +- (STDSNavigationBarCustomization *)_customNavigationBar { + STDSNavigationBarCustomization *custom = [STDSNavigationBarCustomization new]; + custom.font = [UIFont italicSystemFontOfSize:1]; + custom.textColor = UIColor.blueColor; + custom.barTintColor = UIColor.redColor; + custom.barStyle = UIBarStyleBlack; + custom.translucent = NO; + custom.headerText = @"foo"; + custom.buttonText = @"bar"; + return custom; +} + +- (STDSLabelCustomization *)_customLabel { + STDSLabelCustomization *custom = [STDSLabelCustomization new]; + custom.font = [UIFont italicSystemFontOfSize:1]; + custom.textColor = UIColor.blueColor; + custom.headingTextColor = UIColor.redColor; + custom.headingFont = [UIFont italicSystemFontOfSize:2]; + return custom; +} + +- (STDSTextFieldCustomization *)_customTextField { + STDSTextFieldCustomization *custom = [STDSTextFieldCustomization new]; + custom.font = [UIFont italicSystemFontOfSize:1]; + custom.textColor = UIColor.blueColor; + custom.borderWidth = -1; + custom.borderColor = UIColor.redColor; + custom.cornerRadius = -8; + custom.keyboardAppearance = UIKeyboardAppearanceAlert; + custom.placeholderTextColor = UIColor.greenColor; + return custom; +} + +- (STDSButtonCustomization *)_customButton { + STDSButtonCustomization *custom = [[STDSButtonCustomization alloc] initWithBackgroundColor:UIColor.redColor cornerRadius:-1]; + custom.font = [UIFont italicSystemFontOfSize:1]; + custom.textColor = UIColor.blueColor; + custom.titleStyle = STDSButtonTitleStyleLowercase; + return custom; +} + +- (STDSFooterCustomization *)_customFooter { + STDSFooterCustomization *custom = [STDSFooterCustomization new]; + custom.font = [UIFont italicSystemFontOfSize:1]; + custom.textColor = UIColor.blueColor; + custom.backgroundColor = UIColor.redColor; + custom.chevronColor = UIColor.greenColor; + custom.headingTextColor = UIColor.grayColor; + custom.headingFont = [UIFont italicSystemFontOfSize:2]; + return custom; +} + +- (STDSSelectionCustomization *)_customSelection { + STDSSelectionCustomization *custom = [STDSSelectionCustomization new]; + custom.primarySelectedColor = UIColor.redColor; + custom.secondarySelectedColor = UIColor.blueColor; + custom.unselectedBorderColor = UIColor.brownColor; + custom.unselectedBackgroundColor = UIColor.cyanColor; + return custom; +} + +#pragma mark - Copying + +- (void)testUICustomizationDeepCopy { + // Make a STDSUICustomization instance with all non-default properties + STDSButtonCustomization *submitButtonCustomization = [self _customButton]; + STDSButtonCustomization *continueButtonCustomization = [self _customButton]; + continueButtonCustomization.cornerRadius = -2; + STDSButtonCustomization *nextButtonCustomization = [self _customButton]; + nextButtonCustomization.cornerRadius = -3; + STDSButtonCustomization *cancelButtonCustomization = [self _customButton]; + cancelButtonCustomization.cornerRadius = -4; + STDSButtonCustomization *resendButtonCustomization = [self _customButton]; + resendButtonCustomization.cornerRadius = -5; + + STDSNavigationBarCustomization *navigationBarCustomization = [self _customNavigationBar]; + STDSLabelCustomization *labelCustomization = [self _customLabel]; + STDSTextFieldCustomization *textFieldCustomization = [self _customTextField]; + STDSFooterCustomization *footerCustomization = [self _customFooter]; + STDSSelectionCustomization *selectionCustomization = [self _customSelection]; + + STDSUICustomization *uiCustomization = [[STDSUICustomization alloc] init]; + uiCustomization.footerCustomization = footerCustomization; + uiCustomization.selectionCustomization = selectionCustomization; + [uiCustomization setButtonCustomization:submitButtonCustomization forType:STDSUICustomizationButtonTypeSubmit]; + [uiCustomization setButtonCustomization:continueButtonCustomization forType:STDSUICustomizationButtonTypeContinue]; + [uiCustomization setButtonCustomization:nextButtonCustomization forType:STDSUICustomizationButtonTypeNext]; + [uiCustomization setButtonCustomization:cancelButtonCustomization forType:STDSUICustomizationButtonTypeCancel]; + [uiCustomization setButtonCustomization:resendButtonCustomization forType:STDSUICustomizationButtonTypeResend]; + uiCustomization.navigationBarCustomization = navigationBarCustomization; + uiCustomization.labelCustomization = labelCustomization; + uiCustomization.textFieldCustomization = textFieldCustomization; + uiCustomization.backgroundColor = UIColor.redColor; + uiCustomization.activityIndicatorViewStyle = UIActivityIndicatorViewStyleLarge; + uiCustomization.blurStyle = UIBlurEffectStyleDark; + uiCustomization.preferredStatusBarStyle = UIStatusBarStyleLightContent; + + STDSUICustomization *copy = [uiCustomization copy]; + XCTAssertNotNil([copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeNext]); + XCTAssertNotNil(copy.navigationBarCustomization); + XCTAssertNotNil(copy.labelCustomization); + XCTAssertNotNil(copy.textFieldCustomization); + XCTAssertNotNil(copy.footerCustomization); + XCTAssertNotNil(copy.selectionCustomization); + + /// The pointers do not reference the same objects. + XCTAssertNotEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeSubmit], [copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeSubmit]); + XCTAssertNotEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeContinue], [copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeContinue]); + XCTAssertNotEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeNext], [copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeNext]); + XCTAssertNotEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeCancel], [copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeCancel]); + XCTAssertNotEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeResend], [copy buttonCustomizationForButtonType:STDSUICustomizationButtonTypeResend]); + XCTAssertNotEqual(uiCustomization.navigationBarCustomization, copy.navigationBarCustomization); + XCTAssertNotEqual(uiCustomization.labelCustomization, copy.labelCustomization); + XCTAssertNotEqual(uiCustomization.textFieldCustomization, copy.textFieldCustomization); + XCTAssertNotEqual(uiCustomization.footerCustomization, copy.footerCustomization); + XCTAssertNotEqual(uiCustomization.selectionCustomization, copy.selectionCustomization); + + /// The properties have been successfully copied. + XCTAssertEqualObjects(uiCustomization.backgroundColor, copy.backgroundColor); + XCTAssertEqual(uiCustomization.activityIndicatorViewStyle, copy.activityIndicatorViewStyle); + XCTAssertEqual(uiCustomization.blurStyle, copy.blurStyle); + // A different test case will cover that our custom classes implemented copy correctly; just sanity check one property here + XCTAssertEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeSubmit].cornerRadius, submitButtonCustomization.cornerRadius); + XCTAssertEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeContinue].cornerRadius, continueButtonCustomization.cornerRadius); + XCTAssertEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeNext].cornerRadius, nextButtonCustomization.cornerRadius); + XCTAssertEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeCancel].cornerRadius, cancelButtonCustomization.cornerRadius); + XCTAssertEqual([uiCustomization buttonCustomizationForButtonType:STDSUICustomizationButtonTypeResend].cornerRadius, resendButtonCustomization.cornerRadius); + XCTAssertEqualObjects(uiCustomization.navigationBarCustomization.font, copy.navigationBarCustomization.font); + XCTAssertEqualObjects(uiCustomization.labelCustomization.font, copy.labelCustomization.font); + XCTAssertEqualObjects(uiCustomization.textFieldCustomization.font, copy.textFieldCustomization.font); + XCTAssertEqualObjects(uiCustomization.footerCustomization.font, copy.footerCustomization.font); + XCTAssertEqualObjects(uiCustomization.selectionCustomization.primarySelectedColor, copy.selectionCustomization.primarySelectedColor); + XCTAssertEqual(uiCustomization.preferredStatusBarStyle, copy.preferredStatusBarStyle); +} + +- (void)testButtonCustomizationIsCopied { + STDSButtonCustomization *buttonCustomization = [self _customButton]; + + /// The pointers do not reference the same objects. + STDSButtonCustomization *copy = [buttonCustomization copy]; + XCTAssertNotEqual(buttonCustomization, copy); + + /// The properties have been successfully copied. + XCTAssertEqual(buttonCustomization.cornerRadius, copy.cornerRadius); + XCTAssertEqual(buttonCustomization.backgroundColor, copy.backgroundColor); + XCTAssertEqual(buttonCustomization.font, copy.font); + XCTAssertEqual(buttonCustomization.textColor, copy.textColor); + XCTAssertEqual(buttonCustomization.titleStyle, buttonCustomization.titleStyle); +} + +- (void)testNavigationBarCustomizationIsCopied { + STDSNavigationBarCustomization *navigationBarCustomization = [self _customNavigationBar]; + + /// The pointers do not reference the same objects. + STDSNavigationBarCustomization *copy = [navigationBarCustomization copy]; + XCTAssertNotEqual(navigationBarCustomization, copy); + + /// The properties have been successfully copied. + XCTAssertEqualObjects(navigationBarCustomization.headerText, copy.headerText); + XCTAssertEqualObjects(navigationBarCustomization.buttonText, copy.buttonText); + XCTAssertEqualObjects(navigationBarCustomization.barTintColor, copy.barTintColor); + XCTAssertEqualObjects(navigationBarCustomization.font, copy.font); + XCTAssertEqualObjects(navigationBarCustomization.textColor, copy.textColor); + XCTAssertEqual(navigationBarCustomization.barStyle, copy.barStyle); + XCTAssertEqual(navigationBarCustomization.translucent, copy.translucent); +} + +- (void)testLabelCustomizationIsCopied { + STDSLabelCustomization *labelCustomization = [self _customLabel]; + + /// The pointers do not reference the same objects. + STDSLabelCustomization *copy = [labelCustomization copy]; + XCTAssertNotEqual(labelCustomization, copy); + + /// The properties have been successfully copied. + XCTAssertEqualObjects(labelCustomization.headingTextColor, copy.headingTextColor); + XCTAssertEqualObjects(labelCustomization.headingFont, copy.headingFont); + XCTAssertEqualObjects(labelCustomization.font, copy.font); + XCTAssertEqualObjects(labelCustomization.textColor, copy.textColor); +} + +- (void)testTextFieldCustomizationIsCopied { + STDSTextFieldCustomization *textFieldCustomization = [self _customTextField]; + + /// The pointers do not reference the same objects. + STDSTextFieldCustomization *copy = [textFieldCustomization copy]; + XCTAssertNotEqual(textFieldCustomization, copy); + + /// The properties have been successfully copied. + XCTAssertEqual(textFieldCustomization.borderWidth, copy.borderWidth); + XCTAssertEqualObjects(textFieldCustomization.borderColor, copy.borderColor); + XCTAssertEqual(textFieldCustomization.cornerRadius, copy.cornerRadius); + XCTAssertEqualObjects(textFieldCustomization.font, copy.font); + XCTAssertEqualObjects(textFieldCustomization.textColor, copy.textColor); + XCTAssertEqual(textFieldCustomization.keyboardAppearance, copy.keyboardAppearance); + XCTAssertEqualObjects(textFieldCustomization.placeholderTextColor, copy.placeholderTextColor); +} + +- (void)testFooterCustomizationIsCopied { + STDSFooterCustomization *footerCustomization = [self _customFooter]; + + /// The pointers do not reference the same objects. + STDSFooterCustomization *copy = [footerCustomization copy]; + XCTAssertNotEqual(footerCustomization, copy); + + /// The properties have been successfully copied. + XCTAssertEqualObjects(footerCustomization.textColor, copy.textColor); + XCTAssertEqualObjects(footerCustomization.font, copy.font); + XCTAssertEqualObjects(footerCustomization.backgroundColor, copy.backgroundColor); + XCTAssertEqualObjects(footerCustomization.chevronColor, copy.chevronColor); + XCTAssertEqualObjects(footerCustomization.headingTextColor, copy.headingTextColor); + XCTAssertEqualObjects(footerCustomization.headingFont, copy.headingFont); +} + +- (void)testSelectionCustomizationIsCopied { + STDSSelectionCustomization *customization = [self _customSelection]; + + /// The pointers do not reference the same objects. + STDSSelectionCustomization *copy = [customization copy]; + XCTAssertNotEqual(customization, copy); + + /// The properties have been successfully copied. + XCTAssertEqualObjects(customization.primarySelectedColor, copy.primarySelectedColor); + XCTAssertEqualObjects(customization.secondarySelectedColor, copy.secondarySelectedColor); + XCTAssertEqualObjects(customization.unselectedBorderColor, copy.unselectedBorderColor); + XCTAssertEqualObjects(customization.unselectedBackgroundColor, copy.unselectedBackgroundColor); + +} + +@end diff --git a/Stripe3DS2/Stripe3DS2Tests/STDSWarningTests.m b/Stripe3DS2/Stripe3DS2Tests/STDSWarningTests.m new file mode 100644 index 00000000..ec559c5d --- /dev/null +++ b/Stripe3DS2/Stripe3DS2Tests/STDSWarningTests.m @@ -0,0 +1,26 @@ +// +// STDSWarningTests.m +// Stripe3DS2Tests +// +// Created by Cameron Sabol on 2/12/19. +// Copyright © 2019 Stripe. All rights reserved. +// + +#import + +#import "STDSWarning.h" + +@interface STDSWarningTests : XCTestCase + +@end + +@implementation STDSWarningTests + +- (void)testWarning { + STDSWarning *warning = [[STDSWarning alloc] initWithIdentifier:@"test_id" message:@"test_message" severity:STDSWarningSeverityMedium]; + XCTAssertEqual(warning.identifier, @"test_id", @"Identifier was not set correctly."); + XCTAssertEqual(warning.message, @"test_message", @"Message was not set correctly."); + XCTAssertEqual(warning.severity, STDSWarningSeverityMedium, @"Severity was not set correctly."); +} + +@end diff --git a/Stripe3DS2/exported_symbols.txt b/Stripe3DS2/exported_symbols.txt new file mode 100644 index 00000000..981c8756 --- /dev/null +++ b/Stripe3DS2/exported_symbols.txt @@ -0,0 +1,13 @@ +_OBJC_CLASS_$_STDSButtonCustomization +_OBJC_CLASS_$_STDSChallengeParameters +_OBJC_CLASS_$_STDSConfigParameters +_OBJC_CLASS_$_STDSFooterCustomization +_OBJC_CLASS_$_STDSJSONEncoder +_OBJC_CLASS_$_STDSLabelCustomization +_OBJC_CLASS_$_STDSNavigationBarCustomization +_OBJC_CLASS_$_STDSSelectionCustomization +_OBJC_CLASS_$_STDSTextFieldCustomization +_OBJC_CLASS_$_STDSThreeDS2Service +_OBJC_CLASS_$_STDSUICustomization +_OBJC_CLASS_$_STDSSwiftTryCatch +_STDSAuthenticationResponseFromJSON diff --git a/StripeApplePay/README.md b/StripeApplePay/README.md new file mode 100644 index 00000000..9c225745 --- /dev/null +++ b/StripeApplePay/README.md @@ -0,0 +1,36 @@ +# Stripe Apple Pay iOS SDK + +StripeApplePay is a lightweight Apple Pay SDK intended for building App Clips or other size-constrained apps. + +## Table of contents + + +- [Stripe Apple Pay iOS SDK](#stripe-apple-pay-ios-sdk) +- [Table of contents](#table-of-contents) + - [Requirements](#requirements) + - [Getting started](#getting-started) + - [Integration](#integration) + - [Example](#example) + - [Manual linking](#manual-linking) + + + +## Requirements + +The Stripe Apple Pay SDK is compatible with apps targeting iOS 13.0 or above. + +## Getting started + +### Integration + +Get started with our [📚 Apple Pay integration guide](https://stripe.com/docs/apple-pay?platform=ios) and [example project](../Example/AppClipExample), or [📘 browse the SDK reference](https://stripe.dev/stripe-ios/stripe-applepay/index.html) for fine-grained documentation of all the classes and methods in the SDK. + +### Example + +[AppClipExample](../Example/AppClipExample) – This example demonstrates how to offer Apple Pay in an App Clip. + +## Manual linking + +If you link this library manually, use a version from our [releases](https://github.com/stripe/stripe-ios/releases) page and make sure to embed all of the following frameworks: +- `StripeCore.xcframework` +- `StripeApplePay.xcframework` diff --git a/StripeApplePay/StripeApplePay.xcodeproj/project.pbxproj b/StripeApplePay/StripeApplePay.xcodeproj/project.pbxproj new file mode 100644 index 00000000..61dad09b --- /dev/null +++ b/StripeApplePay/StripeApplePay.xcodeproj/project.pbxproj @@ -0,0 +1,618 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 01AA289840E0AC9229A8CF63 /* StripeApplePay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29BFCC7B0FCEA743A857B51F /* StripeApplePay.framework */; }; + 09EB0F7E346CB22144515E67 /* SetupIntent+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DDE851AD7450BE70381337 /* SetupIntent+API.swift */; }; + 1990346BA0B39ADD47210E18 /* StripeApplePay.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C1D3421B1B2BB91FAA66620 /* StripeApplePay.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 22E1F50D066A294A316052ED /* PaymentIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2398CA91B601B5FC7C8FB48 /* PaymentIntent.swift */; }; + 234FA38DC46927B23871A75D /* SetupIntentParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7CF2B75A797D6B2B5A04B4 /* SetupIntentParams.swift */; }; + 2D6CD6872A00B6FE0243C3F5 /* PKPayment+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906F7217DBF694BF42F23458 /* PKPayment+Stripe.swift */; }; + 2EBB230815383A8402D71146 /* STPPaymentMethodFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905182496A07DE4F056A3EAA /* STPPaymentMethodFunctionalTest.swift */; }; + 313F5F7B2B0BE5A600BD98A9 /* Docs.docc in Sources */ = {isa = PBXBuildFile; fileRef = 313F5F7A2B0BE5A600BD98A9 /* Docs.docc */; }; + 43682CEAB00A93868FA3188A /* SetupIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2A354F50C5D563436A0068 /* SetupIntent.swift */; }; + 463680AADB8CED6E962CD45A /* PaymentIntentParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478361D29DF10F2B43F7A1D2 /* PaymentIntentParams.swift */; }; + 4AA6A66246DD30798E5CC5F7 /* PaymentIntent+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EAD8979A20E239E16257E4 /* PaymentIntent+API.swift */; }; + 4ADC5356764DC5E3F1C1D51B /* CardBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBC2BC8C0A60983D0948A11 /* CardBrand.swift */; }; + 4B2DE4109D876CCBDC53C2A3 /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62ECC1AA5583E4104C073B63 /* StripeCore.framework */; }; + 4CE57B5BF79D1515F27A18A3 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF86CF1EFE4AC1A46F055202 /* Address.swift */; }; + 505A82AADE15E2B2B99C5D32 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 594F0478E94E6FA2F551EA0A /* Token.swift */; }; + 526AE8381F232C9FEABFFDCD /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 8ED017D12DE81D3ABD013768 /* OHHTTPStubsSwift */; }; + 53BA33D02E07DFF1393DF0E4 /* PaymentMethod+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264583DBFFA42C864220D7FF /* PaymentMethod+API.swift */; }; + 59AEBEB856DB8A0118F1D0DE /* STPAnalyticsClient+Payments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20A70C7D203A80129DBB9304 /* STPAnalyticsClient+Payments.swift */; }; + 59C1DB9EF052987BD20B65A3 /* BillingDetails+ApplePay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FD028203455E50A637227C /* BillingDetails+ApplePay.swift */; }; + 62AFEE5A1E32BD84588CA233 /* STPTelemetryClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B4DF82BA51CC68265B0795 /* STPTelemetryClientTest.swift */; }; + 7C7C92AFED77FC4C5D26DC36 /* BillingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C563CA8E1D005A453C762703 /* BillingDetails.swift */; }; + 848843F1145350ABF540D69F /* ShippingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08710B3CD45DE9DCE2055A3 /* ShippingDetails.swift */; }; + 90286A48FA6C350BDEC227D0 /* TelemetryInjectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 446F888DB3EC0FC1FE9B9377 /* TelemetryInjectionTest.swift */; }; + 9F50D23599CD0B1270F8C295 /* STPApplePayContext+LegacySupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7023208C7E3B3044F553DFDD /* STPApplePayContext+LegacySupport.swift */; }; + A69598FE8095AFA9898297A9 /* Token+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81AB96E6201D534220ABB9D /* Token+API.swift */; }; + A79510332C88568637C9E867 /* STPAnalyticsClient+ApplePayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E9055B54A5634B636570E3 /* STPAnalyticsClient+ApplePayTest.swift */; }; + AB48B0CBFFEFC46E01C76B6C /* STPAPIClient+PaymentsCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894B7D67F2364A12DD133CF4 /* STPAPIClient+PaymentsCore.swift */; }; + BCC7454DB40673493DF940F5 /* STPAnalyticsClient+PaymentsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD5C52C679274D830AA5B93 /* STPAnalyticsClient+PaymentsAPI.swift */; }; + C0D4B1753F584397DD1AD946 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87A189B89110D3A8EF052425 /* XCTest.framework */; }; + C6552A9ADEA160B9BBAF3A10 /* STPApplePayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4AEF10AF7900AF7ED0EDE21 /* STPApplePayContext.swift */; }; + C6A333FBB72EE91849DD6202 /* STPAPIClient+ApplePay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AB5D51581CE6ABCCD2493 /* STPAPIClient+ApplePay.swift */; }; + CB78059C8EB6A072C30A98DA /* STPTelemetryClientFunctionalTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7AA620790EC4257132D738 /* STPTelemetryClientFunctionalTest.swift */; }; + CECAB0CB1CE4D7BD1EF897F0 /* Blocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5439AD0D1236F6A21EE93CB3 /* Blocks.swift */; }; + D8D613D3A85B81F8C4386E08 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = CB64080D8A41D33BC3DEEAF8 /* OHHTTPStubs */; }; + E1E7B153B169D0A6363ADD4B /* PaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D1E8A9F849655BAC63DB2FD /* PaymentMethod.swift */; }; + EEF84AC6C1EBF27BF6AAC0BF /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FA663325484F2D515613494 /* StripeCoreTestUtils.framework */; }; + F42BC784EDBD141C90E74A5F /* PKContact+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133A7483EDB9F1B1CFFDB9ED /* PKContact+Stripe.swift */; }; + F59D5A3F2C5849CD465062A5 /* StripeCore+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88664A990EE5D99076E88987 /* StripeCore+Import.swift */; }; + FADFD3B7E3832928E254FAF1 /* PaymentMethodParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 468654E242DFE2F85FF422EB /* PaymentMethodParams.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6DAB385DAFF8DCB87110CC7E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 95898E5CA0A1871DDFFB66B0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 747C9FE1743C0B4444303569; + remoteInfo = StripeApplePay; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + F937383B644A63BD5FC4A081 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + FF3CB7A978895136380088BB /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0B7AA620790EC4257132D738 /* STPTelemetryClientFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPTelemetryClientFunctionalTest.swift; sourceTree = ""; }; + 0C1D3421B1B2BB91FAA66620 /* StripeApplePay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeApplePay.h; sourceTree = ""; }; + 0FA663325484F2D515613494 /* StripeCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 133A7483EDB9F1B1CFFDB9ED /* PKContact+Stripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKContact+Stripe.swift"; sourceTree = ""; }; + 20A70C7D203A80129DBB9304 /* STPAnalyticsClient+Payments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAnalyticsClient+Payments.swift"; sourceTree = ""; }; + 229446797CC5FB07DB344D81 /* StripeApplePayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeApplePayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 264583DBFFA42C864220D7FF /* PaymentMethod+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentMethod+API.swift"; sourceTree = ""; }; + 29BFCC7B0FCEA743A857B51F /* StripeApplePay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeApplePay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 30DDE851AD7450BE70381337 /* SetupIntent+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupIntent+API.swift"; sourceTree = ""; }; + 313F5F7A2B0BE5A600BD98A9 /* Docs.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Docs.docc; sourceTree = ""; }; + 3D7CF2B75A797D6B2B5A04B4 /* SetupIntentParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupIntentParams.swift; sourceTree = ""; }; + 40D5ED47331313B6AD8B6F46 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + 425B19195E4B86F77229252D /* StripeiOS Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Debug.xcconfig"; sourceTree = ""; }; + 446F888DB3EC0FC1FE9B9377 /* TelemetryInjectionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryInjectionTest.swift; sourceTree = ""; }; + 468654E242DFE2F85FF422EB /* PaymentMethodParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodParams.swift; sourceTree = ""; }; + 478361D29DF10F2B43F7A1D2 /* PaymentIntentParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentIntentParams.swift; sourceTree = ""; }; + 49B0926F8F65BC0102B99BF4 /* StripeiOS-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Debug.xcconfig"; sourceTree = ""; }; + 4EE8157F3536C238AC337337 /* StripeiOS-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Release.xcconfig"; sourceTree = ""; }; + 5439AD0D1236F6A21EE93CB3 /* Blocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Blocks.swift; sourceTree = ""; }; + 594F0478E94E6FA2F551EA0A /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + 62ECC1AA5583E4104C073B63 /* StripeCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67711D1BC01BA83323EA1F6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 6D1E8A9F849655BAC63DB2FD /* PaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethod.swift; sourceTree = ""; }; + 7023208C7E3B3044F553DFDD /* STPApplePayContext+LegacySupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPApplePayContext+LegacySupport.swift"; sourceTree = ""; }; + 79B089D69DBA1A0BCF4503B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 855AB5D51581CE6ABCCD2493 /* STPAPIClient+ApplePay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+ApplePay.swift"; sourceTree = ""; }; + 87A189B89110D3A8EF052425 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 88664A990EE5D99076E88987 /* StripeCore+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StripeCore+Import.swift"; sourceTree = ""; }; + 894B7D67F2364A12DD133CF4 /* STPAPIClient+PaymentsCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+PaymentsCore.swift"; sourceTree = ""; }; + 8DBC2BC8C0A60983D0948A11 /* CardBrand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBrand.swift; sourceTree = ""; }; + 905182496A07DE4F056A3EAA /* STPPaymentMethodFunctionalTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodFunctionalTest.swift; sourceTree = ""; }; + 906F7217DBF694BF42F23458 /* PKPayment+Stripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPayment+Stripe.swift"; sourceTree = ""; }; + 93EAD8979A20E239E16257E4 /* PaymentIntent+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentIntent+API.swift"; sourceTree = ""; }; + BDAEEEB96953ECBB9C68CF1C /* StripeiOS Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Release.xcconfig"; sourceTree = ""; }; + C08710B3CD45DE9DCE2055A3 /* ShippingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingDetails.swift; sourceTree = ""; }; + C563CA8E1D005A453C762703 /* BillingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingDetails.swift; sourceTree = ""; }; + CB01F67DF47C468825F8FF6A /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + CC2A354F50C5D563436A0068 /* SetupIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupIntent.swift; sourceTree = ""; }; + D2398CA91B601B5FC7C8FB48 /* PaymentIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentIntent.swift; sourceTree = ""; }; + D81AB96E6201D534220ABB9D /* Token+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Token+API.swift"; sourceTree = ""; }; + E4AEF10AF7900AF7ED0EDE21 /* STPApplePayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPApplePayContext.swift; sourceTree = ""; }; + E4B4DF82BA51CC68265B0795 /* STPTelemetryClientTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPTelemetryClientTest.swift; sourceTree = ""; }; + E6E9055B54A5634B636570E3 /* STPAnalyticsClient+ApplePayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAnalyticsClient+ApplePayTest.swift"; sourceTree = ""; }; + F4FD028203455E50A637227C /* BillingDetails+ApplePay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BillingDetails+ApplePay.swift"; sourceTree = ""; }; + FBD5C52C679274D830AA5B93 /* STPAnalyticsClient+PaymentsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAnalyticsClient+PaymentsAPI.swift"; sourceTree = ""; }; + FF86CF1EFE4AC1A46F055202 /* Address.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Address.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A690CB3AEAD3A05D0DF3D47 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B2DE4109D876CCBDC53C2A3 /* StripeCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 92797DEFFA4201115E38DEAD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C0D4B1753F584397DD1AD946 /* XCTest.framework in Frameworks */, + 01AA289840E0AC9229A8CF63 /* StripeApplePay.framework in Frameworks */, + EEF84AC6C1EBF27BF6AAC0BF /* StripeCoreTestUtils.framework in Frameworks */, + D8D613D3A85B81F8C4386E08 /* OHHTTPStubs in Frameworks */, + 526AE8381F232C9FEABFFDCD /* OHHTTPStubsSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0D2F315747B25043D0135CBC /* Analytics */ = { + isa = PBXGroup; + children = ( + 20A70C7D203A80129DBB9304 /* STPAnalyticsClient+Payments.swift */, + FBD5C52C679274D830AA5B93 /* STPAnalyticsClient+PaymentsAPI.swift */, + ); + path = Analytics; + sourceTree = ""; + }; + 18E783E8B2360F08F2FCDEB9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 87A189B89110D3A8EF052425 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1A99D698FD7EE445DA2366ED /* StripeApplePay */ = { + isa = PBXGroup; + children = ( + 313F5F7A2B0BE5A600BD98A9 /* Docs.docc */, + B7FC50BAFE8B7A10FA7EA283 /* Source */, + 67711D1BC01BA83323EA1F6B /* Info.plist */, + 0C1D3421B1B2BB91FAA66620 /* StripeApplePay.h */, + ); + path = StripeApplePay; + sourceTree = ""; + }; + 2F76FD687FF6507E5B2C1A3F /* ApplePayContext */ = { + isa = PBXGroup; + children = ( + 855AB5D51581CE6ABCCD2493 /* STPAPIClient+ApplePay.swift */, + E4AEF10AF7900AF7ED0EDE21 /* STPApplePayContext.swift */, + 7023208C7E3B3044F553DFDD /* STPApplePayContext+LegacySupport.swift */, + ); + path = ApplePayContext; + sourceTree = ""; + }; + 3581723D3A3FF58EE3E59033 /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + 40D5ED47331313B6AD8B6F46 /* Project-Debug.xcconfig */, + CB01F67DF47C468825F8FF6A /* Project-Release.xcconfig */, + 425B19195E4B86F77229252D /* StripeiOS Tests-Debug.xcconfig */, + BDAEEEB96953ECBB9C68CF1C /* StripeiOS Tests-Release.xcconfig */, + 49B0926F8F65BC0102B99BF4 /* StripeiOS-Debug.xcconfig */, + 4EE8157F3536C238AC337337 /* StripeiOS-Release.xcconfig */, + ); + name = BuildConfigurations; + path = ../BuildConfigurations; + sourceTree = ""; + }; + 3E8665C11A450F2FB7D2F2E7 /* Models */ = { + isa = PBXGroup; + children = ( + FF86CF1EFE4AC1A46F055202 /* Address.swift */, + C563CA8E1D005A453C762703 /* BillingDetails.swift */, + 8DBC2BC8C0A60983D0948A11 /* CardBrand.swift */, + D2398CA91B601B5FC7C8FB48 /* PaymentIntent.swift */, + 478361D29DF10F2B43F7A1D2 /* PaymentIntentParams.swift */, + 6D1E8A9F849655BAC63DB2FD /* PaymentMethod.swift */, + 468654E242DFE2F85FF422EB /* PaymentMethodParams.swift */, + CC2A354F50C5D563436A0068 /* SetupIntent.swift */, + 3D7CF2B75A797D6B2B5A04B4 /* SetupIntentParams.swift */, + C08710B3CD45DE9DCE2055A3 /* ShippingDetails.swift */, + 594F0478E94E6FA2F551EA0A /* Token.swift */, + ); + path = Models; + sourceTree = ""; + }; + 56479504C3EB58BC9E0C860A = { + isa = PBXGroup; + children = ( + DC6E0D845E48183E2C732ECF /* Project */, + 18E783E8B2360F08F2FCDEB9 /* Frameworks */, + 7C21384B46F40CB3F23DD515 /* Products */, + ); + sourceTree = ""; + }; + 6FAB736FE07EDFA07F922A75 /* API */ = { + isa = PBXGroup; + children = ( + 3E8665C11A450F2FB7D2F2E7 /* Models */, + 93EAD8979A20E239E16257E4 /* PaymentIntent+API.swift */, + 264583DBFFA42C864220D7FF /* PaymentMethod+API.swift */, + 30DDE851AD7450BE70381337 /* SetupIntent+API.swift */, + D81AB96E6201D534220ABB9D /* Token+API.swift */, + ); + path = API; + sourceTree = ""; + }; + 7261423B1B83AF12EE2FEA40 /* PaymentsCore */ = { + isa = PBXGroup; + children = ( + 0B7AA620790EC4257132D738 /* STPTelemetryClientFunctionalTest.swift */, + E4B4DF82BA51CC68265B0795 /* STPTelemetryClientTest.swift */, + ); + path = PaymentsCore; + sourceTree = ""; + }; + 7C21384B46F40CB3F23DD515 /* Products */ = { + isa = PBXGroup; + children = ( + 29BFCC7B0FCEA743A857B51F /* StripeApplePay.framework */, + 229446797CC5FB07DB344D81 /* StripeApplePayTests.xctest */, + 62ECC1AA5583E4104C073B63 /* StripeCore.framework */, + 0FA663325484F2D515613494 /* StripeCoreTestUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + 823E8B8D6F322BF2BCC4C030 /* PaymentsCore */ = { + isa = PBXGroup; + children = ( + 0D2F315747B25043D0135CBC /* Analytics */, + 6FAB736FE07EDFA07F922A75 /* API */, + 9FE0B80D161B8DB9C70902BF /* Categories */, + ); + path = PaymentsCore; + sourceTree = ""; + }; + 9FE0B80D161B8DB9C70902BF /* Categories */ = { + isa = PBXGroup; + children = ( + 894B7D67F2364A12DD133CF4 /* STPAPIClient+PaymentsCore.swift */, + ); + path = Categories; + sourceTree = ""; + }; + A4676BE706A785EA50A1FEBE /* StripeApplePayTests */ = { + isa = PBXGroup; + children = ( + 7261423B1B83AF12EE2FEA40 /* PaymentsCore */, + 79B089D69DBA1A0BCF4503B6 /* Info.plist */, + E6E9055B54A5634B636570E3 /* STPAnalyticsClient+ApplePayTest.swift */, + 905182496A07DE4F056A3EAA /* STPPaymentMethodFunctionalTest.swift */, + 446F888DB3EC0FC1FE9B9377 /* TelemetryInjectionTest.swift */, + ); + path = StripeApplePayTests; + sourceTree = ""; + }; + B7FC50BAFE8B7A10FA7EA283 /* Source */ = { + isa = PBXGroup; + children = ( + 2F76FD687FF6507E5B2C1A3F /* ApplePayContext */, + E1B6C6692BA02E5DD1AED20B /* Extensions */, + 823E8B8D6F322BF2BCC4C030 /* PaymentsCore */, + 5439AD0D1236F6A21EE93CB3 /* Blocks.swift */, + 88664A990EE5D99076E88987 /* StripeCore+Import.swift */, + ); + path = Source; + sourceTree = ""; + }; + DC6E0D845E48183E2C732ECF /* Project */ = { + isa = PBXGroup; + children = ( + 3581723D3A3FF58EE3E59033 /* BuildConfigurations */, + 1A99D698FD7EE445DA2366ED /* StripeApplePay */, + A4676BE706A785EA50A1FEBE /* StripeApplePayTests */, + ); + name = Project; + sourceTree = ""; + }; + E1B6C6692BA02E5DD1AED20B /* Extensions */ = { + isa = PBXGroup; + children = ( + F4FD028203455E50A637227C /* BillingDetails+ApplePay.swift */, + 133A7483EDB9F1B1CFFDB9ED /* PKContact+Stripe.swift */, + 906F7217DBF694BF42F23458 /* PKPayment+Stripe.swift */, + ); + path = Extensions; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 82FBB6E9E55342EADEEE1865 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1990346BA0B39ADD47210E18 /* StripeApplePay.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 747C9FE1743C0B4444303569 /* StripeApplePay */ = { + isa = PBXNativeTarget; + buildConfigurationList = 49AD325F0339BB78EE36ABC4 /* Build configuration list for PBXNativeTarget "StripeApplePay" */; + buildPhases = ( + 82FBB6E9E55342EADEEE1865 /* Headers */, + 0FA9CF39340C39235D831C53 /* Sources */, + 52997B8984C3A02398E230F4 /* Resources */, + FF3CB7A978895136380088BB /* Embed Frameworks */, + 2A690CB3AEAD3A05D0DF3D47 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StripeApplePay; + productName = StripeApplePay; + productReference = 29BFCC7B0FCEA743A857B51F /* StripeApplePay.framework */; + productType = "com.apple.product-type.framework"; + }; + ACAFA21CF224F80EFAEFDC2F /* StripeApplePayTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48EC33D43FBA14658B0E9567 /* Build configuration list for PBXNativeTarget "StripeApplePayTests" */; + buildPhases = ( + C5D7D85B45B4D8BECDF5B7CD /* Sources */, + 200F36028817645AC1730398 /* Resources */, + F937383B644A63BD5FC4A081 /* Embed Frameworks */, + 92797DEFFA4201115E38DEAD /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 9D2CBE0177D04009899ECFED /* PBXTargetDependency */, + ); + name = StripeApplePayTests; + packageProductDependencies = ( + CB64080D8A41D33BC3DEEAF8 /* OHHTTPStubs */, + 8ED017D12DE81D3ABD013768 /* OHHTTPStubsSwift */, + ); + productName = StripeApplePayTests; + productReference = 229446797CC5FB07DB344D81 /* StripeApplePayTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 95898E5CA0A1871DDFFB66B0 /* Project object */ = { + isa = PBXProject; + attributes = { + }; + buildConfigurationList = 2182982C71AEF088A6D2AA6A /* Build configuration list for PBXProject "StripeApplePay" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 56479504C3EB58BC9E0C860A; + packageReferences = ( + B45F7C9F63270FAF880F5EEF /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, + ); + productRefGroup = 7C21384B46F40CB3F23DD515 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 747C9FE1743C0B4444303569 /* StripeApplePay */, + ACAFA21CF224F80EFAEFDC2F /* StripeApplePayTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 200F36028817645AC1730398 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 52997B8984C3A02398E230F4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0FA9CF39340C39235D831C53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C6A333FBB72EE91849DD6202 /* STPAPIClient+ApplePay.swift in Sources */, + 9F50D23599CD0B1270F8C295 /* STPApplePayContext+LegacySupport.swift in Sources */, + C6552A9ADEA160B9BBAF3A10 /* STPApplePayContext.swift in Sources */, + CECAB0CB1CE4D7BD1EF897F0 /* Blocks.swift in Sources */, + 59C1DB9EF052987BD20B65A3 /* BillingDetails+ApplePay.swift in Sources */, + 313F5F7B2B0BE5A600BD98A9 /* Docs.docc in Sources */, + F42BC784EDBD141C90E74A5F /* PKContact+Stripe.swift in Sources */, + 2D6CD6872A00B6FE0243C3F5 /* PKPayment+Stripe.swift in Sources */, + 4CE57B5BF79D1515F27A18A3 /* Address.swift in Sources */, + 7C7C92AFED77FC4C5D26DC36 /* BillingDetails.swift in Sources */, + 4ADC5356764DC5E3F1C1D51B /* CardBrand.swift in Sources */, + 22E1F50D066A294A316052ED /* PaymentIntent.swift in Sources */, + 463680AADB8CED6E962CD45A /* PaymentIntentParams.swift in Sources */, + E1E7B153B169D0A6363ADD4B /* PaymentMethod.swift in Sources */, + FADFD3B7E3832928E254FAF1 /* PaymentMethodParams.swift in Sources */, + 43682CEAB00A93868FA3188A /* SetupIntent.swift in Sources */, + 234FA38DC46927B23871A75D /* SetupIntentParams.swift in Sources */, + 848843F1145350ABF540D69F /* ShippingDetails.swift in Sources */, + 505A82AADE15E2B2B99C5D32 /* Token.swift in Sources */, + 4AA6A66246DD30798E5CC5F7 /* PaymentIntent+API.swift in Sources */, + 53BA33D02E07DFF1393DF0E4 /* PaymentMethod+API.swift in Sources */, + 09EB0F7E346CB22144515E67 /* SetupIntent+API.swift in Sources */, + A69598FE8095AFA9898297A9 /* Token+API.swift in Sources */, + 59AEBEB856DB8A0118F1D0DE /* STPAnalyticsClient+Payments.swift in Sources */, + BCC7454DB40673493DF940F5 /* STPAnalyticsClient+PaymentsAPI.swift in Sources */, + AB48B0CBFFEFC46E01C76B6C /* STPAPIClient+PaymentsCore.swift in Sources */, + F59D5A3F2C5849CD465062A5 /* StripeCore+Import.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C5D7D85B45B4D8BECDF5B7CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CB78059C8EB6A072C30A98DA /* STPTelemetryClientFunctionalTest.swift in Sources */, + 62AFEE5A1E32BD84588CA233 /* STPTelemetryClientTest.swift in Sources */, + A79510332C88568637C9E867 /* STPAnalyticsClient+ApplePayTest.swift in Sources */, + 2EBB230815383A8402D71146 /* STPPaymentMethodFunctionalTest.swift in Sources */, + 90286A48FA6C350BDEC227D0 /* TelemetryInjectionTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 9D2CBE0177D04009899ECFED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeApplePay; + target = 747C9FE1743C0B4444303569 /* StripeApplePay */; + targetProxy = 6DAB385DAFF8DCB87110CC7E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 13B57D9AA89321957F3ACF3A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BDAEEEB96953ECBB9C68CF1C /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeApplePayTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeApplePayTests; + PRODUCT_NAME = StripeApplePayTests; + SDKROOT = iphoneos; + }; + name = Release; + }; + 15219003E9DE09D774FB72BB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4EE8157F3536C238AC337337 /* StripeiOS-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeApplePay/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-apple-pay"; + PRODUCT_NAME = StripeApplePay; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; + 6EEB3D72619B3E4C89798C09 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 49B0926F8F65BC0102B99BF4 /* StripeiOS-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeApplePay/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-apple-pay"; + PRODUCT_NAME = StripeApplePay; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Debug; + }; + 73AE406DB5197038572F40A7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB01F67DF47C468825F8FF6A /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7CA6A9C0F1D4C842935D7CEC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 40D5ED47331313B6AD8B6F46 /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + EB1D00A86E16D71A6153C2A0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 425B19195E4B86F77229252D /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeApplePayTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeApplePayTests; + PRODUCT_NAME = StripeApplePayTests; + SDKROOT = iphoneos; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2182982C71AEF088A6D2AA6A /* Build configuration list for PBXProject "StripeApplePay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7CA6A9C0F1D4C842935D7CEC /* Debug */, + 73AE406DB5197038572F40A7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48EC33D43FBA14658B0E9567 /* Build configuration list for PBXNativeTarget "StripeApplePayTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EB1D00A86E16D71A6153C2A0 /* Debug */, + 13B57D9AA89321957F3ACF3A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 49AD325F0339BB78EE36ABC4 /* Build configuration list for PBXNativeTarget "StripeApplePay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EEB3D72619B3E4C89798C09 /* Debug */, + 15219003E9DE09D774FB72BB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + B45F7C9F63270FAF880F5EEF /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/davidme-stripe/OHHTTPStubs"; + requirement = { + branch = "stripe-mock"; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8ED017D12DE81D3ABD013768 /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubsSwift; + }; + CB64080D8A41D33BC3DEEAF8 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubs; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 95898E5CA0A1871DDFFB66B0 /* Project object */; +} diff --git a/StripeApplePay/StripeApplePay.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/StripeApplePay/StripeApplePay.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/StripeApplePay/StripeApplePay.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/StripeApplePay/StripeApplePay.xcodeproj/xcshareddata/xcschemes/StripeApplePay.xcscheme b/StripeApplePay/StripeApplePay.xcodeproj/xcshareddata/xcschemes/StripeApplePay.xcscheme new file mode 100644 index 00000000..64ccb761 --- /dev/null +++ b/StripeApplePay/StripeApplePay.xcodeproj/xcshareddata/xcschemes/StripeApplePay.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StripeApplePay/StripeApplePay/Docs.docc/StripeApplePay.md b/StripeApplePay/StripeApplePay/Docs.docc/StripeApplePay.md new file mode 100644 index 00000000..8d6de7aa --- /dev/null +++ b/StripeApplePay/StripeApplePay/Docs.docc/StripeApplePay.md @@ -0,0 +1,3 @@ +# ``StripeApplePay`` + +Placeholder diff --git a/StripeApplePay/StripeApplePay/Info.plist b/StripeApplePay/StripeApplePay/Info.plist new file mode 100644 index 00000000..cd4a496b --- /dev/null +++ b/StripeApplePay/StripeApplePay/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPAPIClient+ApplePay.swift b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPAPIClient+ApplePay.swift new file mode 100644 index 00000000..d00717db --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPAPIClient+ApplePay.swift @@ -0,0 +1,44 @@ +// +// STPAPIClient+ApplePay.swift +// StripeApplePay +// +// Created by Jack Flintermann on 12/19/14. +// Copyright © 2014 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore + +/// STPAPIClient extensions to create Stripe Tokens, Sources, or PaymentMethods from Apple Pay PKPayment objects. +extension STPAPIClient { + /// Converts Stripe errors into the appropriate Apple Pay error, for use in `PKPaymentAuthorizationResult`. + /// If the error can be fixed by the customer within the Apple Pay sheet, we return an NSError that can be displayed in the Apple Pay sheet. + /// Otherwise, the original error is returned, resulting in the Apple Pay sheet being dismissed. You should display the error message to the customer afterwards. + /// Currently, we convert billing address related errors into a PKPaymentError that helpfully points to the billing address field in the Apple Pay sheet. + /// Note that Apple Pay should prevent most card errors (e.g. invalid CVC, expired cards) when you add a card to the wallet. + /// - Parameter stripeError: An error from the Stripe SDK. + @objc(pkPaymentErrorForStripeError:) + public class func pkPaymentError(forStripeError stripeError: Error?) -> Error? { + guard let stripeError = stripeError else { + return nil + } + + if (stripeError as NSError).domain == STPError.stripeDomain + && ((stripeError as NSError).userInfo[STPError.cardErrorCodeKey] as? String + == STPCardErrorCode.incorrectZip.rawValue) + { + var userInfo = (stripeError as NSError).userInfo + var errorCode: PKPaymentError.Code = .unknownError + errorCode = .billingContactInvalidError + userInfo[PKPaymentErrorKey.postalAddressUserInfoKey.rawValue] = + CNPostalAddressPostalCodeKey + return NSError( + domain: STPError.stripeDomain, + code: errorCode.rawValue, + userInfo: userInfo + ) + } + return stripeError + } +} diff --git a/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext+LegacySupport.swift b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext+LegacySupport.swift new file mode 100644 index 00000000..c71eb499 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext+LegacySupport.swift @@ -0,0 +1,55 @@ +// +// STPApplePayContext+LegacySupport.swift +// StripeApplePay +// +// Created by David Estes on 1/25/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit + +/// Internal Apple Pay class. Do not use. +/// :nodoc: +@objc @_spi(STP) public class _stpinternal_ApplePayContextDidCreatePaymentMethodStorage: NSObject { + @_spi(STP) public weak var delegate: _stpinternal_STPApplePayContextDelegateBase? + @_spi(STP) public var context: STPApplePayContext + @_spi(STP) public var paymentMethod: StripeAPI.PaymentMethod + @_spi(STP) public var paymentInformation: PKPayment + @_spi(STP) public var completion: STPIntentClientSecretCompletionBlock + + @_spi(STP) public init( + delegate: _stpinternal_STPApplePayContextDelegateBase, + context: STPApplePayContext, + paymentMethod: StripeAPI.PaymentMethod, + paymentInformation: PKPayment, + completion: @escaping STPIntentClientSecretCompletionBlock + ) { + self.delegate = delegate + self.context = context + self.paymentMethod = paymentMethod + self.paymentInformation = paymentInformation + self.completion = completion + } +} + +/// Internal Apple Pay class. Do not use. +/// :nodoc: +@objc @_spi(STP) public class _stpinternal_ApplePayContextDidCompleteStorage: NSObject { + @_spi(STP) public weak var delegate: _stpinternal_STPApplePayContextDelegateBase? + @_spi(STP) public var context: STPApplePayContext + @_spi(STP) public var status: STPApplePayContext.PaymentStatus + @_spi(STP) public var error: Error? + + @_spi(STP) public init( + delegate: _stpinternal_STPApplePayContextDelegateBase, + context: STPApplePayContext, + status: STPApplePayContext.PaymentStatus, + error: Error? + ) { + self.delegate = delegate + self.context = context + self.status = status + self.error = error + } +} diff --git a/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext.swift b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext.swift new file mode 100644 index 00000000..9fa84a3d --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/ApplePayContext/STPApplePayContext.swift @@ -0,0 +1,824 @@ +// +// STPApplePayContext.swift +// StripeApplePay +// +// Created by Yuki Tokuhiro on 2/20/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import Foundation +import ObjectiveC +import PassKit +@_spi(STP) import StripeCore + +/// :nodoc: +@objc public protocol _stpinternal_STPApplePayContextDelegateBase: NSObjectProtocol { + /// Called when the user selects a new shipping method. The delegate should determine + /// shipping costs based on the shipping method and either the shipping address supplied in the original + /// PKPaymentRequest or the address fragment provided by the last call to paymentAuthorizationController: + /// didSelectShippingContact:completion:. + /// You must invoke the completion block with an updated array of PKPaymentSummaryItem objects. + @objc(applePayContext:didSelectShippingMethod:handler:) + optional func applePayContext( + _ context: STPApplePayContext, + didSelect shippingMethod: PKShippingMethod, + handler: @escaping (_ update: PKPaymentRequestShippingMethodUpdate) -> Void + ) + + /// Called when the user has selected a new shipping address. You should inspect the + /// address and must invoke the completion block with an updated array of PKPaymentSummaryItem objects. + /// @note To maintain privacy, the shipping information is anonymized. For example, in the United States it only includes the city, state, and zip code. This provides enough information to calculate shipping costs, without revealing sensitive information until the user actually approves the purchase. + /// Receive full shipping information in the paymentInformation passed to `applePayContext:didCreatePaymentMethod:paymentInformation:completion:` + @objc optional func applePayContext( + _ context: STPApplePayContext, + didSelectShippingContact contact: PKContact, + handler: @escaping (_ update: PKPaymentRequestShippingContactUpdate) -> Void + ) + + /// Called when the user has entered or updated a coupon code. You should validate the + /// coupon and must invoke the completion block with a PKPaymentRequestCouponCodeUpdate object. + @available(iOS 15.0, *) + @objc optional func applePayContext( + _ context: STPApplePayContext, + didChangeCouponCode couponCode: String, + handler completion: @escaping (PKPaymentRequestCouponCodeUpdate) -> Void + ) + + /// Optionally configure additional information on your PKPaymentAuthorizationResult. + /// This closure will be called after the PaymentIntent or SetupIntent is confirmed, but before + /// the Apple Pay sheet has been closed. + /// In your implementation, you can configure the PKPaymentAuthorizationResult to add custom fields, such as `orderDetails`. + /// See https://developer.apple.com/documentation/passkit/pkpaymentauthorizationresult for all configuration options. + /// This method is optional. If you implement this, you must call the handler block with the PKPaymentAuthorizationResult on the main queue. + /// WARNING: If you do not call the completion handler, your app will hang until the Apple Pay sheet times out. + @objc optional func applePayContext( + _ context: STPApplePayContext, + willCompleteWithResult authorizationResult: PKPaymentAuthorizationResult, + handler: @escaping (_ authorizationResult: PKPaymentAuthorizationResult) -> Void + ) +} + +/// Implement the required methods of this delegate to supply a PaymentIntent to ApplePayContext and be notified of the completion of the Apple Pay payment. +/// You may also implement the optional delegate methods to handle shipping methods and shipping address changes e.g. to verify you can ship to the address, or update the payment amount. +public protocol ApplePayContextDelegate: _stpinternal_STPApplePayContextDelegateBase { + /// Called after the customer has authorized Apple Pay. Implement this method to call the completion block with the client secret of a PaymentIntent or SetupIntent. + /// - Parameters: + /// - paymentMethod: The PaymentMethod that represents the customer's Apple Pay payment method. + /// If you create the PaymentIntent with confirmation_method=manual, pass `paymentMethod.id` as the payment_method and confirm=true. Otherwise, you can ignore this parameter. + /// - paymentInformation: The underlying PKPayment created by Apple Pay. + /// If you create the PaymentIntent with confirmation_method=manual, you can collect shipping information using its `shippingContact` and `shippingMethod` properties. + /// - completion: Call this with the PaymentIntent or SetupIntent client secret, or the error that occurred creating the PaymentIntent or SetupIntent. + func applePayContext( + _ context: STPApplePayContext, + didCreatePaymentMethod paymentMethod: StripeAPI.PaymentMethod, + paymentInformation: PKPayment, + completion: @escaping STPIntentClientSecretCompletionBlock + ) + + /// Called after the Apple Pay sheet is dismissed with the result of the payment. + /// Your implementation could stop a spinner and display a receipt view or error to the customer, for example. + /// - Parameters: + /// - status: The status of the payment + /// - error: The error that occurred, if any. + func applePayContext( + _ context: STPApplePayContext, + didCompleteWith status: STPApplePayContext.PaymentStatus, + error: Error? + ) +} + +/// A helper class that implements Apple Pay. +/// Usage looks like this: +/// 1. Initialize this class with a PKPaymentRequest describing the payment request (amount, line items, required shipping info, etc) +/// 2. Call presentApplePayOnViewController:completion: to present the Apple Pay sheet and begin the payment process +/// 3 (optional): If you need to respond to the user changing their shipping information/shipping method, implement the optional delegate methods +/// 4. When the user taps 'Buy', this class uses the PaymentIntent that you supply in the applePayContext:didCreatePaymentMethod:completion: delegate method to complete the payment +/// 5. After payment completes/errors and the sheet is dismissed, this class informs you in the applePayContext:didCompleteWithStatus: delegate method +/// - seealso: https://stripe.com/docs/apple-pay#native for a full guide +/// - seealso: ApplePayExampleViewController for an example +@objc(STPApplePayContext) +public class STPApplePayContext: NSObject, PKPaymentAuthorizationControllerDelegate { + enum Error: Swift.Error { + case invalidFinalState + } + /// A special string that can be passed in place of a intent client secret to force showing success and return a PaymentState of `success`. + /// - Note: ⚠️ If provided, the SDK performs no action to complete the payment or setup - it doesn't confirm a PaymentIntent or SetupIntent or handle next actions. + /// You should only use this if your integration can't create a PaymentIntent or SetupIntent. It is your responsibility to ensure that you only pass this value if the payment or set up is successful. + @_spi(STP) public static let COMPLETE_WITHOUT_CONFIRMING_INTENT = "COMPLETE_WITHOUT_CONFIRMING_INTENT" + + /// Initializes this class. + /// @note This may return nil if the request is invalid e.g. the user is restricted by parental controls, or can't make payments on any of the request's supported networks + /// @note If using Swift, using ApplePayContextDelegate is recommended over STPApplePayContextDelegate. + /// - Parameters: + /// - paymentRequest: The payment request to use with Apple Pay. + /// - delegate: The delegate. + @objc(initWithPaymentRequest:delegate:) + public required init?( + paymentRequest: PKPaymentRequest, + delegate: _stpinternal_STPApplePayContextDelegateBase? + ) { + STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: STPApplePayContext.self) + let canMakePayments: Bool = { + if #available(iOS 15.0, *) { + // On iOS 15+, Apple Pay can be displayed even though there are no cards because Apple added the ability for customers to add cards in the payment sheet (see WWDC '21 "What's new in Wallet and Apple Pay") + return PKPaymentAuthorizationController.canMakePayments() + } else { + return PKPaymentAuthorizationController.canMakePayments(usingNetworks: StripeAPI.supportedPKPaymentNetworks()) + } + }() + + assert(!paymentRequest.merchantIdentifier.isEmpty, "You must set `merchantIdentifier` on your payment request.") + guard + canMakePayments, + !paymentRequest.merchantIdentifier.isEmpty, + // PKPaymentAuthorizationController's docs incorrectly state: + // "If the user can’t make payments on any of the payment request’s supported networks, initialization fails and this method returns nil." + // In actuality, this initializer is non-nullable. To make sure we return nil when the request is invalid, we'll use PKPaymentAuthorizationViewController's initializer, which *is* nullable. + PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) != nil + else { + return nil + } + authorizationController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) + + self.delegate = delegate + + super.init() + authorizationController?.delegate = self + } + + private var presentationWindow: UIWindow? + + /// Presents the Apple Pay sheet from the key window, starting the payment process. + /// @note This method should only be called once; create a new instance of STPApplePayContext every time you present Apple Pay. + /// - Parameters: + /// - completion: Called after the Apple Pay sheet is presented + @available(iOSApplicationExtension, unavailable) + @available(macCatalystApplicationExtension, unavailable) + @objc(presentApplePayWithCompletion:) + public func presentApplePay(completion: STPVoidBlock? = nil) { + #if os(visionOS) + // This isn't great: We should encourage the use of presentApplePay(from window:) instead. + let windows = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.windows } + .flatMap { $0 } + .sorted { firstWindow, _ in firstWindow.isKeyWindow } + let window = windows.first + #else + let window = UIApplication.shared.windows.first { $0.isKeyWindow } + #endif + self.presentApplePay(from: window, completion: completion) + } + + /// Presents the Apple Pay sheet from the specified window, starting the payment process. + /// @note This method should only be called once; create a new instance of STPApplePayContext every time you present Apple Pay. + /// - Parameters: + /// - window: The UIWindow to host the Apple Pay sheet + /// - completion: Called after the Apple Pay sheet is presented + @objc(presentApplePayFromWindow:completion:) + public func presentApplePay(from window: UIWindow?, completion: STPVoidBlock? = nil) { + presentationWindow = window + guard !didPresentApplePay, let applePayController = self.authorizationController else { + assert( + false, + "This method should only be called once; create a new instance of STPApplePayContext every time you present Apple Pay." + ) + return + } + didPresentApplePay = true + + // This instance (and the associated Objective-C bridge object, if any) must live so + // that the apple pay sheet is dismissed; until then, the app is effectively frozen. + objc_setAssociatedObject( + applePayController, + UnsafeRawPointer(&kApplePayContextAssociatedObjectKey), + self, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + + applePayController.present { (_) in + DispatchQueue.main.async { + completion?() + } + } + } + + /// Presents the Apple Pay sheet from the specified view controller, starting the payment process. + /// @note This method should only be called once; create a new instance of STPApplePayContext every time you present Apple Pay. + /// @deprecated A presenting UIViewController is no longer needed. Use presentApplePay(completion:) instead. + /// - Parameters: + /// - viewController: The UIViewController instance to present the Apple Pay sheet on + /// - completion: Called after the Apple Pay sheet is presented + @objc(presentApplePayOnViewController:completion:) + @available( + *, + deprecated, + message: "Use `presentApplePay(completion:)` instead.", + renamed: "presentApplePay(completion:)" + ) + public func presentApplePay( + on viewController: UIViewController, + completion: STPVoidBlock? = nil + ) { + let window = viewController.viewIfLoaded?.window + presentApplePay(from: window, completion: completion) + } + + /// The API Client to use to make requests. + /// Defaults to `STPAPIClient.shared` + @objc public var apiClient: STPAPIClient = STPAPIClient.shared + /// ApplePayContext passes this to the /confirm endpoint for PaymentIntents if it did not collect shipping details itself. + /// :nodoc: + @_spi(STP) public var shippingDetails: StripeAPI.ShippingDetails? + private weak var delegate: _stpinternal_STPApplePayContextDelegateBase? + @objc var authorizationController: PKPaymentAuthorizationController? + @_spi(STP) public var returnUrl: String? + + @_spi(STP) @frozen public enum ConfirmType { + case client + case server + /// The merchant backend used the special string instead of a intent client secret, so we completed the payment without confirming an intent. + case none + } + /// Tracks where the call to confirm the PaymentIntent or SetupIntent happened. + @_spi(STP) public var confirmType: ConfirmType? + // Internal state + private var paymentState: PaymentState = .notStarted + private var error: Swift.Error? + /// YES if the flow cancelled or timed out. This toggles which delegate method (didFinish or didAuthorize) calls our didComplete delegate method + private var didCancelOrTimeoutWhilePending = false + private var didPresentApplePay = false + + /// :nodoc: + @objc public override func responds(to aSelector: Selector!) -> Bool { + // ApplePayContextDelegate exposes methods that map 1:1 to PKPaymentAuthorizationControllerDelegate methods + // We want this method to return YES for these methods IFF they are implemented by our delegate + + // Why not simply implement the methods to call their equivalents on self.delegate? + // The implementation of e.g. didSelectShippingMethod must call the completion block. + // If the user does not implement e.g. didSelectShippingMethod, we don't know the correct PKPaymentSummaryItems to pass to the completion block + // (it may have changed since we were initialized due to another delegate method) + if let equivalentDelegateSelector = _delegateToAppleDelegateMapping()[aSelector] { + return delegate?.responds(to: equivalentDelegateSelector) ?? false + } else { + return super.responds(to: aSelector) + } + } + + // MARK: - Private Helper + func _delegateToAppleDelegateMapping() -> [Selector: Selector] { + typealias pkDidSelectShippingMethodSignature = + (any PKPaymentAuthorizationControllerDelegate) -> ( + ( + PKPaymentAuthorizationController, + PKShippingMethod, + @escaping (PKPaymentRequestShippingMethodUpdate) -> Void + ) -> Void + )? + let pk_didSelectShippingMethod = #selector( + (PKPaymentAuthorizationControllerDelegate.paymentAuthorizationController( + _: + didSelectShippingMethod: + handler: + )) as pkDidSelectShippingMethodSignature) + let stp_didSelectShippingMethod = #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext(_:didSelect:handler:)) + let pk_didSelectShippingContact = #selector( + PKPaymentAuthorizationControllerDelegate.paymentAuthorizationController( + _: + didSelectShippingContact: + handler: + )) + let stp_didSelectShippingContact = #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext( + _: + didSelectShippingContact: + handler: + )) + + var delegateToAppleDelegateMapping = [ + pk_didSelectShippingMethod: stp_didSelectShippingMethod, + pk_didSelectShippingContact: stp_didSelectShippingContact, + ] + + if #available(iOS 15.0, *) { + // On iOS 15+, Apple Pay can now accept coupon codes directly, so we need to broker the + // new coupon delegate functions between the host app and Apple Pay. + let pk_didChangeCouponCode = #selector( + PKPaymentAuthorizationControllerDelegate.paymentAuthorizationController( + _: + didChangeCouponCode: + handler: + )) + let stp_didChangeCouponCode = #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext( + _: + didChangeCouponCode: + handler: + )) + + delegateToAppleDelegateMapping[pk_didChangeCouponCode] = stp_didChangeCouponCode + } + + return delegateToAppleDelegateMapping + } + + func _end() { + if let authorizationController = authorizationController { + objc_setAssociatedObject( + authorizationController, + UnsafeRawPointer(&kApplePayContextAssociatedObjectKey), + nil, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + authorizationController = nil + delegate = nil + } + + func _shippingDetails(from payment: PKPayment) -> StripeAPI.ShippingDetails? { + guard let address = payment.shippingContact?.postalAddress, + let name = payment.shippingContact?.name + else { + // The shipping address street and name are required parameters for a valid .ShippingDetails + // Return `shippingDetails` instead + return shippingDetails + } + + let addressParams = StripeAPI.ShippingDetails.Address( + city: address.city, + country: address.isoCountryCode, + line1: address.street, + postalCode: address.postalCode, + state: address.state + ) + + let formatter = PersonNameComponentsFormatter() + formatter.style = .long + let shippingParams = StripeAPI.ShippingDetails( + address: addressParams, + name: formatter.string(from: name), + phone: payment.shippingContact?.phoneNumber?.stringValue + ) + + return shippingParams + } + + // MARK: - PKPaymentAuthorizationControllerDelegate + /// :nodoc: + @objc(paymentAuthorizationController:didAuthorizePayment:handler:) + public func paymentAuthorizationController( + _ controller: PKPaymentAuthorizationController, + didAuthorizePayment payment: PKPayment, + handler completion: @escaping (PKPaymentAuthorizationResult) -> Void + ) { + // Some observations (on iOS 12 simulator): + // - The docs say localizedDescription can be shown in the Apple Pay sheet, but I haven't seen this. + // - If you call the completion block w/ a status of .failure and an error, the user is prompted to try again. + + _completePayment(with: payment) { status, error in + let errors = [STPAPIClient.pkPaymentError(forStripeError: error)].compactMap({ $0 }) + let result = PKPaymentAuthorizationResult(status: status, errors: errors) + if self.delegate?.responds( + to: #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext( + _: + willCompleteWithResult: + handler: + )) + ) + ?? false + { + self.delegate?.applePayContext?( + self, + willCompleteWithResult: result, + handler: { newResult in + completion(newResult) + } + ) + } else { + completion(result) + } + } + } + + /// :nodoc: + @objc + public func paymentAuthorizationController( + _ controller: PKPaymentAuthorizationController, + didSelectShippingMethod shippingMethod: PKShippingMethod, + handler completion: @escaping (PKPaymentRequestShippingMethodUpdate) -> Void + ) { + if delegate?.responds( + to: #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext(_:didSelect:handler:)) + ) + ?? false + { + delegate?.applePayContext?(self, didSelect: shippingMethod, handler: completion) + } + } + + /// :nodoc: + @objc + public func paymentAuthorizationController( + _ controller: PKPaymentAuthorizationController, + didSelectShippingContact contact: PKContact, + handler completion: @escaping (PKPaymentRequestShippingContactUpdate) -> Void + ) { + if delegate?.responds( + to: #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext( + _: + didSelectShippingContact: + handler: + )) + ) ?? false { + delegate?.applePayContext?(self, didSelectShippingContact: contact, handler: completion) + } + } + + /// :nodoc: + @available(iOS 15.0, *) + @objc + public func paymentAuthorizationController( + _ controller: PKPaymentAuthorizationController, + didChangeCouponCode couponCode: String, + handler completion: @escaping (PKPaymentRequestCouponCodeUpdate) -> Void) { + + if delegate?.responds( + to: #selector( + _stpinternal_STPApplePayContextDelegateBase.applePayContext(_:didChangeCouponCode:handler:)) + ) ?? false { + delegate?.applePayContext?(self, didChangeCouponCode: couponCode, handler: completion) + } + } + + /// :nodoc: + @objc public func paymentAuthorizationControllerDidFinish( + _ controller: PKPaymentAuthorizationController + ) { + // Note: If you don't dismiss the VC, the UI disappears, the VC blocks interaction, and this method gets called again. + // Note: This method is called if the user cancels (taps outside the sheet) or Apple Pay times out (empirically 30 seconds) + switch paymentState { + case .notStarted: + controller.dismiss { + stpDispatchToMainThreadIfNecessary { + self.callDidCompleteDelegate(status: .userCancellation, error: nil) + self._end() + } + } + case .pending: + // We can't cancel a pending payment. If we dismiss the VC now, the customer might interact with the app and miss seeing the result of the payment - risking a double charge, chargeback, etc. + // Instead, we'll dismiss and notify our delegate when the payment finishes. + didCancelOrTimeoutWhilePending = true + case .error: + controller.dismiss { + stpDispatchToMainThreadIfNecessary { + self.callDidCompleteDelegate(status: .error, error: self.error) + self._end() + } + } + case .success: + controller.dismiss { + stpDispatchToMainThreadIfNecessary { + self.callDidCompleteDelegate(status: .success, error: nil) + self._end() + } + } + } + } + + /// :nodoc: + @objc public func presentationWindow( + for controller: PKPaymentAuthorizationController + ) -> UIWindow? { + return presentationWindow + } + + // MARK: - Helpers + func _completePayment( + with payment: PKPayment, + completion: @escaping (PKPaymentAuthorizationStatus, Swift.Error?) -> Void + ) { + // Helper to handle annoying logic around "Do I call completion block or dismiss + call delegate?" + let handleFinalState: ((PaymentState, Swift.Error?) -> Void) = { state, error in + switch state { + case .error: + self.paymentState = .error + self.error = error + if self.didCancelOrTimeoutWhilePending { + self.authorizationController?.dismiss { + DispatchQueue.main.async { + self.callDidCompleteDelegate(status: .error, error: self.error) + self._end() + } + } + } else { + completion(PKPaymentAuthorizationStatus.failure, error) + } + return + case .success: + self.paymentState = .success + if self.didCancelOrTimeoutWhilePending { + self.authorizationController?.dismiss { + DispatchQueue.main.async { + self.callDidCompleteDelegate(status: .success, error: nil) + self._end() + } + } + } else { + completion(PKPaymentAuthorizationStatus.success, nil) + } + return + case .pending, .notStarted: + let errorAnalytic = ErrorAnalytic(event: .unexpectedApplePayError, + error: Error.invalidFinalState) + STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic) + stpAssertionFailure("Invalid final state") + return + } + } + + // 1. Create PaymentMethod + StripeAPI.PaymentMethod.create(apiClient: apiClient, payment: payment) { result in + guard let paymentMethod = try? result.get(), self.authorizationController != nil else { + if case .failure(let error) = result { + handleFinalState(.error, error) + } else { + handleFinalState(.error, nil) + } + return + } + + let paymentMethodCompletion: STPIntentClientSecretCompletionBlock = { + clientSecret, + intentCreationError in + guard let clientSecret = clientSecret, intentCreationError == nil, + self.authorizationController != nil + else { + handleFinalState(.error, intentCreationError) + return + } + + guard clientSecret != STPApplePayContext.COMPLETE_WITHOUT_CONFIRMING_INTENT else { + self.confirmType = STPApplePayContext.ConfirmType.none + handleFinalState(.success, nil) + return + } + + if StripeAPI.SetupIntentConfirmParams.isClientSecretValid(clientSecret) { + // 3a. Retrieve the SetupIntent and see if we need to confirm it client-side + StripeAPI.SetupIntent.get(apiClient: self.apiClient, clientSecret: clientSecret) + { + result in + guard let setupIntent = try? result.get(), + self.authorizationController != nil + else { + if case .failure(let error) = result { + handleFinalState(.error, error) + } else { + handleFinalState(.error, nil) + } + return + } + + switch setupIntent.status { + case .requiresConfirmation, .requiresAction, .requiresPaymentMethod: + self.confirmType = .client + // 4a. Confirm the SetupIntent + self.paymentState = .pending // After this point, we can't cancel + var confirmParams = StripeAPI.SetupIntentConfirmParams( + clientSecret: clientSecret + ) + confirmParams.paymentMethod = paymentMethod.id + confirmParams.useStripeSdk = true + confirmParams.returnUrl = self.returnUrl + + StripeAPI.SetupIntent.confirm( + apiClient: self.apiClient, + params: confirmParams + ) { + result in + guard let setupIntent = try? result.get(), + self.authorizationController != nil, + setupIntent.status == .succeeded + else { + if case .failure(let error) = result { + handleFinalState(.error, error) + } else { + handleFinalState(.error, nil) + } + return + } + + handleFinalState(.success, nil) + } + case .succeeded: + self.confirmType = .server + handleFinalState(.success, nil) + case .canceled, .processing, .unknown, .unparsable, .none: + handleFinalState( + .error, + Self.makeUnknownError( + message: + "The SetupIntent is in an unexpected state: \(setupIntent.status!)" + ) + ) + } + } + } else { + let paymentIntentClientSecret = clientSecret + // 3b. Retrieve the PaymentIntent and see if we need to confirm it client-side + StripeAPI.PaymentIntent.get( + apiClient: self.apiClient, + clientSecret: paymentIntentClientSecret + ) { result in + guard let paymentIntent = try? result.get(), + self.authorizationController != nil + else { + if case .failure(let error) = result { + handleFinalState(.error, error) + } else { + handleFinalState(.error, nil) + } + return + } + + if paymentIntent.confirmationMethod == .automatic + && (paymentIntent.status == .requiresPaymentMethod + || paymentIntent.status == .requiresConfirmation) + { + self.confirmType = .client + // 4b. Confirm the PaymentIntent + + var paymentIntentParams = StripeAPI.PaymentIntentParams( + clientSecret: paymentIntentClientSecret + ) + paymentIntentParams.paymentMethod = paymentMethod.id + paymentIntentParams.useStripeSdk = true + // If a merchant attaches shipping to the PI on their server, the /confirm endpoint will error if we update shipping with a “requires secret key” error message. + // To accommodate this, don't attach if our shipping is the same as the PI's shipping + if paymentIntent.shipping != self._shippingDetails(from: payment) { + paymentIntentParams.shipping = self._shippingDetails(from: payment) + } + + self.paymentState = .pending // After this point, we can't cancel + + // We don't use PaymentHandler because we can't handle next actions as-is - we'd need to dismiss the Apple Pay VC. + StripeAPI.PaymentIntent.confirm( + apiClient: self.apiClient, + params: paymentIntentParams + ) { + result in + guard let postConfirmPI = try? result.get(), + postConfirmPI.status == .succeeded + || postConfirmPI.status == .requiresCapture + else { + if case .failure(let error) = result { + handleFinalState(.error, error) + } else { + handleFinalState(.error, nil) + } + return + } + handleFinalState(.success, nil) + } + } else if paymentIntent.status == .succeeded + || paymentIntent.status == .requiresCapture + { + self.confirmType = .server + handleFinalState(.success, nil) + } else { + let unknownError = Self.makeUnknownError( + message: + "The PaymentIntent is in an unexpected state. If you pass confirmation_method = manual when creating the PaymentIntent, also pass confirm = true. If server-side confirmation fails, double check you are passing the error back to the client." + ) + handleFinalState(.error, unknownError) + } + } + } + } + // 2. Fetch PaymentIntent/SetupIntent client secret from delegate + let legacyDelegateSelector = NSSelectorFromString( + "applePayContext:didCreatePaymentMethod:paymentInformation:completion:" + ) + if let delegate = self.delegate { + if let delegate = delegate as? ApplePayContextDelegate { + delegate.applePayContext( + self, + didCreatePaymentMethod: paymentMethod, + paymentInformation: payment, + completion: paymentMethodCompletion + ) + } else if delegate.responds(to: legacyDelegateSelector), + let helperClass = NSClassFromString("STPApplePayContextLegacyHelper") + { + let legacyStorage = _stpinternal_ApplePayContextDidCreatePaymentMethodStorage( + delegate: delegate, + context: self, + paymentMethod: paymentMethod, + paymentInformation: payment, + completion: paymentMethodCompletion + ) + helperClass.performDidCreatePaymentMethod(legacyStorage) + } else { + assertionFailure( + "An STPApplePayContext's delegate must conform to ApplePayContextDelegate or STPApplePayContextDelegate." + ) + } + } + } + } + + func callDidCompleteDelegate(status: PaymentStatus, error: Swift.Error?) { + if let delegate = self.delegate { + if let delegate = delegate as? ApplePayContextDelegate { + delegate.applePayContext(self, didCompleteWith: status, error: error) + } else if delegate.responds( + to: NSSelectorFromString("applePayContext:didCompleteWithStatus:error:") + ) { + if let helperClass = NSClassFromString("STPApplePayContextLegacyHelper") { + let legacyStorage = _stpinternal_ApplePayContextDidCompleteStorage( + delegate: delegate, + context: self, + status: status, + error: error + ) + helperClass.performDidComplete(legacyStorage) + } + } else { + assertionFailure( + "An STPApplePayContext's delegate must conform to ApplePayContextDelegate or STPApplePayContextDelegate." + ) + } + } + + } + + @_spi(STP) public static func makeUnknownError(message: String) -> NSError { + let userInfo = [ + NSLocalizedDescriptionKey: NSError.stp_unexpectedErrorMessage(), + STPError.errorMessageKey: message, + ] + return NSError( + domain: STPError.STPPaymentHandlerErrorDomain, + code: STPPaymentHandlerErrorCodeIntentStatusErrorCode, + userInfo: userInfo + ) + } + + /// This is STPPaymentHandlerErrorCode.intentStatusErrorCode.rawValue, which we don't want to vend from this framework. + fileprivate static let STPPaymentHandlerErrorCodeIntentStatusErrorCode = 3 + + enum PaymentState { + case notStarted + case pending + case error + case success + } + + /// An enum representing the status of a payment requested from the user. + @frozen public enum PaymentStatus { + /// The payment succeeded. + case success + /// The payment failed due to an unforeseen error, such as the user's Internet connection being offline. + case error + /// The user cancelled the payment (for example, by hitting "cancel" in the Apple Pay dialog). + case userCancellation + } +} + +/// :nodoc: +@_spi(STP) extension STPApplePayContext: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier: String { + return "STPApplePayContext" + } +} + +/// :nodoc: +class ModernApplePayContext: STPAnalyticsProtocol { + @_spi(STP) public static var stp_analyticsIdentifier: String { + return "ModernApplePayContext" + } +} + +private var kSTPApplePayContextAssociatedObjectKey = 0 +enum STPPaymentState: Int { + case notStarted + case pending + case error + case success +} + +private class _stpinternal_STPApplePayContextLegacyHelper: NSObject { + @objc class func performDidCreatePaymentMethod( + _ storage: _stpinternal_ApplePayContextDidCreatePaymentMethodStorage + ) { + // Placeholder to allow this to be called on AnyObject + } + @objc class func performDidComplete(_ storage: _stpinternal_ApplePayContextDidCompleteStorage) { + // Placeholder to allow this to be called on AnyObject + } +} + +private var kApplePayContextAssociatedObjectKey = 0 diff --git a/StripeApplePay/StripeApplePay/Source/Blocks.swift b/StripeApplePay/StripeApplePay/Source/Blocks.swift new file mode 100644 index 00000000..e434acbc --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/Blocks.swift @@ -0,0 +1,18 @@ +// +// Blocks.swift +// StripeApplePay +// +// Created by David Estes on 1/6/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation + +/// An empty block, called with no arguments, returning nothing. +public typealias STPVoidBlock = () -> Void + +/// A block to be run with the client secret of a PaymentIntent or SetupIntent. +/// - Parameters: +/// - clientSecret: The client secret of the PaymentIntent or SetupIntent. See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-client_secret +/// - error: The error that occurred when creating the Intent, or nil if none occurred. +public typealias STPIntentClientSecretCompletionBlock = (String?, Error?) -> Void diff --git a/StripeApplePay/StripeApplePay/Source/Extensions/BillingDetails+ApplePay.swift b/StripeApplePay/StripeApplePay/Source/Extensions/BillingDetails+ApplePay.swift new file mode 100644 index 00000000..8e1ed5c2 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/Extensions/BillingDetails+ApplePay.swift @@ -0,0 +1,101 @@ +// +// BillingDetails+ApplePay.swift +// StripeApplePay +// +// Created by David Estes on 8/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore + +extension StripeContact { + /// Initializes a new Contact with data from an PassKit contact. + /// - Parameter contact: The PassKit contact you want to populate the Contact from. + /// - Returns: A new Contact with data copied from the passed in contact. + init( + pkContact contact: PKContact + ) { + let nameComponents = contact.name + if let nameComponents = nameComponents { + givenName = stringIfHasContentsElseNil(nameComponents.givenName) + familyName = stringIfHasContentsElseNil(nameComponents.familyName) + + name = stringIfHasContentsElseNil( + PersonNameComponentsFormatter.localizedString(from: nameComponents, style: .default) + ) + } + email = stringIfHasContentsElseNil(contact.emailAddress) + if let phoneNumber = contact.phoneNumber { + phone = sanitizedPhoneString(from: phoneNumber) + } else { + phone = nil + } + setAddressFromCNPostal(contact.postalAddress) + } + + private func sanitizedPhoneString(from phoneNumber: CNPhoneNumber) -> String? { + return stringIfHasContentsElseNil( + STPNumericStringValidator.sanitizedNumericString(for: phoneNumber.stringValue) + ) + } + + private mutating func setAddressFromCNPostal(_ address: CNPostalAddress?) { + line1 = stringIfHasContentsElseNil(address?.street) + city = stringIfHasContentsElseNil(address?.city) + state = stringIfHasContentsElseNil(address?.state) + postalCode = stringIfHasContentsElseNil(address?.postalCode) + country = stringIfHasContentsElseNil(address?.isoCountryCode.uppercased()) + } +} + +extension StripeAPI.BillingDetails { + init?( + from payment: PKPayment + ) { + var billingDetails: StripeAPI.BillingDetails? + if payment.billingContact != nil { + billingDetails = StripeAPI.BillingDetails() + if let billingContact = payment.billingContact { + let billingAddress = StripeContact(pkContact: billingContact) + billingDetails?.name = billingAddress.name + billingDetails?.email = billingAddress.email + billingDetails?.phone = billingAddress.phone + if billingContact.postalAddress != nil { + billingDetails?.address = StripeAPI.BillingDetails.Address( + contact: billingAddress + ) + } + } + } + + // The phone number and email in the "Contact" panel in the Apple Pay dialog go into the shippingContact, + // not the billingContact. To work around this, we should fill the billingDetails' email and phone + // number from the shippingDetails. + if payment.shippingContact != nil { + var shippingAddress: StripeContact? + if let shippingContact = payment.shippingContact { + shippingAddress = StripeContact(pkContact: shippingContact) + } + if billingDetails?.email == nil && shippingAddress?.email != nil { + if billingDetails == nil { + billingDetails = StripeAPI.BillingDetails() + } + billingDetails?.email = shippingAddress?.email + } + if billingDetails?.phone == nil && shippingAddress?.phone != nil { + if billingDetails == nil { + billingDetails = StripeAPI.BillingDetails() + } + billingDetails?.phone = shippingAddress?.phone + } + } + + if let billingDetails = billingDetails { + self = billingDetails + } else { + return nil + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/Extensions/PKContact+Stripe.swift b/StripeApplePay/StripeApplePay/Source/Extensions/PKContact+Stripe.swift new file mode 100644 index 00000000..03db9af0 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/Extensions/PKContact+Stripe.swift @@ -0,0 +1,26 @@ +// +// PKContact+Stripe.swift +// StripeApplePay +// +// Created by David Estes on 11/16/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit + +extension PKContact { + @_spi(STP) public var addressParams: [AnyHashable: Any] { + var params: [AnyHashable: Any] = [:] + let stpAddress = StripeContact(pkContact: self) + + params["name"] = stpAddress.name + params["address_line1"] = stpAddress.line1 + params["address_city"] = stpAddress.city + params["address_state"] = stpAddress.state + params["address_zip"] = stpAddress.postalCode + params["address_country"] = stpAddress.country + + return params + } +} diff --git a/StripeApplePay/StripeApplePay/Source/Extensions/PKPayment+Stripe.swift b/StripeApplePay/StripeApplePay/Source/Extensions/PKPayment+Stripe.swift new file mode 100644 index 00000000..2d85c930 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/Extensions/PKPayment+Stripe.swift @@ -0,0 +1,32 @@ +// +// PKPayment+Stripe.swift +// StripeApplePay +// +// Created by Ben Guo on 7/2/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +import PassKit + +extension PKPayment { + /// Returns true if the instance is a payment from the simulator. + @_spi(STP) public func stp_isSimulated() -> Bool { + return token.transactionIdentifier == "Simulated Identifier" + } + + /// Returns a fake transaction identifier with the expected ~-separated format. + @_spi(STP) public class func stp_testTransactionIdentifier() -> String { + var uuid = UUID().uuidString + uuid = uuid.replacingOccurrences(of: "~", with: "") + + // Simulated cards don't have enough info yet. For now, use a fake Visa number + let number = "4242424242424242" + + // Without the original PKPaymentRequest, we'll need to use fake data here. + let amount = NSDecimalNumber(string: "0") + let cents = NSNumber(value: amount.multiplying(byPowerOf10: 2).intValue).stringValue + let currency = "USD" + let identifier = ["ApplePayStubs", number, cents, currency, uuid].joined(separator: "~") + return identifier + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Address.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Address.swift new file mode 100644 index 00000000..dc80d6dd --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Address.swift @@ -0,0 +1,43 @@ +// +// Address.swift +// StripeApplePay +// +// Created by David Estes on 8/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +/// An internal struct for handling contacts. This is not encodable/decodable for use with the Stripe API. +struct StripeContact { + /// The user's full name (e.g. "Jane Doe") + public var name: String? + + /// The first line of the user's street address (e.g. "123 Fake St") + public var line1: String? + + /// The apartment, floor number, etc of the user's street address (e.g. "Apartment 1A") + public var line2: String? + + /// The city in which the user resides (e.g. "San Francisco") + public var city: String? + + /// The state in which the user resides (e.g. "CA") + public var state: String? + + /// The postal code in which the user resides (e.g. "90210") + public var postalCode: String? + + /// The ISO country code of the address (e.g. "US") + public var country: String? + + /// The phone number of the address (e.g. "8885551212") + public var phone: String? + + /// The email of the address (e.g. "jane@doe.com") + public var email: String? + + internal var givenName: String? + internal var familyName: String? +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/BillingDetails.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/BillingDetails.swift new file mode 100644 index 00000000..1871ec79 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/BillingDetails.swift @@ -0,0 +1,67 @@ +// +// BillingDetails.swift +// StripeApplePay +// +// Created by David Estes on 7/15/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + /// Billing information associated with a `STPPaymentMethod` that may be used or required by particular types of payment methods. + /// - seealso: https://stripe.com/docs/api/payment_methods/object#payment_method_object-billing_details + public struct BillingDetails: UnknownFieldsCodable { + /// Billing address. + public var address: Address? + + /// The billing address, a property sent in a PaymentMethod response + public struct Address: UnknownFieldsCodable { + /// The first line of the user's street address (e.g. "123 Fake St") + public var line1: String? + + /// The apartment, floor number, etc of the user's street address (e.g. "Apartment 1A") + public var line2: String? + + /// The city in which the user resides (e.g. "San Francisco") + public var city: String? + + /// The state in which the user resides (e.g. "CA") + public var state: String? + + /// The postal code in which the user resides (e.g. "90210") + public var postalCode: String? + + /// The ISO country code of the address (e.g. "US") + public var country: String? + + public var _additionalParametersStorage: NonEncodableParameters? + public var _allResponseFieldsStorage: NonEncodableParameters? + } + + /// Email address. + public var email: String? + /// Full name. + public var name: String? + /// Billing phone number (including extension). + public var phone: String? + + public var _additionalParametersStorage: NonEncodableParameters? + public var _allResponseFieldsStorage: NonEncodableParameters? + } + +} + +extension StripeAPI.BillingDetails.Address { + init( + contact: StripeContact + ) { + self.city = contact.city + self.country = contact.country + self.line1 = contact.line1 + self.line2 = contact.line2 + self.postalCode = contact.postalCode + self.state = contact.state + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/CardBrand.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/CardBrand.swift new file mode 100644 index 00000000..0ca26890 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/CardBrand.swift @@ -0,0 +1,33 @@ +// +// CardBrand.swift +// StripeApplePay +// +// Created by David Estes on 4/14/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation + +extension StripeAPI { + /// The various card brands to which a payment card can belong. + enum CardBrand: String, SafeEnumCodable { + /// Visa card + case visa = "Visa" + /// American Express card + case amex = "American Express" + /// Mastercard card + case mastercard = "MasterCard" + /// Discover card + case discover = "Discover" + /// JCB card + case JCB = "JCB" + /// Diners Club card + case dinersClub = "Diners Club" + /// UnionPay card + case unionPay = "UnionPay" + /// An unknown card brand type + case unknown = "Unknown" + /// An unparsable card brand + case unparsable + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntent.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntent.swift new file mode 100644 index 00000000..24cbfcb2 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntent.swift @@ -0,0 +1,149 @@ +// +// PaymentIntent.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + @_spi(STP) public struct PaymentIntent: UnknownFieldsDecodable { + // TODO: (MOBILESDK-468) Add modern bindings for more PaymentIntent fields + /// The Stripe ID of the PaymentIntent. + @_spi(STP) public let id: String + + /// The client secret used to fetch this PaymentIntent + @_spi(STP) public let clientSecret: String + + /// Amount intended to be collected by this PaymentIntent. + @_spi(STP) public let amount: Int + + /// If status is `.canceled`, when the PaymentIntent was canceled. + @_spi(STP) public let canceledAt: Date? + + /// Capture method of this PaymentIntent + @_spi(STP) public let captureMethod: CaptureMethod + + /// Confirmation method of this PaymentIntent + @_spi(STP) public let confirmationMethod: ConfirmationMethod + + /// When the PaymentIntent was created. + @_spi(STP) public let created: Date + + /// The currency associated with the PaymentIntent. + @_spi(STP) public let currency: String + + /// The `description` field of the PaymentIntent. + /// An arbitrary string attached to the object. Often useful for displaying to users. + @_spi(STP) public let stripeDescription: String? + + /// Whether or not this PaymentIntent was created in livemode. + @_spi(STP) public let livemode: Bool + + /// Email address that the receipt for the resulting payment will be sent to. + @_spi(STP) public let receiptEmail: String? + + /// The Stripe ID of the Source used in this PaymentIntent. + @_spi(STP) public let sourceId: String? + + /// The Stripe ID of the PaymentMethod used in this PaymentIntent. + @_spi(STP) public let paymentMethodId: String? + + /// Status of the PaymentIntent + @_spi(STP) public let status: Status + + /// Shipping information for this PaymentIntent. + @_spi(STP) public let shipping: ShippingDetails? + + /// Status types for a PaymentIntent + @frozen @_spi(STP) public enum Status: String, SafeEnumCodable { + /// Unknown status + case unknown + /// This PaymentIntent requires a PaymentMethod or Source + case requiresPaymentMethod = "requires_payment_method" + /// This PaymentIntent requires a Source + /// Deprecated: Use STPPaymentIntentStatusRequiresPaymentMethod instead. + @available( + *, + deprecated, + message: "Use STPPaymentIntentStatus.requiresPaymentMethod instead", + renamed: "STPPaymentIntentStatus.requiresPaymentMethod" + ) + case requiresSource = "requires_source" + /// This PaymentIntent needs to be confirmed + case requiresConfirmation = "requires_confirmation" + /// The selected PaymentMethod or Source requires additional authentication steps. + /// Additional actions found via `next_action` + case requiresAction = "requires_action" + /// The selected Source requires additional authentication steps. + /// Additional actions found via `next_source_action` + /// Deprecated: Use STPPaymentIntentStatusRequiresAction instead. + @available( + *, + deprecated, + message: "Use STPPaymentIntentStatus.requiresAction instead", + renamed: "STPPaymentIntentStatus.requiresAction" + ) + case requiresSourceAction = "requires_source_action" + /// Stripe is processing this PaymentIntent + case processing + /// The payment has succeeded + case succeeded + /// Indicates the payment must be captured, for STPPaymentIntentCaptureMethodManual + case requiresCapture = "requires_capture" + /// This PaymentIntent was canceled and cannot be changed. + case canceled + + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + @frozen @_spi(STP) public enum ConfirmationMethod: String, SafeEnumCodable { + /// Unknown confirmation method + case unknown + /// Confirmed via publishable key + case manual + /// Confirmed via secret key + case automatic + + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + @frozen @_spi(STP) public enum CaptureMethod: String, SafeEnumCodable { + /// Unknown capture method + case unknown + /// The PaymentIntent will be automatically captured + case automatic + /// The PaymentIntent must be manually captured once it has the status + /// `.requiresCapture` + case manual + + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + @_spi(STP) public var _allResponseFieldsStorage: NonEncodableParameters? + } +} + +extension StripeAPI.PaymentIntent { + /// Helper function for extracting PaymentIntent id from the Client Secret. + /// This avoids having to pass around both the id and the secret. + /// - Parameter clientSecret: The `client_secret` from the PaymentIntent + internal static func id(fromClientSecret clientSecret: String) -> String? { + // see parseClientSecret from stripe-js-v3 + let components = clientSecret.components(separatedBy: "_secret_") + if components.count >= 2 && components[0].hasPrefix("pi_") { + return components[0] + } else { + return nil + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntentParams.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntentParams.swift new file mode 100644 index 00000000..5dfe11a8 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentIntentParams.swift @@ -0,0 +1,101 @@ +// +// PaymentIntentParams.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + @_spi(STP) public struct PaymentIntentParams: UnknownFieldsEncodable { + /// The client secret of the PaymentIntent. Required + @_spi(STP) public let clientSecret: String + + @_spi(STP) public init( + clientSecret: String + ) { + self.clientSecret = clientSecret + } + + @_spi(STP) public var id: String? { + return PaymentIntent.id(fromClientSecret: clientSecret) + } + + /// Provide a supported `PaymentMethodParams` object, and Stripe will create a + /// PaymentMethod during PaymentIntent confirmation. + /// @note alternative to `paymentMethodId` + @_spi(STP) public var paymentMethodData: PaymentMethodParams? + + /// Provide an already created PaymentMethod's id, and it will be used to confirm the PaymentIntent. + /// @note alternative to `paymentMethodParams` + @_spi(STP) public var paymentMethod: String? + + /// Provide an already created Source's id, and it will be used to confirm the PaymentIntent. + @_spi(STP) public var sourceId: String? + + /// Email address that the receipt for the resulting payment will be sent to. + @_spi(STP) public var receiptEmail: String? + + /// `@YES` to save this PaymentIntent’s PaymentMethod or Source to the associated Customer, + /// if the PaymentMethod/Source is not already attached. + /// This should be a boolean NSNumber, so that it can be `nil` + @_spi(STP) public var savePaymentMethod: Bool? + + /// The URL to redirect your customer back to after they authenticate or cancel + /// their payment on the payment method’s app or site. + /// This should probably be a URL that opens your iOS app. + @_spi(STP) public var returnURL: String? + + /// When provided, this property indicates how you intend to use the payment method that your customer provides after the current payment completes. + /// If applicable, additional authentication may be performed to comply with regional legislation or network rules required to enable the usage of the same payment method for additional payments. + @_spi(STP) public var setupFutureUsage: SetupFutureUsage? + + /// A boolean number to indicate whether you intend to use the Stripe SDK's functionality to handle any PaymentIntent next actions. + /// If set to false, STPPaymentIntent.nextAction will only ever contain a redirect url that can be opened in a webview or mobile browser. + /// When set to true, the nextAction may contain information that the Stripe SDK can use to perform native authentication within your + /// app. + @_spi(STP) public var useStripeSdk: Bool? + + /// Shipping information. + @_spi(STP) public var shipping: ShippingDetails? + + /// Indicates how you intend to use the payment method that your customer provides after the current payment completes. + /// If applicable, additional authentication may be performed to comply with regional legislation or network rules required to enable the usage of the same payment method for additional payments. + /// - seealso: https://stripe.com/docs/api/payment_intents/object#payment_intent_object-setup_future_usage + @frozen @_spi(STP) public enum SetupFutureUsage: String, SafeEnumCodable { + /// Unknown value. Update your SDK, or use `allResponseFields` for custom handling. + case unknown + /// No value was provided. + case none + /// Indicates you intend to only reuse the payment method when the customer is in your checkout flow. + case onSession + /// Indicates you intend to reuse the payment method when the customer may or may not be in your checkout flow. + case offSession + + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + } +} + +extension StripeAPI.PaymentIntentParams { + static internal let isClientSecretValidRegex: NSRegularExpression = try! NSRegularExpression( + pattern: "^pi_[^_]+_secret_[^_]+$", + options: [] + ) + + @_spi(STP) public static func isClientSecretValid(_ clientSecret: String) -> Bool { + return + (isClientSecretValidRegex.numberOfMatches( + in: clientSecret, + options: .anchored, + range: NSRange(location: 0, length: clientSecret.count) + )) == 1 + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethod.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethod.swift new file mode 100644 index 00000000..235e47e2 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethod.swift @@ -0,0 +1,176 @@ +// +// PaymentMethod.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + /// PaymentMethod objects represent your customer's payment instruments. They can be used with PaymentIntents to collect payments. + /// - seealso: https://stripe.com/docs/api/payment_methods + public struct PaymentMethod: UnknownFieldsDecodable { + /// The Stripe ID of the PaymentMethod. + public let id: String + + /// Time at which the object was created. Measured in seconds since the Unix epoch. + public var created: Date? + /// `YES` if the object exists in live mode or the value `NO` if the object exists in test mode. + public var livemode = false + + /// The type of the PaymentMethod. The corresponding, similarly named property contains additional information specific to the PaymentMethod type. + /// e.g. if the type is `Card`, the `card` property is also populated. + public var type: PaymentMethodType? + + /// The type of the PaymentMethod. + @frozen public enum PaymentMethodType: String, SafeEnumCodable { + /// A card payment method. + case card + /// An unknown type. + case unknown + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + /// Billing information associated with the PaymentMethod that may be used or required by particular types of payment methods. + public var billingDetails: BillingDetails? + /// The ID of the Customer to which this PaymentMethod is saved. Nil when the PaymentMethod has not been saved to a Customer. + public var customerId: String? + /// If this is a card PaymentMethod (ie `self.type == .card`), this contains additional details. + public var card: Card? + + /// :nodoc: + public struct Card: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + /// The issuer of the card. + public private(set) var brand: Brand = .unknown + + /// The various card brands to which a payment card can belong. + @frozen public enum Brand: String, SafeEnumCodable { + /// Visa + case visa + /// American Express + case amex + /// Mastercard + case mastercard + /// Discover + case discover + /// JCB + case jcb + /// Diners Club + case diners + /// UnionPay + case unionpay + /// An unknown card brand + case unknown + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + /// Two-letter ISO code representing the country of the card. + public private(set) var country: String? + /// Two-digit number representing the card’s expiration month. + public private(set) var expMonth: Int + /// Four-digit number representing the card’s expiration year. + public private(set) var expYear: Int + /// Card funding type. Can be credit, debit, prepaid, or unknown. + public private(set) var funding: String? + /// The last four digits of the card. + public private(set) var last4: String? + /// Uniquely identifies this particular card number. You can use this attribute to check whether two customers who’ve signed up with you are using the same card number, for example. + public private(set) var fingerprint: String? + + /// Contains information about card networks that can be used to process the payment. + public private(set) var networks: Networks? + + /// Contains details on how this Card maybe be used for 3D Secure authentication. + public private(set) var threeDSecureUsage: ThreeDSecureUsage? + + /// If this Card is part of a Card Wallet, this contains the details of the Card Wallet. + public private(set) var wallet: Wallet? + + public struct Networks: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + + /// All available networks for the card. + public private(set) var available: [String]? + /// The preferred network for the card if one exists. + public private(set) var preferred: String? + } + + /// Contains details on how a `Card` may be used for 3D Secure authentication. + public struct ThreeDSecureUsage: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + + /// `true` if 3D Secure is supported on this card. + public private(set) var supported = false + } + + public struct Wallet: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + /// The type of the Card Wallet. A matching property is populated if the type is `.masterpass` or `.visaCheckout` containing additional information specific to the Card Wallet type. + public private(set) var type: WalletType = .unknown + /// Contains additional Masterpass information, if the type of the Card Wallet is `STPPaymentMethodCardWalletTypeMasterpass` + public private(set) var masterpass: Masterpass? + /// Contains additional Visa Checkout information, if the type of the Card Wallet is `STPPaymentMethodCardWalletTypeVisaCheckout` + public private(set) var visaCheckout: VisaCheckout? + + /// The type of Card Wallet. + @frozen public enum WalletType: String, SafeEnumCodable { + /// Amex Express Checkout + case amexExpressCheckout = "amex_express_checkout" + /// Apple Pay + case applePay = "apple_pay" + /// Google Pay + case googlePay = "google_pay" + /// Masterpass + case masterpass = "masterpass" + /// Samsung Pay + case samsungPay = "samsung_pay" + /// Visa Checkout + case visaCheckout = "visa_checkout" + /// An unknown Card Wallet type. + case unknown = "unknown" + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + public struct Masterpass: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + + /// Owner’s verified email. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var email: String? + /// Owner’s verified email. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var name: String? + /// Owner’s verified billing address. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var billingAddress: BillingDetails.Address? + /// Owner’s verified shipping address. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var shippingAddress: BillingDetails.Address? + } + + /// A Visa Checkout Card Wallet + /// - seealso: https://stripe.com/docs/visa-checkout + public struct VisaCheckout: UnknownFieldsDecodable { + /// Owner’s verified email. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var email: String? + /// Owner’s verified email. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var name: String? + /// Owner’s verified billing address. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var billingAddress: BillingDetails.Address? + /// Owner’s verified shipping address. Values are verified or provided by the payment method directly (and if supported) at the time of authorization or settlement. + public private(set) var shippingAddress: BillingDetails.Address? + + public var _allResponseFieldsStorage: NonEncodableParameters? + } + } + } + + public var _allResponseFieldsStorage: NonEncodableParameters? + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethodParams.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethodParams.swift new file mode 100644 index 00000000..c790a9a7 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/PaymentMethodParams.swift @@ -0,0 +1,73 @@ +// +// PaymentMethodParams.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + /// An object representing parameters used to create a PaymentMethod object. + /// - seealso: https://stripe.com/docs/api/payment_methods/create + @_spi(STP) public struct PaymentMethodParams: UnknownFieldsEncodable { + /// The type of payment method. + /// The associated property will contain additional information (e.g. `type == .card` means `card` should also be populated). + @_spi(STP) public var type: PaymentMethod.PaymentMethodType + + /// If this is a card PaymentMethod, this contains the user’s card details. + @_spi(STP) public var card: Card? + + /// Billing information associated with the PaymentMethod that may be used or required by particular types of payment methods. + @_spi(STP) public var billingDetails: BillingDetails? + + /// Used internally to identify the version of the SDK sending the request + @_spi(STP) public var paymentUserAgent: String? = { + return PaymentsSDKVariant.paymentUserAgent + }() + + /// :nodoc: + @_spi(STP) public struct Card: UnknownFieldsEncodable { + /// The card number, as a string without any separators. Ex. "4242424242424242" + @_spi(STP) public var number: String? + /// Number representing the card's expiration month. Ex. 1 + @_spi(STP) public var expMonth: Int? + /// Two- or four-digit number representing the card's expiration year. + @_spi(STP) public var expYear: Int? + /// For backwards compatibility, you can alternatively set this as a Stripe token (e.g., for Apple Pay) + @_spi(STP) public var token: String? + /// Card security code. It is highly recommended to always include this value. + @_spi(STP) public var cvc: String? + + /// The last 4 digits of the card's number, if it's been set, otherwise nil. + @_spi(STP) public var last4: String? { + if number != nil && (number?.count ?? 0) >= 4 { + return (number as NSString?)?.substring(from: (number?.count ?? 0) - 4) + } else { + return nil + } + } + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + } + + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + } +} + +extension StripeAPI.PaymentMethodParams.Card: CustomStringConvertible, CustomDebugStringConvertible, + CustomLeafReflectable +{ + @_spi(STP) public var debugDescription: String { + return description + } + + @_spi(STP) public var description: String { + return "Card \(last4 ?? "")" + } + + @_spi(STP) public var customMirror: Mirror { + return Mirror(reflecting: self.description) + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntent.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntent.swift new file mode 100644 index 00000000..a8369479 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntent.swift @@ -0,0 +1,58 @@ +// +// SetupIntent.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + @_spi(STP) public struct SetupIntent: UnknownFieldsDecodable { + @_spi(STP) public let id: String + // TODO: (MOBILESDK-467) Add modern bindings for more SetupIntent fields + @_spi(STP) public let status: SetupIntentStatus? + + /// Status types for an STPSetupIntent + @frozen @_spi(STP) public enum SetupIntentStatus: String, SafeEnumCodable { + /// Unknown status + case unknown + /// This SetupIntent requires a PaymentMethod + case requiresPaymentMethod = "requires_payment_method" + /// This SetupIntent needs to be confirmed + case requiresConfirmation = "requires_confirmation" + /// The selected PaymentMethod requires additional authentication steps. + /// Additional actions found via the `nextAction` property of `STPSetupIntent` + case requiresAction = "requires_action" + /// Stripe is processing this SetupIntent + case processing + /// The SetupIntent has succeeded + case succeeded + /// This SetupIntent was canceled and cannot be changed. + case canceled + + case unparsable + // TODO: This is @frozen because of a bug in the Xcode 12.2 Swift compiler. + // Remove @frozen after Xcode 12.2 support has been dropped. + } + + @_spi(STP) public var _allResponseFieldsStorage: NonEncodableParameters? + } +} + +extension StripeAPI.SetupIntent { + /// Helper function for extracting SetupIntent id from the Client Secret. + /// This avoids having to pass around both the id and the secret. + /// - Parameter clientSecret: The `client_secret` from the SetupIntent + internal static func id(fromClientSecret clientSecret: String) -> String? { + // see parseClientSecret from stripe-js-v3 + let components = clientSecret.components(separatedBy: "_secret_") + if components.count >= 2 && components[0].hasPrefix("seti_") { + return components[0] + } else { + return nil + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntentParams.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntentParams.swift new file mode 100644 index 00000000..72d750b1 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/SetupIntentParams.swift @@ -0,0 +1,66 @@ +// +// SetupIntentParams.swift +// StripeApplePay +// +// Created by David Estes on 6/29/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + @_spi(STP) public struct SetupIntentConfirmParams: UnknownFieldsEncodable { + /// Generated by by Editor -> Refactor -> Generate Memberwise Initializer + @_spi(STP) public init( + clientSecret: String, + paymentMethodData: StripeAPI.PaymentMethodParams? = nil, + paymentMethod: String? = nil, + returnUrl: String? = nil, + useStripeSdk: Bool? = nil, + _additionalParametersStorage: NonEncodableParameters? = nil + ) { + self.clientSecret = clientSecret + self.paymentMethodData = paymentMethodData + self.paymentMethod = paymentMethod + self.returnUrl = returnUrl + self.useStripeSdk = useStripeSdk + self._additionalParametersStorage = _additionalParametersStorage + } + + /// The client secret of the SetupIntent. Required. + @_spi(STP) public let clientSecret: String + /// Provide a supported `PaymentMethodParams` object, and Stripe will create a + /// PaymentMethod during PaymentIntent confirmation. + /// @note alternative to `paymentMethodId` + @_spi(STP) public var paymentMethodData: PaymentMethodParams? + /// Provide an already created PaymentMethod's id, and it will be used to confirm the SetupIntent. + /// @note alternative to `paymentMethodParams` + @_spi(STP) public var paymentMethod: String? + /// The URL to redirect your customer back to after they authenticate or cancel + /// their payment on the payment method’s app or site. + /// This should probably be a URL that opens your iOS app. + @_spi(STP) public var returnUrl: String? + /// A boolean number to indicate whether you intend to use the Stripe SDK's functionality to handle any SetupIntent next actions. + /// If set to false, SetupIntent.nextAction will only ever contain a redirect url that can be opened in a webview or mobile browser. + /// When set to true, the nextAction may contain information that the Stripe SDK can use to perform native authentication within your + /// app. + @_spi(STP) public var useStripeSdk: Bool? + + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + + // MARK: - Utilities + static private let regex = try! NSRegularExpression( + pattern: "^seti_[^_]+_secret_[^_]+$", + options: [] + ) + @_spi(STP) public static func isClientSecretValid(_ clientSecret: String) -> Bool { + return + (regex.numberOfMatches( + in: clientSecret, + options: .anchored, + range: NSRange(location: 0, length: clientSecret.count) + )) == 1 + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/ShippingDetails.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/ShippingDetails.swift new file mode 100644 index 00000000..cec8a3bb --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/ShippingDetails.swift @@ -0,0 +1,93 @@ +// +// ShippingDetails.swift +// StripeApplePay +// +// Created by Yuki Tokuhiro on 8/4/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI { + @_spi(STP) public struct ShippingDetails: UnknownFieldsCodable, Equatable { + @_spi(STP) public init( + address: StripeAPI.ShippingDetails.Address, + name: String, + carrier: String? = nil, + phone: String? = nil, + trackingNumber: String? = nil, + _allResponseFieldsStorage: NonEncodableParameters? = nil, + _additionalParametersStorage: NonEncodableParameters? = nil + ) { + self.address = address + self.name = name + self.carrier = carrier + self.phone = phone + self.trackingNumber = trackingNumber + self._allResponseFieldsStorage = _allResponseFieldsStorage + self._additionalParametersStorage = _additionalParametersStorage + } + + /// Shipping address. + @_spi(STP) public var address: Address + + /// Recipient name. + @_spi(STP) public var name: String + + /// The delivery service that shipped a physical product, such as Fedex, UPS, USPS, etc. + @_spi(STP) public var carrier: String? + + /// Recipient phone (including extension). + @_spi(STP) public var phone: String? + + /// The tracking number for a physical product, obtained from the delivery service. If multiple tracking numbers were generated for this purchase, please separate them with commas. + @_spi(STP) public var trackingNumber: String? + + @_spi(STP) public var _allResponseFieldsStorage: NonEncodableParameters? + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + + @_spi(STP) public struct Address: UnknownFieldsCodable, Equatable { + @_spi(STP) public init( + city: String? = nil, + country: String? = nil, + line1: String, + line2: String? = nil, + postalCode: String? = nil, + state: String? = nil, + _allResponseFieldsStorage: NonEncodableParameters? = nil, + _additionalParametersStorage: NonEncodableParameters? = nil + ) { + self.city = city + self.country = country + self.line1 = line1 + self.line2 = line2 + self.postalCode = postalCode + self.state = state + self._allResponseFieldsStorage = _allResponseFieldsStorage + self._additionalParametersStorage = _additionalParametersStorage + } + + /// City/District/Suburb/Town/Village. + @_spi(STP) public var city: String? + + /// Two-letter country code (ISO 3166-1 alpha-2). + @_spi(STP) public var country: String? + + /// Address line 1 (Street address/PO Box/Company name). + @_spi(STP) public var line1: String + + /// Address line 2 (Apartment/Suite/Unit/Building). + @_spi(STP) public var line2: String? + + /// ZIP or postal code. + @_spi(STP) public var postalCode: String? + + /// State/County/Province/Region. + @_spi(STP) public var state: String? + + @_spi(STP) public var _allResponseFieldsStorage: NonEncodableParameters? + @_spi(STP) public var _additionalParametersStorage: NonEncodableParameters? + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Token.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Token.swift new file mode 100644 index 00000000..c3a3bba2 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Models/Token.swift @@ -0,0 +1,130 @@ +// +// Token.swift +// StripeApplePay +// +// Created by David Estes on 7/14/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore + +extension StripeAPI { + // Internal note: @_spi(StripeApplePayTokenization) is intended for limited public use. See https://docs.google.com/document/d/1Z9bTUBvDDufoqTaQeI3A0Cxdsoj_D0IkxdWX-GB-RTQ + @_spi(StripeApplePayTokenization) public struct Token: UnknownFieldsDecodable { + public var _allResponseFieldsStorage: NonEncodableParameters? + + /// The value of the token. You can store this value on your server and use it to make charges and customers. + /// - seealso: https://stripe.com/docs/payments/charges-api + public let id: String + /// Whether or not this token was created in livemode. Will be YES if you used your Live Publishable Key, and NO if you used your Test Publishable Key. + var livemode: Bool + /// The type of this token. + var type: TokenType + + /// Possible Token types + enum TokenType: String, SafeEnumCodable { + /// Account token type + case account + /// Bank account token type + case bankAccount = "bank_account" + /// Card token type + case card + /// PII token type + case PII = "pii" + /// CVC update token type + case cvcUpdate = "cvc_update" + case unparsable + } + + /// The credit card details that were used to create the token. Will only be set if the token was created via a credit card or Apple Pay, otherwise it will be + /// nil. + var card: Card? + // /// The bank account details that were used to create the token. Will only be set if the token was created with a bank account, otherwise it will be nil. + // Not yet implemented. + // var bankAccount: BankAccount? + /// When the token was created. + var created: Date? + + struct Card: UnknownFieldsDecodable { + var _allResponseFieldsStorage: NonEncodableParameters? + + /// The last 4 digits of the card. + var last4: String + /// For cards made with Apple Pay, this refers to the last 4 digits of the + /// "Device Account Number" for the tokenized card. For regular cards, it will + /// be nil. + var dynamicLast4: String? + /// Whether or not the card originated from Apple Pay. + var isApplePayCard: Bool { + return (allResponseFields["tokenization_method"] as? String) == "apple_pay" + } + /// The card's expiration month. 1-indexed (i.e. 1 == January) + var expMonth: Int + /// The card's expiration year. + var expYear: Int + /// The cardholder's name. + var name: String? + + /// City/District/Suburb/Town/Village. + var addressCity: String? + + /// Billing address country, if provided when creating card. + var addressCountry: String? + + /// Address line 1 (Street address/PO Box/Company name). + var addressLine1: String? + + /// If address_line1 was provided, results of the check. + var addressLine1Check: AddressCheck? + + /// Results of an address check. + enum AddressCheck: String, SafeEnumCodable { + case pass + case fail + case unavailable + case unchecked + case unparsable + } + + /// Address line 2 (Apartment/Suite/Unit/Building). + var addressLine2: String? + + /// State/County/Province/Region. + var addressState: String? + + /// ZIP or postal code. + var addressZip: String? + + /// If address_zip was provided, results of the check. + var addressZipCheck: AddressCheck? + + /// The issuer of the card. + var brand: CardBrand = .unknown + + /// The funding source for the card (credit, debit, prepaid, or other) + var funding: FundingType = .unknown + + /// The various funding sources for a payment card. + enum FundingType: String, SafeEnumCodable { + /// Debit card funding + case debit + /// Credit card funding + case credit + /// Prepaid card funding + case prepaid + /// An other or unknown type of funding source. + case unknown + case unparsable + } + + /// Two-letter ISO code representing the issuing country of the card. + var country: String? + /// This is only applicable when tokenizing debit cards to issue payouts to managed + /// accounts. You should not set it otherwise. The card can then be used as a + /// transfer destination for funds in this currency. + var currency: String? + } + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentIntent+API.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentIntent+API.swift new file mode 100644 index 00000000..76c49cf6 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentIntent+API.swift @@ -0,0 +1,85 @@ +// +// PaymentIntent+API.swift +// StripeApplePay +// +// Created by David Estes on 8/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI.PaymentIntent { + /// A callback to be run with a PaymentIntent response from the Stripe API. + /// - Parameters: + /// - paymentIntent: The Stripe PaymentIntent from the response. Will be nil if an error occurs. - seealso: PaymentIntent + /// - error: The error returned from the response, or nil if none occurs. - seealso: StripeError.h for possible values. + @_spi(STP) public typealias PaymentIntentCompletionBlock = ( + Result + ) -> Void + + /// Retrieves the PaymentIntent object using the given secret. - seealso: https://stripe.com/docs/api#retrieve_payment_intent + /// - Parameters: + /// - secret: The client secret of the payment intent to be retrieved. Cannot be nil. + /// - completion: The callback to run with the returned PaymentIntent object, or an error. + @_spi(STP) public static func get( + apiClient: STPAPIClient = .shared, + clientSecret: String, + completion: @escaping PaymentIntentCompletionBlock + ) { + assert( + StripeAPI.PaymentIntentParams.isClientSecretValid(clientSecret), + "`secret` format does not match expected client secret formatting." + ) + guard let identifier = StripeAPI.PaymentIntent.id(fromClientSecret: clientSecret) else { + completion(.failure(StripeError.invalidRequest)) + return + } + let endpoint = "\(Resource)/\(identifier)" + let parameters: [String: String] = ["client_secret": clientSecret] + + apiClient.get(resource: endpoint, parameters: parameters, completion: completion) + } + + /// Confirms the PaymentIntent object with the provided params object. + /// At a minimum, the params object must include the `clientSecret`. + /// - seealso: https://stripe.com/docs/api#confirm_payment_intent + /// @note Use the `confirmPayment:withAuthenticationContext:completion:` method on `PaymentHandler` instead + /// of calling this method directly. It handles any authentication necessary for you. - seealso: https://stripe.com/docs/payments/3d-secure + /// - Parameters: + /// - paymentIntentParams: The `PaymentIntentParams` to pass to `/confirm` + /// - completion: The callback to run with the returned PaymentIntent object, or an error. + @_spi(STP) public static func confirm( + apiClient: STPAPIClient = .shared, + params: StripeAPI.PaymentIntentParams, + completion: @escaping PaymentIntentCompletionBlock + ) { + assert( + StripeAPI.PaymentIntentParams.isClientSecretValid(params.clientSecret), + "`paymentIntentParams.clientSecret` format does not match expected client secret formatting." + ) + + guard let identifier = StripeAPI.PaymentIntent.id(fromClientSecret: params.clientSecret) + else { + completion(.failure(StripeError.invalidRequest)) + return + } + let endpoint = "\(Resource)/\(identifier)/confirm" + + let type = params.paymentMethodData?.type.rawValue + STPAnalyticsClient.sharedClient.logPaymentIntentConfirmationAttempt( + paymentMethodType: type + ) + + // Add telemetry + var paramsWithTelemetry = params + if let pmAdditionalParams = paramsWithTelemetry.paymentMethodData?.additionalParameters { + paramsWithTelemetry.paymentMethodData?.additionalParameters = STPTelemetryClient.shared + .paramsByAddingTelemetryFields(toParams: pmAdditionalParams) + } + + apiClient.post(resource: endpoint, object: paramsWithTelemetry, completion: completion) + } + + static let Resource = "payment_intents" +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentMethod+API.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentMethod+API.swift new file mode 100644 index 00000000..f48e682c --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/PaymentMethod+API.swift @@ -0,0 +1,61 @@ +// +// PaymentMethod+API.swift +// StripeApplePay +// +// Created by David Estes on 8/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore + +extension StripeAPI.PaymentMethod { + /// A callback to be run with a PaymentMethod response from the Stripe API. + /// - Parameters: + /// - paymentMethod: The Stripe PaymentMethod from the response. Will be nil if an error occurs. - seealso: PaymentMethod + /// - error: The error returned from the response, or nil if none occurs. - seealso: StripeError.h for possible values. + @_spi(STP) public typealias PaymentMethodCompletionBlock = ( + Result + ) -> Void + + static func create( + apiClient: STPAPIClient = .shared, + params: StripeAPI.PaymentMethodParams, + completion: @escaping PaymentMethodCompletionBlock + ) { + STPAnalyticsClient.sharedClient.logPaymentMethodCreationAttempt( + paymentMethodType: params.type.rawValue + ) + apiClient.post(resource: Resource, object: params, completion: completion) + } + + /// Converts a PKPayment object into a Stripe Payment Method using the Stripe API. + /// - Parameters: + /// - payment: The user's encrypted payment information as returned from a PKPaymentAuthorizationController. Cannot be nil. + /// - completion: The callback to run with the returned Stripe source (and any errors that may have occurred). + @_spi(STP) public static func create( + apiClient: STPAPIClient = .shared, + payment: PKPayment, + completion: @escaping PaymentMethodCompletionBlock + ) { + StripeAPI.Token.create(apiClient: apiClient, payment: payment) { (result) in + guard let token = try? result.get() else { + if case .failure(let error) = result { + completion(.failure(error)) + } else { + completion(.failure(NSError.stp_genericConnectionError())) + } + return + } + var cardParams = StripeAPI.PaymentMethodParams.Card() + cardParams.token = token.id + let billingDetails = StripeAPI.BillingDetails(from: payment) + var paymentMethodParams = StripeAPI.PaymentMethodParams(type: .card, card: cardParams) + paymentMethodParams.billingDetails = billingDetails + Self.create(apiClient: apiClient, params: paymentMethodParams, completion: completion) + } + } + + static let Resource = "payment_methods" +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/SetupIntent+API.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/SetupIntent+API.swift new file mode 100644 index 00000000..74fbfd82 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/SetupIntent+API.swift @@ -0,0 +1,84 @@ +// +// SetupIntent+API.swift +// StripeApplePay +// +// Created by David Estes on 8/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension StripeAPI.SetupIntent { + /// A callback to be run with a SetupIntent response from the Stripe API. + /// - Parameters: + /// - setupIntent: The Stripe SetupIntent from the response. Will be nil if an error occurs. - seealso: SetupIntent + /// - error: The error returned from the response, or nil if none occurs. - seealso: StripeError.h for possible values. + @_spi(STP) public typealias SetupIntentCompletionBlock = (Result) + -> Void + + /// Retrieves the SetupIntent object using the given secret. - seealso: https://stripe.com/docs/api/setup_intents/retrieve + /// - Parameters: + /// - secret: The client secret of the SetupIntent to be retrieved. Cannot be nil. + /// - completion: The callback to run with the returned SetupIntent object, or an error. + @_spi(STP) public static func get( + apiClient: STPAPIClient = .shared, + clientSecret: String, + completion: @escaping SetupIntentCompletionBlock + ) { + assert( + StripeAPI.SetupIntentConfirmParams.isClientSecretValid(clientSecret), + "`secret` format does not match expected client secret formatting." + ) + guard let identifier = StripeAPI.SetupIntent.id(fromClientSecret: clientSecret) else { + completion(.failure(StripeError.invalidRequest)) + return + } + let endpoint = "\(Resource)/\(identifier)" + let parameters: [String: String] = ["client_secret": clientSecret] + + apiClient.get(resource: endpoint, parameters: parameters, completion: completion) + } + + /// Confirms the SetupIntent object with the provided params object. + /// At a minimum, the params object must include the `clientSecret`. + /// - seealso: https://stripe.com/docs/api/setup_intents/confirm + /// @note Use the `confirmSetupIntent:withAuthenticationContext:completion:` method on `PaymentHandler` instead + /// of calling this method directly. It handles any authentication necessary for you. - seealso: https://stripe.com/docs/payments/3d-secure + /// - Parameters: + /// - setupIntentParams: The `SetupIntentConfirmParams` to pass to `/confirm` + /// - completion: The callback to run with the returned PaymentIntent object, or an error. + @_spi(STP) public static func confirm( + apiClient: STPAPIClient = .shared, + params: StripeAPI.SetupIntentConfirmParams, + completion: @escaping SetupIntentCompletionBlock + ) { + assert( + StripeAPI.SetupIntentConfirmParams.isClientSecretValid(params.clientSecret), + "`setupIntentConfirmParams.clientSecret` format does not match expected client secret formatting." + ) + + guard let identifier = StripeAPI.SetupIntent.id(fromClientSecret: params.clientSecret) + else { + completion(.failure(StripeError.invalidRequest)) + return + } + let endpoint = "\(Resource)/\(identifier)/confirm" + + let type = params.paymentMethodData?.type.rawValue + STPAnalyticsClient.sharedClient.logSetupIntentConfirmationAttempt( + paymentMethodType: type + ) + + // Add telemetry + var paramsWithTelemetry = params + if let pmAdditionalParams = paramsWithTelemetry.paymentMethodData?.additionalParameters { + paramsWithTelemetry.paymentMethodData?.additionalParameters = STPTelemetryClient.shared + .paramsByAddingTelemetryFields(toParams: pmAdditionalParams) + } + + apiClient.post(resource: endpoint, object: paramsWithTelemetry, completion: completion) + } + + static let Resource = "setup_intents" +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Token+API.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Token+API.swift new file mode 100644 index 00000000..0622e5fe --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/API/Token+API.swift @@ -0,0 +1,92 @@ +// +// Token+API.swift +// StripeApplePay +// +// Created by David Estes on 8/10/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import PassKit +@_spi(STP) import StripeCore + +extension StripeAPI.Token { + /// A callback to be run with a token response from the Stripe API. + /// - Parameters: + /// - token: The Stripe token from the response. Will be nil if an error occurs. - seealso: STPToken + /// - error: The error returned from the response, or nil if none occurs. - seealso: StripeError.h for possible values. + @_spi(StripeApplePayTokenization) public typealias TokenCompletionBlock = (Result) -> Void + + /// Converts a PKPayment object into a Stripe token using the Stripe API. + /// - Parameters: + /// - payment: The user's encrypted payment information as returned from a PKPaymentAuthorizationController. Cannot be nil. + /// - completion: The callback to run with the returned Stripe token (and any errors that may have occurred). + @_spi(StripeApplePayTokenization) public static func create( + apiClient: STPAPIClient = .shared, + payment: PKPayment, + completion: @escaping TokenCompletionBlock + ) { + // Internal note: @_spi(StripeApplePayTokenization) is intended for limited public use. See https://docs.google.com/document/d/1Z9bTUBvDDufoqTaQeI3A0Cxdsoj_D0IkxdWX-GB-RTQ + let params = payment.stp_tokenParameters(apiClient: apiClient) + create( + apiClient: apiClient, + parameters: params, + completion: completion + ) + } + + static func create( + apiClient: STPAPIClient = .shared, + parameters: [String: Any], + completion: @escaping TokenCompletionBlock + ) { + let tokenType = STPAnalyticsClient.tokenType(fromParameters: parameters) + var mutableParams = parameters + STPTelemetryClient.shared.addTelemetryFields(toParams: &mutableParams) + mutableParams = STPAPIClient.paramsAddingPaymentUserAgent(mutableParams) + STPAnalyticsClient.sharedClient.logTokenCreationAttempt(tokenType: tokenType) + apiClient.post(resource: Resource, parameters: mutableParams, completion: completion) + STPTelemetryClient.shared.sendTelemetryData() + } + + static let Resource = "tokens" +} + +extension PKPayment { + func stp_tokenParameters(apiClient: STPAPIClient) -> [String: Any] { + let paymentString = String(data: self.token.paymentData, encoding: .utf8) + var payload: [String: Any] = [:] + payload["pk_token"] = paymentString + if let billingContact = self.billingContact { + payload["card"] = billingContact.addressParams + } + + assert( + !((paymentString?.count ?? 0) == 0 + && apiClient.publishableKey?.hasPrefix("pk_live") ?? false), + "The pk_token is empty. Using Apple Pay with an iOS Simulator while not in Stripe Test Mode will always fail." + ) + + let paymentInstrumentName = self.token.paymentMethod.displayName + if let paymentInstrumentName = paymentInstrumentName { + payload["pk_token_instrument_name"] = paymentInstrumentName + } + + let paymentNetwork = self.token.paymentMethod.network + if let paymentNetwork = paymentNetwork { + // Note: As of SDK 20.0.0, this will return `PKPaymentNetwork(_rawValue: MasterCard)`. + // We're intentionally leaving it this way: See RUN_MOBILESDK-125. + payload["pk_token_payment_network"] = paymentNetwork + } + + var transactionIdentifier = self.token.transactionIdentifier + if transactionIdentifier != "" { + if self.stp_isSimulated() { + transactionIdentifier = PKPayment.stp_testTransactionIdentifier() + } + payload["pk_token_transaction_id"] = transactionIdentifier + } + + return payload + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+Payments.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+Payments.swift new file mode 100644 index 00000000..ecd52639 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+Payments.swift @@ -0,0 +1,27 @@ +// +// STPAnalyticsClient+Payments.swift +// StripeApplePay +// +// Created by David Estes on 1/24/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +/// An analytic specific to payments that serializes payment-specific +/// information into its params. +@_spi(STP) public protocol PaymentAnalytic: Analytic { + var additionalParams: [String: Any] { get } +} + +@_spi(STP) extension PaymentAnalytic { + public var params: [String: Any] { + var params = additionalParams + + params["apple_pay_enabled"] = NSNumber(value: StripeAPI.deviceSupportsApplePay()) + params["ocr_type"] = PaymentsSDKVariant.ocrTypeString + params["pay_var"] = PaymentsSDKVariant.variant + return params + } +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+PaymentsAPI.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+PaymentsAPI.swift new file mode 100644 index 00000000..745eecb8 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Analytics/STPAnalyticsClient+PaymentsAPI.swift @@ -0,0 +1,67 @@ +// +// STPAnalyticsClient+PaymentsAPI.swift +// StripeApplePay +// +// Created by David Estes on 1/21/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension STPAnalyticsClient { + // MARK: - Log events + + func logPaymentMethodCreationAttempt(paymentMethodType: String?) { + log( + analytic: PaymentAPIAnalytic( + event: .paymentMethodCreation, + additionalParams: [ + "source_type": paymentMethodType ?? "unknown", + ] + ) + ) + } + + func logTokenCreationAttempt(tokenType: String?) { + log( + analytic: PaymentAPIAnalytic( + event: .tokenCreation, + additionalParams: [ + "token_type": tokenType ?? "unknown", + ] + ) + ) + } + + func logPaymentIntentConfirmationAttempt( + paymentMethodType: String? + ) { + log( + analytic: PaymentAPIAnalytic( + event: .paymentMethodIntentCreation, + additionalParams: [ + "source_type": paymentMethodType ?? "unknown", + ] + ) + ) + } + + func logSetupIntentConfirmationAttempt( + paymentMethodType: String? + ) { + log( + analytic: PaymentAPIAnalytic( + event: .setupIntentConfirmationAttempt, + additionalParams: [ + "source_type": paymentMethodType ?? "unknown", + ] + ) + ) + } +} + +struct PaymentAPIAnalytic: PaymentAnalytic { + let event: STPAnalyticEvent + let additionalParams: [String: Any] +} diff --git a/StripeApplePay/StripeApplePay/Source/PaymentsCore/Categories/STPAPIClient+PaymentsCore.swift b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Categories/STPAPIClient+PaymentsCore.swift new file mode 100644 index 00000000..2ed46cf8 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/PaymentsCore/Categories/STPAPIClient+PaymentsCore.swift @@ -0,0 +1,20 @@ +// +// STPAPIClient+PaymentsCore.swift +// StripeApplePay +// +// Created by David Estes on 1/25/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore + +extension STPAPIClient { + @_spi(STP) public class func paramsAddingPaymentUserAgent( + _ params: [String: Any] + ) -> [String: Any] { + var newParams = params + newParams["payment_user_agent"] = PaymentsSDKVariant.paymentUserAgent + return newParams + } +} diff --git a/StripeApplePay/StripeApplePay/Source/StripeCore+Import.swift b/StripeApplePay/StripeApplePay/Source/StripeCore+Import.swift new file mode 100644 index 00000000..123a8e98 --- /dev/null +++ b/StripeApplePay/StripeApplePay/Source/StripeCore+Import.swift @@ -0,0 +1,10 @@ +// +// StripeCore+Import.swift +// StripeApplePay +// +// Created by David Estes on 11/15/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_exported import StripeCore diff --git a/StripeApplePay/StripeApplePay/StripeApplePay.h b/StripeApplePay/StripeApplePay/StripeApplePay.h new file mode 100644 index 00000000..e6b9e0f8 --- /dev/null +++ b/StripeApplePay/StripeApplePay/StripeApplePay.h @@ -0,0 +1,18 @@ +// +// StripeApplePay.h +// StripeApplePay +// +// Created by David Estes on 11/8/21. +// + +#import + +//! Project version number for StripeApplePay. +FOUNDATION_EXPORT double StripeApplePayVersionNumber; + +//! Project version string for StripeApplePay. +FOUNDATION_EXPORT const unsigned char StripeApplePayVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/StripeApplePay/StripeApplePayTests/Info.plist b/StripeApplePay/StripeApplePayTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientFunctionalTest.swift b/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientFunctionalTest.swift new file mode 100644 index 00000000..9f70bfb5 --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientFunctionalTest.swift @@ -0,0 +1,67 @@ +// +// STPTelemetryClientFunctionalTest.swift +// StripeApplePayTests +// +// Created by Yuki Tokuhiro on 5/21/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest + +// swift-format-ignore +@testable @_spi(STP) import StripeApplePay + +// swift-format-ignore +@testable @_spi(STP) import StripeCore + +class STPTelemetryClientFunctionalTest: XCTestCase { + func testSendFraudDetectionData() { + // Sending telemetry without any FraudDetectionData... + FraudDetectionData.shared.sid = nil + FraudDetectionData.shared.sidCreationDate = nil + FraudDetectionData.shared.muid = nil + FraudDetectionData.shared.guid = nil + let sendTelemetry1 = expectation(description: "") + STPTelemetryClient.shared.sendTelemetryData(forceSend: true) { _ in + sendTelemetry1.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + // ...populates FraudDetectionData + let sid = FraudDetectionData.shared.sid + let muid = FraudDetectionData.shared.muid + let guid = FraudDetectionData.shared.guid + XCTAssertNotNil(sid) + XCTAssertNotNil(muid) + XCTAssertNotNil(guid) + + let sendTelemetry2 = expectation(description: "") + // Sending telemetry again... + STPTelemetryClient.shared.sendTelemetryData(forceSend: true) { _ in + sendTelemetry2.fulfill() + } + // ...gives the same FraudDetectionData + XCTAssertEqual(FraudDetectionData.shared.sid, sid) + XCTAssertEqual(FraudDetectionData.shared.muid, muid) + XCTAssertEqual(FraudDetectionData.shared.guid, guid) + guard let sidCreationDate = FraudDetectionData.shared.sidCreationDate else { + XCTFail() + return + } + // sanity check creation date looks right + XCTAssertTrue(sidCreationDate > Date(timeIntervalSinceNow: -10)) + waitForExpectations(timeout: 10, handler: nil) + + // Expiring the FraudDetectionData + FraudDetectionData.shared.sidCreationDate = Date(timeIntervalSinceNow: -999999) + let sendTelemetry3 = expectation(description: "") + // ...and sending telemetry again + STPTelemetryClient.shared.sendTelemetryData(forceSend: true) { _ in + sendTelemetry3.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + // ...gives the same muid and guid but different sid + XCTAssertEqual(FraudDetectionData.shared.muid, muid) + XCTAssertEqual(FraudDetectionData.shared.guid, guid) + XCTAssertNotEqual(FraudDetectionData.shared.sid, sid) + } +} diff --git a/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientTest.swift b/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientTest.swift new file mode 100644 index 00000000..8a730008 --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/PaymentsCore/STPTelemetryClientTest.swift @@ -0,0 +1,75 @@ +// +// STPTelemetryClientTest.swift +// StripeApplePayTests +// +// Created by Yuki Tokuhiro on 9/24/20. +// Copyright © 2020 Stripe, Inc. All rights reserved. +// + +import XCTest + +// swift-format-ignore +@testable @_spi(STP) import StripeApplePay + +// swift-format-ignore +@testable @_spi(STP) import StripeCore + +class STPTelemetryClientTest: XCTestCase { + + func testAddTelemetryData() { + let sut = STPTelemetryClient.shared + var params: [String: Any] = [ + "foo": "bar" + ] + let exp = expectation(description: "delay") + DispatchQueue.main.asyncAfter( + deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) + / Double(NSEC_PER_SEC), + execute: { + sut.addTelemetryFields(toParams: ¶ms) + XCTAssertNotNil(params) + exp.fulfill() + } + ) + waitForExpectations(timeout: 2, handler: nil) + } + + func testAdvancedFraudSignalsSwitch() { + XCTAssertTrue(StripeAPI.advancedFraudSignalsEnabled) + StripeAPI.advancedFraudSignalsEnabled = false + XCTAssertFalse(StripeAPI.advancedFraudSignalsEnabled) + } + + func testAddTelemetryFieldsWhenFraudDetectionDataEmpty() { + // Should not add any fields if fraudDetectionData is empty + FraudDetectionData.shared.reset() + var params: [String: Any] = [:] + STPTelemetryClient.shared.addTelemetryFields(toParams: ¶ms) + XCTAssertTrue(params.isEmpty) + } + + func testAddTelemetryFieldsWhenSIDExpired() { + // Should add muid, but not add sid if it's expired + var params: [String: Any] = [:] + FraudDetectionData.shared.sid = "expired" + FraudDetectionData.shared.sidCreationDate = Date(timeInterval: -30 * 60, since: Date()) + FraudDetectionData.shared.muid = "muid value" + FraudDetectionData.shared.guid = "guid value" + STPTelemetryClient.shared.addTelemetryFields(toParams: ¶ms) + XCTAssertEqual(params["muid"] as? String, "muid value") + XCTAssertEqual(params["guid"] as? String, "guid value") + XCTAssertNil(params["sid"] as? String) + } + + func testAddTelemetryFields() { + var params: [String: Any] = [:] + FraudDetectionData.shared.sid = "sid value" + FraudDetectionData.shared.muid = "muid value" + FraudDetectionData.shared.guid = "guid value" + FraudDetectionData.shared.sidCreationDate = Date() + STPTelemetryClient.shared.addTelemetryFields(toParams: ¶ms) + XCTAssertEqual(params["muid"] as? String, "muid value") + XCTAssertEqual(params["sid"] as? String, "sid value") + XCTAssertEqual(params["guid"] as? String, "guid value") + } +} diff --git a/StripeApplePay/StripeApplePayTests/STPAnalyticsClient+ApplePayTest.swift b/StripeApplePay/StripeApplePayTests/STPAnalyticsClient+ApplePayTest.swift new file mode 100644 index 00000000..bed3eb7e --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/STPAnalyticsClient+ApplePayTest.swift @@ -0,0 +1,29 @@ +// +// STPAnalyticsClient+ApplePayTest.swift +// StripeApplePayTests +// +// Created by David Estes on 2/3/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import Foundation +import XCTest + +// swift-format-ignore +@_spi(STP) @testable import StripeApplePay + +// swift-format-ignore +@_spi(STP) @testable import StripeCore + +class STPAnalyticsClientApplePayTest: XCTestCase { + func testApplePaySDKVariantPayload() throws { + // setup + let analytic = PaymentAPIAnalytic( + event: .paymentMethodCreation, + additionalParams: [:] + ) + let client = STPAnalyticsClient() + let payload = client.payload(from: analytic) + XCTAssertEqual("applepay", payload["pay_var"] as? String) + } +} diff --git a/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift b/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift new file mode 100644 index 00000000..d309d66f --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift @@ -0,0 +1,97 @@ +// +// STPPaymentMethodFunctionalTest.swift +// StripeApplePayTests +// +// Created by David Estes on 8/9/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +@_spi(STP) import StripeCore // for StripeError +import StripeCoreTestUtils +import XCTest + +// swift-format-ignore +@_spi(STP) @testable import StripeApplePay + +let STPTestingDefaultPublishableKey = "pk_test_ErsyMEOTudSjQR8hh0VrQr5X008sBXGOu6" +public let STPTestingNetworkRequestTimeout: TimeInterval = 8 + +class STPPaymentMethodModernTest: XCTestCase { + func testCreateCardPaymentMethod() { + let expectation = self.expectation(description: "Created") + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + var params = StripeAPI.PaymentMethodParams(type: .card) + var card = StripeAPI.PaymentMethodParams.Card() + card.number = "4242424242424242" + card.expYear = 28 + card.expMonth = 12 + card.cvc = "100" + var billingAddress = StripeAPI.BillingDetails.Address() + billingAddress.city = "San Francisco" + billingAddress.country = "US" + billingAddress.line1 = "150 Townsend St" + billingAddress.line2 = "4th Floor" + billingAddress.postalCode = "94103" + billingAddress.state = "CA" + + var billingDetails = StripeAPI.BillingDetails() + billingDetails.address = billingAddress + billingDetails.email = "email@email.com" + billingDetails.name = "Isaac Asimov" + billingDetails.phone = "555-555-5555" + + params.card = card + params.billingDetails = billingDetails + + StripeAPI.PaymentMethod.create(apiClient: apiClient, params: params) { result in + let paymentMethod = try! result.get() + XCTAssertEqual(paymentMethod.card?.last4, "4242") + expectation.fulfill() + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } + + func testCreateCardPaymentMethodWithAdditionalAPIStuff() { + let expectation = self.expectation(description: "Created") + let apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey) + var params = StripeAPI.PaymentMethodParams(type: .card) + var card = StripeAPI.PaymentMethodParams.Card() + card.number = "4242424242424242" + card.expYear = 28 + card.expMonth = 12 + card.cvc = "100" + var billingAddress = StripeAPI.BillingDetails.Address() + billingAddress.city = "San Francisco" + billingAddress.country = "US" + billingAddress.line1 = "150 Townsend St" + billingAddress.line2 = "4th Floor" + billingAddress.postalCode = "94103" + billingAddress.state = "CA" + billingAddress.additionalParameters = ["invalid_thing": "yes"] + + var billingDetails = StripeAPI.BillingDetails() + billingDetails.address = billingAddress + billingDetails.email = "email@email.com" + billingDetails.name = "Isaac Asimov" + billingDetails.phone = "555-555-5555" + + params.card = card + params.billingDetails = billingDetails + + StripeAPI.PaymentMethod.create(apiClient: apiClient, params: params) { result in + do { + _ = try result.get() + XCTFail("This request should fail") + } catch { + let stripeError = error as? StripeError + if case .apiError(let apiError) = stripeError { + XCTAssertEqual(apiError.code, "parameter_unknown") + XCTAssertEqual(apiError.param, "billing_details[address][invalid_thing]") + expectation.fulfill() + } + } + } + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift b/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift new file mode 100644 index 00000000..b337a440 --- /dev/null +++ b/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift @@ -0,0 +1,95 @@ +// +// TelemetryInjectionTest.swift +// StripeApplePayTests +// +// Created by David Estes on 2/9/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import AVFoundation +import Foundation +import OHHTTPStubs +import OHHTTPStubsSwift +import StripeCoreTestUtils +import XCTest + +// swift-format-ignore +@_spi(STP) @testable import StripeApplePay + +// swift-format-ignore +@_spi(STP) @testable import StripeCore + +class TelemetryInjectionTest: APIStubbedTestCase { + func testIntentConfirmAddsTelemetry() { + let apiClient = stubbedAPIClient() + + let piTelemetryExpectation = self.expectation(description: "saw pi telemetry") + let siTelemetryExpectation = self.expectation(description: "saw si telemetry") + + // As an implementation detail, OHHTTPStubs will run this block in `canInitWithRequest` in addition to + // `initWithRequest`. So it could be called more times than we expect. + // We don't have control over this behavior (CFNetwork drives it), so let's not worry + // about overfulfillment. + piTelemetryExpectation.assertForOverFulfill = false + siTelemetryExpectation.assertForOverFulfill = false + + stub { urlRequest in + if urlRequest.url!.absoluteString.contains("_intent") { + let ua = urlRequest.queryItems!.first(where: { + $0.name == "payment_method_data[payment_user_agent]" + })!.value! + XCTAssertTrue(ua.hasPrefix("stripe-ios/")) + let muid = urlRequest.queryItems!.first(where: { + $0.name == "payment_method_data[muid]" + })!.value! + let guid = urlRequest.queryItems!.first(where: { + $0.name == "payment_method_data[guid]" + })!.value! + XCTAssertNotNil(muid) + XCTAssertNotNil(guid) + if urlRequest.url!.absoluteString.contains("payment_intent") { + piTelemetryExpectation.fulfill() + } + if urlRequest.url!.absoluteString.contains("setup_intent") { + siTelemetryExpectation.fulfill() + } + return true + } + return false + } response: { _ in + // We don't care about the response + return HTTPStubsResponse() + } + + var params = StripeAPI.PaymentMethodParams(type: .card) + var card = StripeAPI.PaymentMethodParams.Card() + card.number = "4242424242424242" + card.expYear = 28 + card.expMonth = 12 + card.cvc = "100" + params.card = card + + // Set up telemetry data + StripeAPI.advancedFraudSignalsEnabled = true + FraudDetectionData.shared.sid = "sid" + FraudDetectionData.shared.muid = "muid" + FraudDetectionData.shared.guid = "guid" + FraudDetectionData.shared.sidCreationDate = Date() + + let piExpectation = self.expectation(description: "PI Confirmed") + var pip = StripeAPI.PaymentIntentParams(clientSecret: "pi_123_secret_abc") + pip.paymentMethodData = params + StripeAPI.PaymentIntent.confirm(apiClient: apiClient, params: pip) { _ in + piExpectation.fulfill() + } + + let siExpectation = self.expectation(description: "SI Confirmed") + var sip = StripeAPI.SetupIntentConfirmParams(clientSecret: "seti_123_secret_abc") + sip.paymentMethodData = params + StripeAPI.SetupIntent.confirm(apiClient: apiClient, params: sip) { _ in + siExpectation.fulfill() + } + + waitForExpectations(timeout: STPTestingNetworkRequestTimeout, handler: nil) + } +} diff --git a/StripeCameraCore/StripeCameraCore.xcodeproj/project.pbxproj b/StripeCameraCore/StripeCameraCore.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ccfeffb1 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore.xcodeproj/project.pbxproj @@ -0,0 +1,640 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 0485C4A62D9444E75877E170 /* CameraExifMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76A4D11F53602B77F9F5B2E /* CameraExifMetadata.swift */; }; + 064567ECD2B71E5EB7D3A201 /* MockCameraPermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832A750C892D9D3FC54D9CF3 /* MockCameraPermissionsManager.swift */; }; + 06FEEA450541E2D36D5A10FC /* CameraPermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0F925920743B648B04C949 /* CameraPermissionsManager.swift */; }; + 0F02BB3F9F7936D7BB593226 /* Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750C5508AB271C099351E42 /* Torch.swift */; }; + 10BCE429AA79F7F0D1A9F2D3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7158E127DC7294CDABDEF5B8 /* XCTest.framework */; }; + 1EF9F2AB3E43769AB2BA6D04 /* StripeCameraCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2720B7BD13954418A8AC52E5 /* StripeCameraCoreTestUtils.framework */; }; + 28F23B4322876ED2BAC8C933 /* StripeCameraCoreTestUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DDD1039F321BAF147F162F7D /* StripeCameraCoreTestUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2A1AAD8E5A589E4E74B02A35 /* MockSimulatorCameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCE205F4CCF2D486967BA25 /* MockSimulatorCameraSession.swift */; }; + 2CEF8A27C8864A6198C69A5D /* CVPixelBuffer+StripeCameraCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0B9FDC8622F6CDF9F8A4B4 /* CVPixelBuffer+StripeCameraCore.swift */; }; + 48B09ECBED820F3097779208 /* CGRect+StripeCameraCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4106DF455718394E209C00 /* CGRect+StripeCameraCore.swift */; }; + 5F57E0BF10039F7055013066 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7158E127DC7294CDABDEF5B8 /* XCTest.framework */; }; + 6D44C32189C0CB409C9E4712 /* StripeCameraCore.h in Headers */ = {isa = PBXBuildFile; fileRef = E7FB32269B8E36C3CBDB370F /* StripeCameraCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 88A1AF2B173B4055D8F44A8C /* MockAppSettingsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F04D387EEF3A5D07ECD1F7E /* MockAppSettingsHelper.swift */; }; + 8F3073987F30B9B8DDF0D956 /* MockTestCameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8091DB42296B7EC745567AF7 /* MockTestCameraSession.swift */; }; + 9AFBF50F47562CB34903064E /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E7BCD0896ADAFFF5CCA738E /* StripeCore.framework */; }; + 9CE6EE572439AD5B22B77294 /* CameraPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79D46F44C7BC55B9B595AEF /* CameraPreviewView.swift */; }; + 9FC42D068D1719AB28C3068F /* AppSettingsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DFFB1E2F90176D156E315B /* AppSettingsHelper.swift */; }; + A95255494DF27A7CAE9A70CE /* CameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD8FEBAE0069BE88B75B0C5 /* CameraSession.swift */; }; + AB24784F49EB99E7C36509BD /* CGRect_StripeCameraCoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5B6BCFF035A9E214EA9EA0 /* CGRect_StripeCameraCoreTest.swift */; }; + D2186D6E491660D0C2A9D577 /* StripeCameraCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DBC37790A9F68168A89095B /* StripeCameraCore.framework */; }; + D9687FAD52D1896F28309580 /* UIImage+Buffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA969E606F5A3829BC9D4445 /* UIImage+Buffer.swift */; }; + E01A054861E5D82D18C7D224 /* StripeCameraCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DBC37790A9F68168A89095B /* StripeCameraCore.framework */; }; + F181EEBCE3D2CAD043DAF0F0 /* UIDeviceOrientation+StripeCameraCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D9BDABE163D20DF20F2237D /* UIDeviceOrientation+StripeCameraCore.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0321B7B71CC9BDDCF236B375 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B93DF1E4460962AFB28CF144 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B50782C89D54809DFFCF80D0; + remoteInfo = StripeCameraCore; + }; + 55BD94DFEA7738F26C23A545 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B93DF1E4460962AFB28CF144 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6A948E338F0BF55DBFD83170; + remoteInfo = StripeCameraCoreTestUtils; + }; + BA0100EB31091CFA64BF12A6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B93DF1E4460962AFB28CF144 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B50782C89D54809DFFCF80D0; + remoteInfo = StripeCameraCore; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2F2CDDE07BFFE88706C6A908 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + BCA0A45A41520FB95F4E8D10 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + F97E2A90DA9F982FC293A4A9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A0B9FDC8622F6CDF9F8A4B4 /* CVPixelBuffer+StripeCameraCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CVPixelBuffer+StripeCameraCore.swift"; sourceTree = ""; }; + 0F4174046507759A9668F4EE /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + 17E9A614AF4543DB62E7F5B4 /* StripeiOS Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Release.xcconfig"; sourceTree = ""; }; + 226FED2248EE3F96F217BEE9 /* StripeCameraCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeCameraCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2720B7BD13954418A8AC52E5 /* StripeCameraCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCameraCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B5B6BCFF035A9E214EA9EA0 /* CGRect_StripeCameraCoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRect_StripeCameraCoreTest.swift; sourceTree = ""; }; + 3B4106DF455718394E209C00 /* CGRect+StripeCameraCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+StripeCameraCore.swift"; sourceTree = ""; }; + 3D0F925920743B648B04C949 /* CameraPermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPermissionsManager.swift; sourceTree = ""; }; + 44E9A4900C4B8D08A617488C /* StripeiOS Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Debug.xcconfig"; sourceTree = ""; }; + 48DFFB1E2F90176D156E315B /* AppSettingsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsHelper.swift; sourceTree = ""; }; + 4E0D85BEF8C269A46D5ECEAC /* StripeiOS-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Release.xcconfig"; sourceTree = ""; }; + 5DFD03E78034CFB59E244199 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 609D9ADB9E9898694A2A67FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 6DBC37790A9F68168A89095B /* StripeCameraCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCameraCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7158E127DC7294CDABDEF5B8 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 7D9BDABE163D20DF20F2237D /* UIDeviceOrientation+StripeCameraCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDeviceOrientation+StripeCameraCore.swift"; sourceTree = ""; }; + 8091DB42296B7EC745567AF7 /* MockTestCameraSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTestCameraSession.swift; sourceTree = ""; }; + 832A750C892D9D3FC54D9CF3 /* MockCameraPermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCameraPermissionsManager.swift; sourceTree = ""; }; + 83596C8ED3829B17A93490A6 /* StripeiOS-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Debug.xcconfig"; sourceTree = ""; }; + 8E7BCD0896ADAFFF5CCA738E /* StripeCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F04D387EEF3A5D07ECD1F7E /* MockAppSettingsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppSettingsHelper.swift; sourceTree = ""; }; + A79D46F44C7BC55B9B595AEF /* CameraPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewView.swift; sourceTree = ""; }; + ABCCADF0E7337301A472E75C /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + B8D39F3EE77B9DF5FFBA4CAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + BA969E606F5A3829BC9D4445 /* UIImage+Buffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Buffer.swift"; sourceTree = ""; }; + BCCE205F4CCF2D486967BA25 /* MockSimulatorCameraSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSimulatorCameraSession.swift; sourceTree = ""; }; + D750C5508AB271C099351E42 /* Torch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Torch.swift; sourceTree = ""; }; + D76A4D11F53602B77F9F5B2E /* CameraExifMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraExifMetadata.swift; sourceTree = ""; }; + DDD1039F321BAF147F162F7D /* StripeCameraCoreTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeCameraCoreTestUtils.h; sourceTree = ""; }; + E7FB32269B8E36C3CBDB370F /* StripeCameraCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeCameraCore.h; sourceTree = ""; }; + FBD8FEBAE0069BE88B75B0C5 /* CameraSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSession.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 267776DB1189FACBD4FA2AEA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F57E0BF10039F7055013066 /* XCTest.framework in Frameworks */, + E01A054861E5D82D18C7D224 /* StripeCameraCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9CC1AF0A3093D51C5085D503 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 10BCE429AA79F7F0D1A9F2D3 /* XCTest.framework in Frameworks */, + D2186D6E491660D0C2A9D577 /* StripeCameraCore.framework in Frameworks */, + 1EF9F2AB3E43769AB2BA6D04 /* StripeCameraCoreTestUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E3A6C5C1A5D47CEFAFD1853F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9AFBF50F47562CB34903064E /* StripeCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00F9BB709FA21D59419FA5AE /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + ABCCADF0E7337301A472E75C /* Project-Debug.xcconfig */, + 0F4174046507759A9668F4EE /* Project-Release.xcconfig */, + 44E9A4900C4B8D08A617488C /* StripeiOS Tests-Debug.xcconfig */, + 17E9A614AF4543DB62E7F5B4 /* StripeiOS Tests-Release.xcconfig */, + 83596C8ED3829B17A93490A6 /* StripeiOS-Debug.xcconfig */, + 4E0D85BEF8C269A46D5ECEAC /* StripeiOS-Release.xcconfig */, + ); + name = BuildConfigurations; + path = ../BuildConfigurations; + sourceTree = ""; + }; + 03256F1FDB473FE93A57A38A /* Source */ = { + isa = PBXGroup; + children = ( + 170C84B7FB58FAA15283903E /* Categories */, + 7E719691677D20AEAEF0C120 /* Coordinators */, + 72FEB5C092565B365BBC3906 /* Views */, + D76A4D11F53602B77F9F5B2E /* CameraExifMetadata.swift */, + ); + path = Source; + sourceTree = ""; + }; + 0FB9D3D4625E1AEA6EE1CC22 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7158E127DC7294CDABDEF5B8 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 170C84B7FB58FAA15283903E /* Categories */ = { + isa = PBXGroup; + children = ( + 3B4106DF455718394E209C00 /* CGRect+StripeCameraCore.swift */, + 0A0B9FDC8622F6CDF9F8A4B4 /* CVPixelBuffer+StripeCameraCore.swift */, + 7D9BDABE163D20DF20F2237D /* UIDeviceOrientation+StripeCameraCore.swift */, + BA969E606F5A3829BC9D4445 /* UIImage+Buffer.swift */, + ); + path = Categories; + sourceTree = ""; + }; + 171BE48EE95E66F94D6CCC33 /* StripeCameraCoreTests */ = { + isa = PBXGroup; + children = ( + CC0771AB4883EEDAB5AA7D1C /* Unit */, + 5DFD03E78034CFB59E244199 /* Info.plist */, + ); + path = StripeCameraCoreTests; + sourceTree = ""; + }; + 18A8A17329DE3C8891103ECD /* Categories */ = { + isa = PBXGroup; + children = ( + 2B5B6BCFF035A9E214EA9EA0 /* CGRect_StripeCameraCoreTest.swift */, + ); + path = Categories; + sourceTree = ""; + }; + 27E56A8242FD577334930F5B /* StripeCameraCoreTestUtils */ = { + isa = PBXGroup; + children = ( + BCB41D804D8A44F6DEEF4B8E /* Mocks */, + B8D39F3EE77B9DF5FFBA4CAE /* Info.plist */, + DDD1039F321BAF147F162F7D /* StripeCameraCoreTestUtils.h */, + ); + path = StripeCameraCoreTestUtils; + sourceTree = ""; + }; + 5CE9B5F8E73CBC18E22AF375 /* Project */ = { + isa = PBXGroup; + children = ( + 00F9BB709FA21D59419FA5AE /* BuildConfigurations */, + E2CD33A048BB3FD49B4D47FD /* StripeCameraCore */, + 171BE48EE95E66F94D6CCC33 /* StripeCameraCoreTests */, + 27E56A8242FD577334930F5B /* StripeCameraCoreTestUtils */, + ); + name = Project; + sourceTree = ""; + }; + 72FEB5C092565B365BBC3906 /* Views */ = { + isa = PBXGroup; + children = ( + A79D46F44C7BC55B9B595AEF /* CameraPreviewView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 7E719691677D20AEAEF0C120 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 48DFFB1E2F90176D156E315B /* AppSettingsHelper.swift */, + 3D0F925920743B648B04C949 /* CameraPermissionsManager.swift */, + FBD8FEBAE0069BE88B75B0C5 /* CameraSession.swift */, + BCCE205F4CCF2D486967BA25 /* MockSimulatorCameraSession.swift */, + D750C5508AB271C099351E42 /* Torch.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; + BCB41D804D8A44F6DEEF4B8E /* Mocks */ = { + isa = PBXGroup; + children = ( + 9F04D387EEF3A5D07ECD1F7E /* MockAppSettingsHelper.swift */, + 832A750C892D9D3FC54D9CF3 /* MockCameraPermissionsManager.swift */, + 8091DB42296B7EC745567AF7 /* MockTestCameraSession.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + CC0771AB4883EEDAB5AA7D1C /* Unit */ = { + isa = PBXGroup; + children = ( + 18A8A17329DE3C8891103ECD /* Categories */, + ); + path = Unit; + sourceTree = ""; + }; + E2B3561EEB5629A212FD2AB4 /* Products */ = { + isa = PBXGroup; + children = ( + 6DBC37790A9F68168A89095B /* StripeCameraCore.framework */, + 226FED2248EE3F96F217BEE9 /* StripeCameraCoreTests.xctest */, + 2720B7BD13954418A8AC52E5 /* StripeCameraCoreTestUtils.framework */, + 8E7BCD0896ADAFFF5CCA738E /* StripeCore.framework */, + ); + name = Products; + sourceTree = ""; + }; + E2CD33A048BB3FD49B4D47FD /* StripeCameraCore */ = { + isa = PBXGroup; + children = ( + 03256F1FDB473FE93A57A38A /* Source */, + 609D9ADB9E9898694A2A67FF /* Info.plist */, + E7FB32269B8E36C3CBDB370F /* StripeCameraCore.h */, + ); + path = StripeCameraCore; + sourceTree = ""; + }; + F72F2AFE9BE5E2CCBE7C6436 = { + isa = PBXGroup; + children = ( + 5CE9B5F8E73CBC18E22AF375 /* Project */, + 0FB9D3D4625E1AEA6EE1CC22 /* Frameworks */, + E2B3561EEB5629A212FD2AB4 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 21D2298D6D8328B46AB437BB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D44C32189C0CB409C9E4712 /* StripeCameraCore.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8FED65A79644B7F1CB19619B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 28F23B4322876ED2BAC8C933 /* StripeCameraCoreTestUtils.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 6A948E338F0BF55DBFD83170 /* StripeCameraCoreTestUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = 12D1B5D975CF0E6EC4956438 /* Build configuration list for PBXNativeTarget "StripeCameraCoreTestUtils" */; + buildPhases = ( + 8FED65A79644B7F1CB19619B /* Headers */, + F2FB0D7ACB63AFA0A81A5FF6 /* Sources */, + 83C726705BB615A9940B98CC /* Resources */, + BCA0A45A41520FB95F4E8D10 /* Embed Frameworks */, + 267776DB1189FACBD4FA2AEA /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D1513DE143A5DDFAB1AA4069 /* PBXTargetDependency */, + ); + name = StripeCameraCoreTestUtils; + productName = StripeCameraCoreTestUtils; + productReference = 2720B7BD13954418A8AC52E5 /* StripeCameraCoreTestUtils.framework */; + productType = "com.apple.product-type.framework"; + }; + B50782C89D54809DFFCF80D0 /* StripeCameraCore */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7056F053793C453C8750F440 /* Build configuration list for PBXNativeTarget "StripeCameraCore" */; + buildPhases = ( + 21D2298D6D8328B46AB437BB /* Headers */, + 62DE5865F3F7B8CB25F62395 /* Sources */, + 445990B4DBEECCD2FBA8B324 /* Resources */, + 2F2CDDE07BFFE88706C6A908 /* Embed Frameworks */, + E3A6C5C1A5D47CEFAFD1853F /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StripeCameraCore; + productName = StripeCameraCore; + productReference = 6DBC37790A9F68168A89095B /* StripeCameraCore.framework */; + productType = "com.apple.product-type.framework"; + }; + F3DED9AB60FDAB786A737384 /* StripeCameraCoreTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6BB26614346BD154B399D073 /* Build configuration list for PBXNativeTarget "StripeCameraCoreTests" */; + buildPhases = ( + 85B8B0A9CAD01C91AD19797A /* Sources */, + 98362E27FEF7FBD502400444 /* Resources */, + F97E2A90DA9F982FC293A4A9 /* Embed Frameworks */, + 9CC1AF0A3093D51C5085D503 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + F102799B90F2BBF57EBDBBDC /* PBXTargetDependency */, + 24ECB0864CAE52D6ABF4503E /* PBXTargetDependency */, + ); + name = StripeCameraCoreTests; + productName = StripeCameraCoreTests; + productReference = 226FED2248EE3F96F217BEE9 /* StripeCameraCoreTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B93DF1E4460962AFB28CF144 /* Project object */ = { + isa = PBXProject; + attributes = { + TargetAttributes = { + }; + }; + buildConfigurationList = C1216BCDB94950EBFA541D74 /* Build configuration list for PBXProject "StripeCameraCore" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = F72F2AFE9BE5E2CCBE7C6436; + productRefGroup = E2B3561EEB5629A212FD2AB4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B50782C89D54809DFFCF80D0 /* StripeCameraCore */, + 6A948E338F0BF55DBFD83170 /* StripeCameraCoreTestUtils */, + F3DED9AB60FDAB786A737384 /* StripeCameraCoreTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 445990B4DBEECCD2FBA8B324 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 83C726705BB615A9940B98CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 98362E27FEF7FBD502400444 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 62DE5865F3F7B8CB25F62395 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0485C4A62D9444E75877E170 /* CameraExifMetadata.swift in Sources */, + 48B09ECBED820F3097779208 /* CGRect+StripeCameraCore.swift in Sources */, + 2CEF8A27C8864A6198C69A5D /* CVPixelBuffer+StripeCameraCore.swift in Sources */, + F181EEBCE3D2CAD043DAF0F0 /* UIDeviceOrientation+StripeCameraCore.swift in Sources */, + D9687FAD52D1896F28309580 /* UIImage+Buffer.swift in Sources */, + 9FC42D068D1719AB28C3068F /* AppSettingsHelper.swift in Sources */, + 06FEEA450541E2D36D5A10FC /* CameraPermissionsManager.swift in Sources */, + A95255494DF27A7CAE9A70CE /* CameraSession.swift in Sources */, + 2A1AAD8E5A589E4E74B02A35 /* MockSimulatorCameraSession.swift in Sources */, + 0F02BB3F9F7936D7BB593226 /* Torch.swift in Sources */, + 9CE6EE572439AD5B22B77294 /* CameraPreviewView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 85B8B0A9CAD01C91AD19797A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AB24784F49EB99E7C36509BD /* CGRect_StripeCameraCoreTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F2FB0D7ACB63AFA0A81A5FF6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 88A1AF2B173B4055D8F44A8C /* MockAppSettingsHelper.swift in Sources */, + 064567ECD2B71E5EB7D3A201 /* MockCameraPermissionsManager.swift in Sources */, + 8F3073987F30B9B8DDF0D956 /* MockTestCameraSession.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 24ECB0864CAE52D6ABF4503E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeCameraCoreTestUtils; + target = 6A948E338F0BF55DBFD83170 /* StripeCameraCoreTestUtils */; + targetProxy = 55BD94DFEA7738F26C23A545 /* PBXContainerItemProxy */; + }; + D1513DE143A5DDFAB1AA4069 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeCameraCore; + target = B50782C89D54809DFFCF80D0 /* StripeCameraCore */; + targetProxy = 0321B7B71CC9BDDCF236B375 /* PBXContainerItemProxy */; + }; + F102799B90F2BBF57EBDBBDC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeCameraCore; + target = B50782C89D54809DFFCF80D0 /* StripeCameraCore */; + targetProxy = BA0100EB31091CFA64BF12A6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 1555A494851A82726CF41684 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 17E9A614AF4543DB62E7F5B4 /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCameraCoreTestUtils/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCameraCoreTestUtils; + PRODUCT_NAME = StripeCameraCoreTestUtils; + SDKROOT = iphoneos; + }; + name = Release; + }; + 8101CFE889F8AC21AF12FC99 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 83596C8ED3829B17A93490A6 /* StripeiOS-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeCameraCore/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-camera-core"; + PRODUCT_NAME = StripeCameraCore; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 85DD0786DE8D23550B2113B7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = ABCCADF0E7337301A472E75C /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + A6D92FFDFE11DCBEC32190D4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 44E9A4900C4B8D08A617488C /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCameraCoreTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCameraCoreTests; + PRODUCT_NAME = StripeCameraCoreTests; + SDKROOT = iphoneos; + }; + name = Debug; + }; + B26CBFD3B42072A398AE6605 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 17E9A614AF4543DB62E7F5B4 /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCameraCoreTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCameraCoreTests; + PRODUCT_NAME = StripeCameraCoreTests; + SDKROOT = iphoneos; + }; + name = Release; + }; + E1A07E117DC9E8051D871578 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E0D85BEF8C269A46D5ECEAC /* StripeiOS-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeCameraCore/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-camera-core"; + PRODUCT_NAME = StripeCameraCore; + SDKROOT = iphoneos; + }; + name = Release; + }; + F11D6A5494AA72875ECC53A4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0F4174046507759A9668F4EE /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + FD80773E1582BF61724D6EDE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 44E9A4900C4B8D08A617488C /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCameraCoreTestUtils/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCameraCoreTestUtils; + PRODUCT_NAME = StripeCameraCoreTestUtils; + SDKROOT = iphoneos; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 12D1B5D975CF0E6EC4956438 /* Build configuration list for PBXNativeTarget "StripeCameraCoreTestUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FD80773E1582BF61724D6EDE /* Debug */, + 1555A494851A82726CF41684 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6BB26614346BD154B399D073 /* Build configuration list for PBXNativeTarget "StripeCameraCoreTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A6D92FFDFE11DCBEC32190D4 /* Debug */, + B26CBFD3B42072A398AE6605 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7056F053793C453C8750F440 /* Build configuration list for PBXNativeTarget "StripeCameraCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8101CFE889F8AC21AF12FC99 /* Debug */, + E1A07E117DC9E8051D871578 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C1216BCDB94950EBFA541D74 /* Build configuration list for PBXProject "StripeCameraCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 85DD0786DE8D23550B2113B7 /* Debug */, + F11D6A5494AA72875ECC53A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B93DF1E4460962AFB28CF144 /* Project object */; +} diff --git a/StripeCameraCore/StripeCameraCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/StripeCameraCore/StripeCameraCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/StripeCameraCore/StripeCameraCore.xcodeproj/xcshareddata/xcschemes/StripeCameraCore.xcscheme b/StripeCameraCore/StripeCameraCore.xcodeproj/xcshareddata/xcschemes/StripeCameraCore.xcscheme new file mode 100644 index 00000000..d616e1ac --- /dev/null +++ b/StripeCameraCore/StripeCameraCore.xcodeproj/xcshareddata/xcschemes/StripeCameraCore.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StripeCameraCore/StripeCameraCore/Info.plist b/StripeCameraCore/StripeCameraCore/Info.plist new file mode 100644 index 00000000..cd4a496b --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/StripeCameraCore/StripeCameraCore/Source/CameraExifMetadata.swift b/StripeCameraCore/StripeCameraCore/Source/CameraExifMetadata.swift new file mode 100644 index 00000000..4860f94b --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/CameraExifMetadata.swift @@ -0,0 +1,46 @@ +// +// CameraExifMetadata.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 4/14/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import CoreMedia +import Foundation +import ImageIO + +/// A helper to extract properties from an EXIF metadata dictionary +@_spi(STP) public struct CameraExifMetadata: Equatable { + public let brightnessValue: Double? + public let focalLength: Double? + public let lensModel: String? +} + +extension CameraExifMetadata { + public init?( + exifDictionary: [CFString: Any]? + ) { + guard let exifDictionary = exifDictionary else { + return nil + } + + self.init( + brightnessValue: exifDictionary[kCGImagePropertyExifBrightnessValue] as? Double, + focalLength: exifDictionary[kCGImagePropertyExifFocalLength] as? Double, + lensModel: exifDictionary[kCGImagePropertyExifLensModel] as? String + ) + } + + public init?( + sampleBuffer: CMSampleBuffer + ) { + self.init( + exifDictionary: CMGetAttachment( + sampleBuffer, + key: kCGImagePropertyExifDictionary, + attachmentModeOut: nil + ) as? [CFString: Any] + ) + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Categories/CGRect+StripeCameraCore.swift b/StripeCameraCore/StripeCameraCore/Source/Categories/CGRect+StripeCameraCore.swift new file mode 100644 index 00000000..29c5661b --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Categories/CGRect+StripeCameraCore.swift @@ -0,0 +1,80 @@ +// +// CGRect+StripeCameraCore.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 12/14/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import CoreGraphics +import Foundation + +@_spi(STP) extension CGRect { + + /// Represents the bounds of a normalized coordinate system with range from (0,0) to (1,1) + public static let normalizedBounds = CGRect(x: 0, y: 0, width: 1, height: 1) + + /// - Returns: A `CGRect` that has its y-coordinates inverted between the + /// upper-left corner and lower-left corner. + /// + /// - Note: + /// This should only be used for rects that are using a normalized + /// coordinate system, meaning that the coordinate of the corner opposite + /// origin is (1,1) + public var invertedNormalizedCoordinates: CGRect { + return CGRect( + x: minX, + y: 1 - minY - height, + width: width, + height: height + ) + } + + /// Converts a rectangle that's using a normalized coordinate system from a + /// center-crop coordinate system to an un-cropped coordinate system + /// + /// Example, if the original size has a portrait aspect ratio, center-cropping + /// the rect will result in the square area: + /// ``` + /// +---------+ + /// | | + /// |---------| + /// | | + /// | | + /// | | + /// |---------| + /// | | + /// +---------+ + /// ``` + /// + /// This method converts the rect's coordinate relative to the center-cropped + /// area into coordinates relative to the original un-cropped area: + /// ``` + /// +---------+ + /// | | + /// +---------+ | | + /// | +--+ | | +--+ | + /// | | | | --> | | | | + /// | +--+ | | +--+ | + /// +---------+ | | + /// | | + /// +---------+ + /// ``` + /// + /// - Parameters: + /// - size: The original size of the un-cropped area. + public func convertFromNormalizedCenterCropSquare( + toOriginalSize originalSize: CGSize + ) -> CGRect { + let croppedWidth = min(originalSize.width, originalSize.height) + let scaleX = croppedWidth / originalSize.width + let scaleY = croppedWidth / originalSize.height + + return CGRect( + x: (minX - 0.5) * scaleX + 0.5, + y: (minY - 0.5) * scaleY + 0.5, + width: width * scaleX, + height: height * scaleY + ) + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Categories/CVPixelBuffer+StripeCameraCore.swift b/StripeCameraCore/StripeCameraCore/Source/Categories/CVPixelBuffer+StripeCameraCore.swift new file mode 100644 index 00000000..6d147652 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Categories/CVPixelBuffer+StripeCameraCore.swift @@ -0,0 +1,18 @@ +// +// CVPixelBuffer+StripeCameraCore.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 3/16/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import CoreVideo +import VideoToolbox + +@_spi(STP) extension CVPixelBuffer { + public func cgImage() -> CGImage? { + var cgImage: CGImage? + VTCreateCGImageFromCVPixelBuffer(self, options: nil, imageOut: &cgImage) + return cgImage + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Categories/UIDeviceOrientation+StripeCameraCore.swift b/StripeCameraCore/StripeCameraCore/Source/Categories/UIDeviceOrientation+StripeCameraCore.swift new file mode 100644 index 00000000..e9bdbdf5 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Categories/UIDeviceOrientation+StripeCameraCore.swift @@ -0,0 +1,26 @@ +// +// UIDeviceOrientation+StripeCameraCore.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 1/20/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +import AVKit +import Foundation +import UIKit + +@_spi(STP) extension UIDeviceOrientation { + public var videoOrientation: AVCaptureVideoOrientation { + switch UIDevice.current.orientation { + case .portraitUpsideDown: + return .portraitUpsideDown + case .landscapeLeft: + return .landscapeRight + case .landscapeRight: + return .landscapeLeft + default: + return .portrait + } + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Categories/UIImage+Buffer.swift b/StripeCameraCore/StripeCameraCore/Source/Categories/UIImage+Buffer.swift new file mode 100644 index 00000000..9d13e713 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Categories/UIImage+Buffer.swift @@ -0,0 +1,118 @@ +// Ignoring file format becase of compiler directive which would force the whole file to be +// indented, including import statements. +// swift-format-ignore-file + +// Taken from https://gist.github.com/createwithswift/30a058c2745c8b09e64e7b073485e516 +// +// MIT License +// +// Copyright (c) 2021 Create with Swift +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if targetEnvironment(simulator) + +import CoreMedia +import UIKit + +extension UIImage { + @_spi(STP) public func convertToPixelBuffer() -> CVPixelBuffer? { + + let attributes = + [ + kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, + kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue, + ] as CFDictionary + + var pixelBuffer: CVPixelBuffer? + + let status = CVPixelBufferCreate( + kCFAllocatorDefault, + Int(self.size.width), + Int(self.size.height), + kCVPixelFormatType_32ARGB, + attributes, + &pixelBuffer + ) + + guard status == kCVReturnSuccess else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!) + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + + let context = CGContext( + data: pixelData, + width: Int(self.size.width), + height: Int(self.size.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), + space: rgbColorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue + ) + + context?.translateBy(x: 0, y: self.size.height) + context?.scaleBy(x: 1.0, y: -1.0) + + UIGraphicsPushContext(context!) + self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) + UIGraphicsPopContext() + + CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + + return pixelBuffer + } + + @_spi(STP) public func convertToSampleBuffer() -> CMSampleBuffer? { + guard let pixelBuffer = convertToPixelBuffer() else { + return nil + } + + var sampleBuffer: CMSampleBuffer? + var optionalVideoInfo: CMVideoFormatDescription? + var timingInfo: CMSampleTimingInfo = .invalid + + CMVideoFormatDescriptionCreateForImageBuffer( + allocator: nil, + imageBuffer: pixelBuffer, + formatDescriptionOut: &optionalVideoInfo + ) + guard let videoInfo = optionalVideoInfo else { + return nil + } + + CMSampleBufferCreateForImageBuffer( + allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer, + dataReady: true, + makeDataReadyCallback: nil, + refcon: nil, + formatDescription: videoInfo, + sampleTiming: &timingInfo, + sampleBufferOut: &sampleBuffer + ) + + return sampleBuffer + } +} + +#endif diff --git a/StripeCameraCore/StripeCameraCore/Source/Coordinators/AppSettingsHelper.swift b/StripeCameraCore/StripeCameraCore/Source/Coordinators/AppSettingsHelper.swift new file mode 100644 index 00000000..02b2ff78 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Coordinators/AppSettingsHelper.swift @@ -0,0 +1,43 @@ +// +// AppSettingsHelper.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 12/3/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import Foundation +import UIKit + +public protocol AppSettingsHelperProtocol { + var canOpenAppSettings: Bool { get } + func openAppSettings() +} + +/// Helper class that opens the app's settings screen in the Settings app. +@_spi(STP) public class AppSettingsHelper: AppSettingsHelperProtocol { + + public static let shared = AppSettingsHelper() + + private(set) lazy var appSettingsUrl: URL? = URL(string: UIApplication.openSettingsURLString) + + private init() { + // Use shared instance instead of init + } + + /// `true` if the system is able to open the app's settings screen. + public var canOpenAppSettings: Bool { + guard let settingsUrl = appSettingsUrl else { + return false + } + return UIApplication.shared.canOpenURL(settingsUrl) + } + + /// Opens the app's settings screen, if possible. + public func openAppSettings() { + guard let settingsUrl = appSettingsUrl else { + return + } + UIApplication.shared.open(settingsUrl) + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraPermissionsManager.swift b/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraPermissionsManager.swift new file mode 100644 index 00000000..36db983b --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraPermissionsManager.swift @@ -0,0 +1,85 @@ +// +// CameraPermissionsManager.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 12/3/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import AVKit +import Foundation + +@_spi(STP) public protocol CameraPermissionsManagerProtocol { + /// Completion block called when done requesting permissions. + /// + /// - Parameter granted: If camera permissions are granted for the app. + /// Value is `nil` if the authorization status cannot be determined. + typealias CompletionBlock = (_ granted: Bool?) -> Void + + var hasCameraAccess: Bool { get } + func requestCameraAccess( + completeOnQueue queue: DispatchQueue, + completion: @escaping CompletionBlock + ) +} + +/// Dependency-injectable class to assist with accessing and requesting camera authorization +@_spi(STP) public final class CameraPermissionsManager: CameraPermissionsManagerProtocol { + + public static let shared = CameraPermissionsManager() + + /// If this app currently has authorization to access the device's camera + public var hasCameraAccess: Bool { + return AVCaptureDevice.authorizationStatus(for: .video) == .authorized + } + + private init() { + // Use shared instance instead of init + } + + /// Requests camera permissions and calls completion block with result after retrieving them. + /// + /// - Parameters: + /// - completion: + /// - queue: DispatchQueue to complete the + /// + /// - Note: + /// If the user has already granted or denied camera permissions to the app, + /// this callback will respond immediately after `requestCameraAccess` is + /// called on the video feed and `showedPrompt` will be false. + /// + /// If the user has not yet granted or denied camera permissions to the app, + /// they will be prompted to do so. This callback will respond after the user + /// selects a response and `showedPrompt` will be true. + /// + public func requestCameraAccess( + completeOnQueue queue: DispatchQueue = .main, + completion: @escaping CompletionBlock + ) { + let wrappedCompletion: CompletionBlock = { granted in + queue.async { + completion(granted) + } + } + + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + wrappedCompletion(true) + + case .notDetermined: + AVCaptureDevice.requestAccess( + for: .video, + completionHandler: { granted in + wrappedCompletion(granted) + } + ) + + case .restricted, + .denied: + wrappedCompletion(false) + + default: + wrappedCompletion(nil) + } + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraSession.swift b/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraSession.swift new file mode 100644 index 00000000..55476fee --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Coordinators/CameraSession.swift @@ -0,0 +1,508 @@ +// +// CameraSession.swift +// StripeCameraCore +// +// Created by Jaime Park on 12/16/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import AVKit +@_spi(STP) import StripeCore + +@_spi(STP) @frozen public enum CameraSessionError: Error { + /// Can't find capture device to add + case captureDeviceNotFound + /// Session configuration has failed + case configurationFailed +} + +@_spi(STP) public protocol CameraSessionProtocol: AnyObject { + + var previewView: CameraPreviewView? { get set } + + func configureSession( + configuration: CameraSession.Configuration, + delegate: AVCaptureVideoDataOutputSampleBufferDelegate, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) + + func setVideoOrientation( + orientation: AVCaptureVideoOrientation + ) + + func toggleCamera( + to position: CameraSession.CameraPosition, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) + + func toggleTorch() + + func getCameraProperties() -> CameraSession.DeviceProperties? + + func startSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) + + func stopSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) +} + +@_spi(STP) public final class CameraSession: CameraSessionProtocol { + @frozen public enum SetupResult { + /// Session has successfully updated + case success + /// Session did not update due to an error + case failed(error: Error) + } + + public enum CameraPosition { + case front + case back + } + + public struct Configuration { + /// The initial position of camera: front or back + public let initialCameraPosition: CameraPosition + /// The initial video orientation of the camera session + public let initialOrientation: AVCaptureVideoOrientation + /// The capture device’s focus mode. + /// - Seealso: https://developer.apple.com/documentation/avfoundation/avcapturedevice/focusmode + public let focusMode: AVCaptureDevice.FocusMode? + /// The point of interest for focusing. + /// - Seealso: + /// https://developer.apple.com/documentation/avfoundation/avcapturedevice/focuspointofinterest + public let focusPointOfInterest: CGPoint? + /// A preset value of the quality of the capture session + public let sessionPreset: AVCaptureSession.Preset + /// Video settings for the video output + /// - Seealso: https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/video_settings + public let outputSettings: [String: Any] + /// https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624622-autofocusrangerestriction + public let autoFocusRangeRestriction: AVCaptureDevice.AutoFocusRangeRestriction + + /// - Parameters: + /// - initialCameraPosition: The initial position of camera: front or back + /// - initialOrientation: The initial video orientation of the camera session + /// - focusMode: The focus mode of the camera session + /// - focusPointOfInterest: The point of interest for focusing + /// - sessionPreset: A preset value of the quality of the capture session + /// - outputSettings: Video settings for the video output + public init( + initialCameraPosition: CameraPosition, + initialOrientation: AVCaptureVideoOrientation, + focusMode: AVCaptureDevice.FocusMode? = nil, + focusPointOfInterest: CGPoint? = nil, + sessionPreset: AVCaptureSession.Preset = .high, + outputSettings: [String: Any] = [:], + autoFocusRangeRestriction: AVCaptureDevice.AutoFocusRangeRestriction = .none + ) { + self.initialCameraPosition = initialCameraPosition + self.initialOrientation = initialOrientation + self.focusMode = focusMode + self.focusPointOfInterest = focusPointOfInterest + self.sessionPreset = sessionPreset + self.outputSettings = outputSettings + self.autoFocusRangeRestriction = autoFocusRangeRestriction + } + } + + public struct DeviceProperties: Equatable { + public let exposureDuration: CMTime + public let cameraDeviceType: AVCaptureDevice.DeviceType + public let isVirtualDevice: Bool? + public let lensPosition: Float + public let exposureISO: Float + public let isAdjustingFocus: Bool + } + + // MARK: - Properties + + public weak var previewView: CameraPreviewView? { + didSet { + guard oldValue !== previewView else { + return + } + + // Remove captureSession from previous view and add it to new one + oldValue?.setCaptureSession(nil, on: sessionQueue) + previewView?.setCaptureSession(session, on: sessionQueue) + } + } + + private let session: AVCaptureSession = AVCaptureSession() + private var captureConnection: AVCaptureConnection? + private let sessionQueue = DispatchQueue(label: "com.stripe.camera-session") + private var torchDevice: Torch? + private var setupResult: SetupResult? + + private var videoDeviceInput: AVCaptureDeviceInput? { + didSet { + if let videoDeviceInput = videoDeviceInput { + // Set torch with new capture input + self.torchDevice = Torch(device: videoDeviceInput.device) + } + } + } + + // MARK: - Public + + public init() { + // This is needed to expose init publicly + } + + /// Configures the camera session with the initial inputs and outputs. + /// + /// If the camera session has been configured already, then the configuration + /// is ignored and the previous setup result is passed to the completion block. + /// + /// - Parameters: + /// - configuration: Configuration settings for the session + /// - delegate: + /// - queue: DispatchQueue the completion block should be called on + /// - completion: A block executed when the session is done being configured + public func configureSession( + configuration: Configuration, + delegate: AVCaptureVideoDataOutputSampleBufferDelegate, + completeOn queue: DispatchQueue, + completion: @escaping (SetupResult) -> Void + ) { + sessionQueue.async { [weak self] in + guard let self = self else { return } + + // Check if already configured + if let setupResult = self.setupResult { + completion(setupResult) + return + } + + self.session.beginConfiguration() + self.session.sessionPreset = configuration.sessionPreset + self.session.commitConfiguration() + + self.configureSessionInput(with: configuration.initialCameraPosition).chained { + [weak self] _ -> Future in + guard let self = self else { + // If self has been deallocated before configuring output, return failure + let promise = Promise() + promise.reject(with: CameraSessionError.configurationFailed) + return promise + } + + return self.configureSessionOutput( + with: configuration.outputSettings, + orientation: configuration.initialOrientation, + focusMode: configuration.focusMode, + focusPointOfInterest: configuration.focusPointOfInterest, + autoFocusRangeRestriction: configuration.autoFocusRangeRestriction, + delegate: delegate + ) + }.observe(on: queue) { [weak self] result in + self?.setupResult = result.setupResult + completion(result.setupResult) + } + } + } + + /// Attempts to change the video orientation of both the session output + /// and the preview view layer. + /// + /// - Parameters: + /// - orientation: The desired video orientation + public func setVideoOrientation( + orientation: AVCaptureVideoOrientation + ) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.captureConnection?.videoOrientation = orientation + self.previewView?.videoPreviewLayer.connection?.videoOrientation = orientation + } + } + + /// Returns the properties from the camera device. + /// + /// - Note: This method can only be called on the camera session thread, + /// meaning it's only meant to be called from the output delegate's + /// `captureOutput` method. + public func getCameraProperties() -> CameraSession.DeviceProperties? { + dispatchPrecondition(condition: .onQueue(sessionQueue)) + + guard let device = videoDeviceInput?.device else { + return nil + } + + let isVirtualDevice = device.isVirtualDevice + + return .init( + exposureDuration: device.exposureDuration, + cameraDeviceType: device.deviceType, + isVirtualDevice: isVirtualDevice, + lensPosition: device.lensPosition, + exposureISO: device.iso, + isAdjustingFocus: device.isAdjustingFocus + ) + } + + /// Attempts to switch camera input to a new camera position. + /// - Parameters: + /// - position: The camera position to toggle to + /// - queue: DispatchQueue the completion block should be called on + /// - completion: A block executed when the camera has finished toggling + public func toggleCamera( + to position: CameraPosition, + completeOn queue: DispatchQueue, + completion: @escaping (SetupResult) -> Void + ) { + configureSessionInput(with: position).observe(on: queue) { result in + completion(result.setupResult) + } + } + + /// Attempts to toggle the torch on or off. + public func toggleTorch() { + self.torchDevice?.toggle() + } + + /// Starts the camera session, calling a completion block when the session has + /// been started. + /// + /// - Parameters: + /// - queue: The queue to call the completion block on + /// - completion: A block executed when the session has been started + public func startSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + sessionQueue.async { [weak self] in + defer { + queue.async { + completion() + } + } + + guard let self = self, + case .success = self.setupResult + else { + return + } + + self.session.startRunning() + } + } + + /// Stop the camera session + public func stopSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + sessionQueue.async { [weak self] in + defer { + queue.async { + completion() + } + } + + guard let self = self, + case .success = self.setupResult + else { + return + } + + self.session.stopRunning() + } + } +} + +// MARK: - Private + +extension CameraSession { + fileprivate func configureSessionInput( + with position: CameraPosition + ) -> Future { + let promise = Promise() + + sessionQueue.async { [weak self] in + guard let self = self else { return } + + self.session.beginConfiguration() + defer { + self.session.commitConfiguration() + } + + do { + // Remove inputs + self.session.inputs.forEach { + self.session.removeInput($0) + } + + let newVideoDeviceInput = try self.captureDeviceInput(position: position) + + // Add video input + guard self.session.canAddInput(newVideoDeviceInput) + else { + promise.reject(with: CameraSessionError.configurationFailed) + return + } + self.session.addInput(newVideoDeviceInput) + + // Keep reference to video device input + self.videoDeviceInput = newVideoDeviceInput + + promise.resolve(with: ()) + } catch { + promise.reject(with: error) + } + } + + return promise + } + + fileprivate func configureSessionOutput( + with videoSettings: [String: Any], + orientation: AVCaptureVideoOrientation, + focusMode: AVCaptureDevice.FocusMode?, + focusPointOfInterest: CGPoint?, + autoFocusRangeRestriction: AVCaptureDevice.AutoFocusRangeRestriction, + delegate: AVCaptureVideoDataOutputSampleBufferDelegate + ) -> Future { + let promise = Promise() + + sessionQueue.async { [weak self] in + guard let self = self else { return } + + self.session.beginConfiguration() + defer { + self.session.commitConfiguration() + } + + let videoOutput = AVCaptureVideoDataOutput() + videoOutput.videoSettings = videoSettings + videoOutput.alwaysDiscardsLateVideoFrames = true + videoOutput.setSampleBufferDelegate(delegate, queue: self.sessionQueue) + + guard self.session.canAddOutput(videoOutput) else { + promise.reject(with: CameraSessionError.configurationFailed) + return + } + + // Add output to session + self.session.addOutput(videoOutput) + + // Update output connection reference + self.captureConnection = videoOutput.connection(with: .video) + + // Update new output and previewLayer orientation + self.setVideoOrientation(orientation: orientation) + + // Set focus if needed + guard let focusMode = focusMode else { + promise.resolve(with: ()) + return + } + + promise.fulfill { [weak self] in + try self?.setFocusOnCurrentQueue( + focusMode: focusMode, + autoFocusRangeRestriction: autoFocusRangeRestriction, + focusPointOfInterest: focusPointOfInterest + ) + } + } + + return promise + } + + fileprivate func captureDeviceInput(position: CameraPosition) throws -> AVCaptureDeviceInput { + let captureDevices = AVCaptureDevice.DiscoverySession( + deviceTypes: position.captureDeviceTypes, + mediaType: .video, + position: position.captureDevicePosition + ) + + guard let captureDevice = captureDevices.devices.first else { + throw CameraSessionError.captureDeviceNotFound + } + + return try AVCaptureDeviceInput(device: captureDevice) + } + + fileprivate func setFocusOnCurrentQueue( + focusMode: AVCaptureDevice.FocusMode, + autoFocusRangeRestriction: AVCaptureDevice.AutoFocusRangeRestriction, + focusPointOfInterest: CGPoint? + ) throws { + dispatchPrecondition(condition: .onQueue(sessionQueue)) + + guard let device = videoDeviceInput?.device else { + return + } + + try device.lockForConfiguration() + if device.isFocusModeSupported(focusMode) { + device.focusMode = focusMode + + } + + if device.isAutoFocusRangeRestrictionSupported { + device.autoFocusRangeRestriction = autoFocusRangeRestriction + } + + if let focusPointOfInterest = focusPointOfInterest, + device.isFocusPointOfInterestSupported + { + if device.isSmoothAutoFocusSupported { + device.isSmoothAutoFocusEnabled = true + } + device.focusPointOfInterest = focusPointOfInterest + } + + if device.isLowLightBoostSupported { + device.automaticallyEnablesLowLightBoostWhenAvailable = true + } + device.unlockForConfiguration() + } +} + +// MARK: - CameraPosition + +extension CameraSession.CameraPosition { + /// Returns a list of camera devices, ordered by preferred device, for this + /// camera position. + var captureDeviceTypes: [AVCaptureDevice.DeviceType] { + switch self { + case .front: + return [.builtInTrueDepthCamera, .builtInWideAngleCamera] + + case .back: + return [.builtInTripleCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInWideAngleCamera] + } + } + + var captureDevicePosition: AVCaptureDevice.Position { + switch self { + case .front: + return .front + case .back: + return .back + } + } +} + +// MARK: - Result + +/// Helper to convert a Result into SetupResult +extension Result where Success == Void, Failure == Error { + fileprivate var setupResult: CameraSession.SetupResult { + switch self { + case .success: + return .success + case .failure(let error): + return .failed(error: error) + } + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Coordinators/MockSimulatorCameraSession.swift b/StripeCameraCore/StripeCameraCore/Source/Coordinators/MockSimulatorCameraSession.swift new file mode 100644 index 00000000..ef0bdf74 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Coordinators/MockSimulatorCameraSession.swift @@ -0,0 +1,220 @@ +// +// MockSimulatorCameraSession.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 1/21/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + +// Ignoring file formatt becase of compiler directive which would force the whole file to be +// indented, including import statements. +// swift-format-ignore-file + +#if targetEnvironment(simulator) + +import AVKit +import Foundation +@_spi(STP) import StripeCore + +/// Mocks a CameraSession on the simulator. +@_spi(STP) public final class MockSimulatorCameraSession: CameraSessionProtocol { + + enum Error: Swift.Error { + case sessionNotConfigured + case noImages + } + + static let mockSampleBufferTimeInterval: TimeInterval = 0.1 + + private let images: [UIImage] + private var nextImageToReturn: Int = 0 + private var currentImage: UIImage? + private let sessionQueue = DispatchQueue(label: "com.stripe.mock-simulator-camera-session") + private let videoOutput = AVCaptureVideoDataOutput() + private lazy var captureConnection = AVCaptureConnection( + inputPorts: [], + output: videoOutput + ) + private var mockSampleBufferTimer: Timer? + weak private var delegate: AVCaptureVideoDataOutputSampleBufferDelegate? + + // MARK: - Public + + private var isConfigured: Bool = false + private var cameraPosition: CameraSession.CameraPosition = .front + + public weak var previewView: CameraPreviewView? { + didSet { + setPreviewViewToCurrentImage() + } + } + + public init( + images: [UIImage] + ) { + self.images = images + } + + public func configureSession( + configuration: CameraSession.Configuration, + delegate: AVCaptureVideoDataOutputSampleBufferDelegate, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) { + sessionQueue.async { [weak self] in + let wrappedCompletion = { setupResult in + queue.async { + completion(setupResult) + } + } + + guard let self = self else { return } + self.delegate = delegate + + guard !self.images.isEmpty else { + self.isConfigured = false + wrappedCompletion(.failed(error: Error.noImages)) + return + } + + self.cameraPosition = configuration.initialCameraPosition + self.isConfigured = true + wrappedCompletion(.success) + } + } + + public func setVideoOrientation(orientation: AVCaptureVideoOrientation) { + // no-op + } + + public func toggleCamera( + to position: CameraSession.CameraPosition, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) { + sessionQueue.async { [weak self] in + guard let self = self else { return } + + let wrappedCompletion = { setupResult in + queue.async { + completion(setupResult) + } + } + + guard self.isConfigured else { + wrappedCompletion(.failed(error: Error.sessionNotConfigured)) + return + } + + self.cameraPosition = position + wrappedCompletion(.success) + } + } + + public func getCameraProperties() -> CameraSession.DeviceProperties? { + return .init( + exposureDuration: CMTime(value: 0, timescale: 1), + cameraDeviceType: .builtInDualCamera, + isVirtualDevice: nil, + lensPosition: 0, + exposureISO: 0, + isAdjustingFocus: false + ) + } + + public func toggleTorch() { + // no-op + } + + public func startSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + sessionQueue.async { [weak self] in + guard let self = self else { return } + + defer { + queue.async { + completion() + } + } + + if self.isConfigured && self.currentImage == nil { + self.currentImage = self.images.stp_boundSafeObject(at: self.nextImageToReturn) + self.setPreviewViewToCurrentImage() + } + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.mockSampleBufferTimer = Timer.scheduledTimer( + timeInterval: MockSimulatorCameraSession.mockSampleBufferTimeInterval, + target: self, + selector: #selector(self.mockSampleBufferDelegateCallback), + userInfo: nil, + repeats: true + ) + } + } + } + + public func stopSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + sessionQueue.async { [weak self] in + guard let self = self else { return } + + defer { + queue.async { + completion() + } + } + + self.mockSampleBufferTimer?.invalidate() + + if self.currentImage != nil { + self.nextImageToReturn += 1 + } + self.currentImage = nil + } + } +} + +extension MockSimulatorCameraSession { + @objc fileprivate func mockSampleBufferDelegateCallback() { + sessionQueue.async { [weak self] in + guard let self = self, + var image = self.currentImage + else { + return + } + + // Flip image horizontally if mocking front-facing camera + if self.cameraPosition == .front, + let cgImage = image.cgImage + { + image = UIImage(cgImage: cgImage, scale: image.scale, orientation: .upMirrored) + } + + guard let sampleBuffer = image.convertToSampleBuffer() else { + return + } + + self.delegate?.captureOutput?( + self.videoOutput, + didOutput: sampleBuffer, + from: self.captureConnection + ) + } + } + + fileprivate func setPreviewViewToCurrentImage() { + DispatchQueue.main.async { [weak self, weak currentImage] in + guard let self = self else { return } + self.previewView?.layer.contents = currentImage?.cgImage + self.previewView?.layer.contentsGravity = .resizeAspectFill + } + } +} + +#endif diff --git a/StripeCameraCore/StripeCameraCore/Source/Coordinators/Torch.swift b/StripeCameraCore/StripeCameraCore/Source/Coordinators/Torch.swift new file mode 100644 index 00000000..caa5ef05 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Coordinators/Torch.swift @@ -0,0 +1,53 @@ +// +// Torch.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 12/1/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import AVFoundation +import Foundation + +struct Torch { + enum State { + case off + case on + } + let device: AVCaptureDevice? + var state: State + var lastStateChange: Date + var level: Float + + init( + device: AVCaptureDevice + ) { + self.state = .off + self.lastStateChange = Date() + if device.hasTorch { + self.device = device + if device.isTorchActive { + self.state = .on + } + } else { + self.device = nil + } + self.level = 1.0 + } + + mutating func toggle() { + self.state = self.state == .on ? .off : .on + do { + try self.device?.lockForConfiguration() + if self.state == .on { + try self.device?.setTorchModeOn(level: self.level) + } else { + self.device?.torchMode = .off + } + } catch { + // no-op + } + // Always unlock when we're done even if `setTorchModeOn` threw + self.device?.unlockForConfiguration() + } +} diff --git a/StripeCameraCore/StripeCameraCore/Source/Views/CameraPreviewView.swift b/StripeCameraCore/StripeCameraCore/Source/Views/CameraPreviewView.swift new file mode 100644 index 00000000..61a8ed97 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/Source/Views/CameraPreviewView.swift @@ -0,0 +1,81 @@ +// +// CameraPreviewView.swift +// StripeCameraCore +// +// Created by Mel Ludowise on 12/1/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import AVFoundation +import UIKit + +@_spi(STP) public class CameraPreviewView: UIView { + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + /// Camera session that configures the video feed for this view. + /// + /// - Note: + /// A session can only be used on one `PreviewView` at a time. Setting the same + /// session on a different `PreviewView` will remove it from the previous one. + public weak var session: CameraSessionProtocol? { + didSet { + guard oldValue !== session else { + return + } + oldValue?.previewView = nil + session?.previewView = self + } + } + + // MARK: Initialization + + public init() { + super.init(frame: .zero) + + videoPreviewLayer.videoGravity = .resizeAspectFill + } + + required init?( + coder aDecoder: NSCoder + ) { + super.init(coder: aDecoder) + } + + // MARK: UIView + + public override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + // MARK: Internal + + /// Sets the video capture session in thread-safe way that doesn't block main + /// thread when configuring the session. + /// + /// - Parameters: + /// - captureSession: The capture session to set on this view + /// - cameraSessionQueue: The CameraSession's queue to configure the session + func setCaptureSession( + _ captureSession: AVCaptureSession?, + on cameraSessionQueue: DispatchQueue + ) { + // Get reference to videoPreviewLayer on main queue then dispatch to + // worker queue to set session so it doesn't block main. + + let mainWorkItem = DispatchWorkItem { [weak self] in + guard let self = self else { return } + cameraSessionQueue.async { + [weak captureSession, weak videoPreviewLayer = self.videoPreviewLayer] in + videoPreviewLayer?.session = captureSession + } + } + + if Thread.isMainThread { + mainWorkItem.perform() + } else { + DispatchQueue.main.async(execute: mainWorkItem) + } + } +} diff --git a/StripeCameraCore/StripeCameraCore/StripeCameraCore.h b/StripeCameraCore/StripeCameraCore/StripeCameraCore.h new file mode 100644 index 00000000..6cb45611 --- /dev/null +++ b/StripeCameraCore/StripeCameraCore/StripeCameraCore.h @@ -0,0 +1,18 @@ +// +// StripeCameraCore.h +// StripeCameraCore +// +// Created by Mel Ludowise on 11/15/21. +// + +#import + +//! Project version number for StripeCameraCore. +FOUNDATION_EXPORT double StripeCameraCoreVersionNumber; + +//! Project version string for StripeCameraCore. +FOUNDATION_EXPORT const unsigned char StripeCameraCoreVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/StripeCameraCore/StripeCameraCoreTestUtils/Info.plist b/StripeCameraCore/StripeCameraCoreTestUtils/Info.plist new file mode 100644 index 00000000..9bcb2444 --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTestUtils/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockAppSettingsHelper.swift b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockAppSettingsHelper.swift new file mode 100644 index 00000000..608e1d1f --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockAppSettingsHelper.swift @@ -0,0 +1,21 @@ +// +// MockAppSettingsHelper.swift +// StripeCameraCoreTestUtils +// +// Created by Mel Ludowise on 12/3/21. +// + +import Foundation +@_spi(STP) import StripeCameraCore + +@_spi(STP) public class MockAppSettingsHelper: AppSettingsHelperProtocol { + + public var canOpenAppSettings = false + public private(set) var didOpenAppSettings = false + + public init() {} + + public func openAppSettings() { + didOpenAppSettings = true + } +} diff --git a/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockCameraPermissionsManager.swift b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockCameraPermissionsManager.swift new file mode 100644 index 00000000..811451be --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockCameraPermissionsManager.swift @@ -0,0 +1,41 @@ +// +// MockCameraPermissionsManager.swift +// StripeCameraCoreTestUtils +// +// Created by Mel Ludowise on 12/3/21. +// + +import Foundation +@_spi(STP) import StripeCameraCore +import XCTest + +@_spi(STP) public class MockCameraPermissionsManager: CameraPermissionsManagerProtocol { + + public var hasCameraAccess = false + + public private(set) var didRequestCameraAccess = false + public let didCompleteExpectation = XCTestExpectation( + description: "MockCameraPermissionsManager completion did finish" + ) + + private var completion: CompletionBlock = { _ in } + + public init() {} + + public func requestCameraAccess( + completeOnQueue queue: DispatchQueue, + completion: @escaping CompletionBlock + ) { + self.completion = { granted in + queue.async { [weak self] in + completion(granted) + self?.didCompleteExpectation.fulfill() + } + } + didRequestCameraAccess = true + } + + public func respondToRequest(granted: Bool?) { + completion(granted) + } +} diff --git a/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockTestCameraSession.swift b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockTestCameraSession.swift new file mode 100644 index 00000000..5ba9bea0 --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTestUtils/Mocks/MockTestCameraSession.swift @@ -0,0 +1,180 @@ +// +// MockTestCameraSession.swift +// StripeCameraCoreTestUtils +// +// Created by Mel Ludowise on 1/21/22. +// + +import AVKit +import Foundation +@_spi(STP)@testable import StripeCameraCore +import XCTest + +@_spi(STP) public final class MockTestCameraSession: CameraSessionProtocol { + + /// Mock image to display in previewView. Should be used for snapshot tests + public var mockImage: UIImage? { + didSet { + setPreviewViewToMockImage() + } + } + + public var previewView: CameraPreviewView? { + didSet { + setPreviewViewToMockImage() + } + } + + public var mockDeviceProperties = CameraSession.DeviceProperties( + exposureDuration: CMTime(), + cameraDeviceType: .builtInDualCamera, + isVirtualDevice: nil, + lensPosition: 0, + exposureISO: 0, + isAdjustingFocus: false + ) + + public init() { + // no-op + } + + // MARK: configureSession + + private var configureCompletion: ((CameraSession.SetupResult) -> Void)? + public private(set) var configureSessionCompletionExp = XCTestExpectation( + description: "configureSession completion block called" + ) + + public private(set) var sessionPreset: AVCaptureSession.Preset? + public private(set) var outputSettings: [String: Any]? + + public func configureSession( + configuration: CameraSession.Configuration, + delegate: AVCaptureVideoDataOutputSampleBufferDelegate, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) { + cameraPosition = configuration.initialCameraPosition + videoOrientation = configuration.initialOrientation + sessionPreset = configuration.sessionPreset + outputSettings = configuration.outputSettings + configureCompletion = { setupResult in + queue.async { [weak self] in + self?.configureSessionCompletionExp.fulfill() + completion(setupResult) + } + } + } + + public func respondToConfigureSession(setupResult: CameraSession.SetupResult) { + configureCompletion?(setupResult) + } + + // MARK: setVideoOrientation + + public private(set) var videoOrientation: AVCaptureVideoOrientation? + + public func setVideoOrientation(orientation: AVCaptureVideoOrientation) { + self.videoOrientation = orientation + } + + // MARK: toggleCamera + + private var toggleCameraCompletion: ((CameraSession.SetupResult) -> Void)? + public private(set) var cameraPosition: CameraSession.CameraPosition? + + public func toggleCamera( + to position: CameraSession.CameraPosition, + completeOn queue: DispatchQueue, + completion: @escaping (CameraSession.SetupResult) -> Void + ) { + toggleCameraCompletion = { setupResult in + queue.async { + completion(setupResult) + } + } + cameraPosition = position + } + + public func respondToToggleCamera(setupResult: CameraSession.SetupResult) { + toggleCameraCompletion?(setupResult) + } + + // MARK: toggleTorch + + public private(set) var didToggleTorch = false + + public func toggleTorch() { + didToggleTorch = true + } + + // MARK: getCameraProperties + + public func getCameraProperties() -> CameraSession.DeviceProperties? { + return mockDeviceProperties + } + + // MARK: startSession + + private var startSessionCompletion: (() -> Void)? + public private(set) var startSessionCompletionExp = XCTestExpectation( + description: "startSession completion block called" + ) + + public func startSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + startSessionCompletion = { + queue.async { [weak self] in + self?.startSessionCompletionExp.fulfill() + completion() + } + } + } + + public func respondToStartSession() { + startSessionCompletion?() + } + + // MARK: stopSession + + private var stopSessionCompletion: (() -> Void)? + public private(set) var stopSessionCompletionExp = XCTestExpectation( + description: "stopSession completion block called" + ) + + public func stopSession( + completeOn queue: DispatchQueue, + completion: @escaping () -> Void + ) { + stopSessionCompletion = { + queue.async { [weak self] in + self?.stopSessionCompletionExp.fulfill() + completion() + } + } + } + + public func respondToStopSession() { + stopSessionCompletion?() + } + + // MARK: previewView + + func setPreviewViewToMockImage() { + let block = { [weak self] in + guard let self = self else { return } + self.previewView?.layer.contents = self.mockImage?.cgImage + self.previewView?.layer.contentsGravity = .resizeAspectFill + } + + // Only dispatch to main async if necessary so this can run + // synchronously for snapshot tests. + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async(execute: block) + } + } +} diff --git a/StripeCameraCore/StripeCameraCoreTestUtils/StripeCameraCoreTestUtils.h b/StripeCameraCore/StripeCameraCoreTestUtils/StripeCameraCoreTestUtils.h new file mode 100644 index 00000000..1f3c16b0 --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTestUtils/StripeCameraCoreTestUtils.h @@ -0,0 +1,18 @@ +// +// StripeCameraCoreTestUtils.h +// StripeCameraCoreTestUtils +// +// Created by Mel Ludowise on 11/15/21. +// + +#import + +//! Project version number for StripeCameraCoreTestUtils. +FOUNDATION_EXPORT double StripeCameraCoreTestUtilsVersionNumber; + +//! Project version string for StripeCameraCoreTestUtils. +FOUNDATION_EXPORT const unsigned char StripeCameraCoreTestUtilsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/StripeCameraCore/StripeCameraCoreTests/Info.plist b/StripeCameraCore/StripeCameraCoreTests/Info.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/StripeCameraCore/StripeCameraCoreTests/Unit/Categories/CGRect_StripeCameraCoreTest.swift b/StripeCameraCore/StripeCameraCoreTests/Unit/Categories/CGRect_StripeCameraCoreTest.swift new file mode 100644 index 00000000..317135ec --- /dev/null +++ b/StripeCameraCore/StripeCameraCoreTests/Unit/Categories/CGRect_StripeCameraCoreTest.swift @@ -0,0 +1,68 @@ +// +// CGRect_StripeCameraCoreTest.swift +// StripeCameraCoreTests +// +// Created by Mel Ludowise on 12/14/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import CoreGraphics +@_spi(STP) import StripeCameraCore +import XCTest + +class CGRect_StripeCameraCoreTest: XCTestCase { + func testInvertedNormalizedCoordinates() { + let rect = CGRect(x: 0.1, y: 0.1, width: 0.5, height: 0.5) + let invertedRect = rect.invertedNormalizedCoordinates + + XCTAssertEqual(invertedRect.minX, 0.1) + XCTAssertEqual(invertedRect.minY, 0.4) + XCTAssertEqual(invertedRect.width, 0.5) + XCTAssertEqual(invertedRect.height, 0.5) + } + + func testConvertFromNormalizedCenterCropSquareLandscape() { + // Centered-square rect corresponds to (350,0) 900x900 + let originalSize = CGSize(width: 1600, height: 900) + + // Corresponds to actual coordinates of + // (350+900*0.25,900*0.25) 900*0.25 x 900*0.25 + // = (575,225) 225x225 + let normalizedSquareRect = CGRect(x: 0.25, y: 0.25, width: 0.25, height: 0.25) + + // Should have coordinates of + // (575/1600, 225/900) 225/1600 x 225/900 + // = (0.359375,0.25) 0.140625 x 0.25 + let convertedRect = normalizedSquareRect.convertFromNormalizedCenterCropSquare( + toOriginalSize: originalSize + ) + + XCTAssertEqual(convertedRect.minX, 0.359375) + XCTAssertEqual(convertedRect.minY, 0.25) + XCTAssertEqual(convertedRect.width, 0.140625) + XCTAssertEqual(convertedRect.height, 0.25) + } + + func testConvertFromNormalizedCenterCropSquarePortrait() { + // Centered-square rect corresponds to (0,350) 900x900 + let originalSize = CGSize(width: 900, height: 1600) + + // Corresponds to actual coordinates of + // (900*0.25,350+900*0.25) 900*0.25 x 900*0.25 + // = (225,575) 225x225 + let normalizedSquareRect = CGRect(x: 0.25, y: 0.25, width: 0.25, height: 0.25) + + // Should have coordinates of + // (225/900, 575/1600) 225/900 x 225/1600 + // = (0.25,0.359375) 0.25 x 0.140625 + let convertedRect = normalizedSquareRect.convertFromNormalizedCenterCropSquare( + toOriginalSize: originalSize + ) + + XCTAssertEqual(convertedRect.minX, 0.25) + XCTAssertEqual(convertedRect.minY, 0.359375) + XCTAssertEqual(convertedRect.width, 0.25) + XCTAssertEqual(convertedRect.height, 0.140625) + } + +} diff --git a/StripeCardScan/BuildConfigurations/StripeCardScan-Debug.xcconfig b/StripeCardScan/BuildConfigurations/StripeCardScan-Debug.xcconfig new file mode 100644 index 00000000..0f5765ca --- /dev/null +++ b/StripeCardScan/BuildConfigurations/StripeCardScan-Debug.xcconfig @@ -0,0 +1,7 @@ +// +// StripeCardScan-Debug.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS-Debug.xcconfig" + +APPLICATION_EXTENSION_API_ONLY = NO diff --git a/StripeCardScan/BuildConfigurations/StripeCardScan-Release.xcconfig b/StripeCardScan/BuildConfigurations/StripeCardScan-Release.xcconfig new file mode 100644 index 00000000..e99bbccc --- /dev/null +++ b/StripeCardScan/BuildConfigurations/StripeCardScan-Release.xcconfig @@ -0,0 +1,7 @@ +// +// StripeCardScan-Release.xcconfig +// + +#include "../../BuildConfigurations/StripeiOS-Release.xcconfig" + +APPLICATION_EXTENSION_API_ONLY = NO diff --git a/StripeCardScan/README.md b/StripeCardScan/README.md new file mode 100644 index 00000000..3f270ff3 --- /dev/null +++ b/StripeCardScan/README.md @@ -0,0 +1,87 @@ +# Stripe CardScan iOS SDK + +This library provides support for the standalone Stripe CardScan product. + +## Overview + +This library provides a user interface through which users can scan payment cards and extract information from them. It uses the Stripe Publishable Key to authenticate with Stripe services. + +Note that this is a standalone SDK and, while compatible with, does not directly integrate with the [PaymentIntent](https://stripe.com/docs/api/payment_intents) API nor with [next_action](https://stripe.com/docs/api/errors#errors-payment_intent-next_action). + +This library can be used entirely outside of a Stripe integration and with other payment processing providers. + +## Requirements + +- iOS 13.0 or higher +- Xcode 15 or higher + +## Example + +See the [CardImageVerification Example](https://github.com/stripe/stripe-ios/tree/master/Example/CardImageVerification%20Example) directory for an example application that you can try for yourself! + +## Installation + +- Cocoapod + - `pod install StripeCardScan` +- SPM + - In Xcode, select File > Add Packages… and enter https://github.com/stripe/stripe-ios-spm as the repository URL. + - Select the latest version number from our releases page. + - Add the `StripeCardScan` product to the target of your app. + +## Integration +### Credit Card OCR +Add `CardScanSheet` in your view controller where you want to invoke the credit card scanning flow. + +1. Set up camera permissions + * The SDK uses the camera, so you'll need to add an description of camera usage to your Info.plist file: +![info.plist camera permissions](https://gblobscdn.gitbook.com/assets%2F-MAfqrnL3d-uke0sAFsI%2Fsync%2F573e3f05043e4d903189b5fb107d4b3565bdb11b.png?alt=media) +![camera permissions prompt](https://gblobscdn.gitbook.com/assets%2F-MAfqrnL3d-uke0sAFsI%2Fsync%2F0d7119d3cbe2f519e5e5c04b56fe43539e4435e1.png?alt=media) + + * Alternatively, you can add this permission directly to your Info.plist file: + ``` + NSCameraUsageDescriptionkey> + We need access to your camera to scan your cardstring> + ``` +2. Add `CardScanSheet` in your app where you want to invoke the scan flow + * Initialize `CardScanSheet` + * When it’s time to invoke the scan flow, display the sheet with `CardScanSheet.present()` + * When the verification flow is finished, the sheet will be dismissed and the completion block will be called with a [Result](https://stripe.dev/stripe-ios/) + +### Example Implementation +```swift + +import UIKit +import StripeCardScan + +class ViewController: UIViewController { + + @IBAction func cardScanSheetButtonPressed() { + let cardScanSheet = CardScanSheet() + + cardScanSheet.present(from: self) { [weak self] result in + switch result { + case .completed(let scannedCard): + /* + * The user scanned a card. The result of the scan are detailed + * in the `scannedCard` field of the result. + */ + print("scan success") + case .canceled: + /* + * The scan was canceled by the user. + */ + print("scan canceled") + case .failed(let error): + /* + * The scan failed. The displayable error is + * included in the `localizedDescription`. + */ + print("scan failed: \(error.localizedDescription)") + } + } + } +} +``` + +## Credit Card Verification +🚧 diff --git a/StripeCardScan/StripeCardScan.xcodeproj/project.pbxproj b/StripeCardScan/StripeCardScan.xcodeproj/project.pbxproj new file mode 100644 index 00000000..91e6292b --- /dev/null +++ b/StripeCardScan/StripeCardScan.xcodeproj/project.pbxproj @@ -0,0 +1,1263 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 040020B8558B0489DF99F8B5 /* DetectedAllBoxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7EE5FDCA3B075FE5BBAF42A /* DetectedAllBoxes.swift */; }; + 050B462804602A9F4DB58A9D /* Expiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F3DDAD14A9F2DEB98E236E /* Expiry.swift */; }; + 05188062E522359CE24912CF /* ScannedCardImageData+Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3EE6315FB1507E40F10D85 /* ScannedCardImageData+Verification.swift */; }; + 06711423AB563634EADFCCC0 /* VerifyCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F912323832888F88EA218BA9 /* VerifyCardViewController.swift */; }; + 0838FEA7071EDCE1B633F093 /* PredictionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B452EE8B96AF1B0691E7D43 /* PredictionAPI.swift */; }; + 0A4DCE2C98659B92DDB29B9A /* ScanAnalyticsManager+Tasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6563E5CE5CD0439D5DBF35D7 /* ScanAnalyticsManager+Tasks.swift */; }; + 0ADD10809FEABDCB59A8FA72 /* CardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236AE86CE7DCB0DFA3B7C978 /* CardType.swift */; }; + 0D062E99363099A5728F3DCD /* ScanAnalyticsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F6ECCDC721E4D46DAC4F48 /* ScanAnalyticsManagerTests.swift */; }; + 0D62200AA65D71F040037CC8 /* CardImageVerification_CardAdd_200.json in Resources */ = {isa = PBXBuildFile; fileRef = CF90067085243D998A1D6030 /* CardImageVerification_CardAdd_200.json */; }; + 0D8CA0E3EFF9694CAE4FB8F7 /* ScanConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9618F7B15B33EC5B339A94 /* ScanConfiguration.swift */; }; + 0EAA2314FEA8D05D24C498BD /* Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C827434739027B2DD43449EA /* Torch.swift */; }; + 10E840B00703A92CC487B324 /* CGRectExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7633166B4D9096FE995F08F0 /* CGRectExtension.swift */; }; + 12C11D2633C7819DF275CCC2 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = EA26D443783FE045B0D7888D /* OHHTTPStubsSwift */; }; + 164BF41B5C522B7338376BB2 /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938958FCB9B918AD6CDF0F4E /* BlurView.swift */; }; + 17E34C9EF882AEAE47BECAB4 /* ErrorCorrection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEC906101069A1E3C50FA51 /* ErrorCorrection.swift */; }; + 189DEBAF38F2FB88CCA39EA2 /* UxModel+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3FCD1B2283A2B02EBAEF4F /* UxModel+Utils.swift */; }; + 18B63245A933E292345C9410 /* UxAndOcrMainLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87448BAE438EF08C55AA4B29 /* UxAndOcrMainLoop.swift */; }; + 19EF4634631CFC121DE21D1B /* CardImageVerificationDetailsResponseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDAF8E33A3D7CD020AC904A /* CardImageVerificationDetailsResponseTest.swift */; }; + 19F7EC09C9B0ED11A4601E08 /* ScanStatsPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61E754E520B4E21920D1DA /* ScanStatsPayload.swift */; }; + 1AD72764EA8BCB66A9D7B637 /* CreditCardOcrPrediction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEE8C18A3CECFF0751AB0D5 /* CreditCardOcrPrediction.swift */; }; + 1B10222A5121C9B2C3479FAB /* StripeCardScan.h in Headers */ = {isa = PBXBuildFile; fileRef = AB56DDBBF0E5BC10682C7D96 /* StripeCardScan.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1B71815D5B3EC8AC21E2A202 /* UIImage+pixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D9886B0ED119E8D1FB6448 /* UIImage+pixelBuffer.swift */; }; + 1DE075A700592250D87DF558 /* ScannedCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76C70F9A1879ED8B41639945 /* ScannedCard.swift */; }; + 20DCEB955D9E0660EAB3ABEB /* InterfaceOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D0D8C2909455B76E246468 /* InterfaceOrientation.swift */; }; + 20DF2E9D5A543360DF3A4DDA /* DetectedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53D407C0AC8D44ECC0C3C52 /* DetectedBox.swift */; }; + 213A91107E76434972A1F848 /* SSDOcr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3E329C37141862BD4A4DE7 /* SSDOcr.swift */; }; + 25FAF64D4FE93BF38E807ECA /* VerifyFramesAPIBindingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4301B281F5E0A5BACBBD68CC /* VerifyFramesAPIBindingsTests.swift */; }; + 2A9ECCF086685AD607D82492 /* AppleOcr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E24CF248F312DDE025D662A /* AppleOcr.swift */; }; + 2D042FD2E8A0C8236596CE54 /* CardScanFraudData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D41E45A285FEF48E9528997 /* CardScanFraudData.swift */; }; + 2F10596629118185A3A8F011 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = C96A49C871FDB73B36A91670 /* OHHTTPStubs */; }; + 2F3FA5E8CCBF7F77106A8268 /* EndToEndTestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3ECBF071E7507C9D41F232 /* EndToEndTestDataSource.swift */; }; + 30E3E90F9C8E3D3E8FCA869E /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC12B6E0657203AAB765468C /* PreviewView.swift */; }; + 313F5F7D2B0BE5BE00BD98A9 /* Docs.docc in Sources */ = {isa = PBXBuildFile; fileRef = 313F5F7C2B0BE5BE00BD98A9 /* Docs.docc */; }; + 35F2BB10395405DB6C1E31B5 /* SSDOcrOutputExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89DA7FF10BC3A7B2D102FB /* SSDOcrOutputExtensions.swift */; }; + 36525A0F774E7FA055D8B525 /* CardBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEDE71D7051DDC98698C4057 /* CardBase.swift */; }; + 3736756FBB875060C86E2777 /* CardScanSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BC8B1C28C3F6C8330164E0 /* CardScanSheet.swift */; }; + 3F5B9466E9FC13CA29218750 /* ImageHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091163ED5E37922332F502B1 /* ImageHelpers.swift */; }; + 3FCC183583090FD0246D9336 /* ScanBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A790FC6FAEC3F88FB2C26E27 /* ScanBaseViewController.swift */; }; + 41366AB64512BB7E4248A904 /* OcrDDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0B91384C63CBFB790C8EE6 /* OcrDDUtils.swift */; }; + 41F2E4475B9B19350C31F255 /* PaymentCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C8AB382B81DE27C8614F6D /* PaymentCard.swift */; }; + 420FF119A7147335D802AB14 /* CardImageVerificationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F09D21BAC44C461B1AD7CB /* CardImageVerificationSheet.swift */; }; + 43AA83BFF8DEFC09D406669F /* CardVerifyStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3242DC2FCDF20D51D23AE1 /* CardVerifyStateMachine.swift */; }; + 449DD2A5D1F3FF94524D3CD6 /* STPAPIClient+CardImageVerificationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC4F647EB512813078B9A6F /* STPAPIClient+CardImageVerificationTest.swift */; }; + 45C8D17FED02529B7FC4E8C3 /* STPLocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6418F87D5168A2A08D65A482 /* STPLocalizedString.swift */; }; + 47DCE237223D40B1EB183704 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA2733CEC85F8E427CA99CB /* AppState.swift */; }; + 48580A7E92CC8EBFD2FD4C91 /* DetectedAllOcrBoxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1441A7A39C36C2A634A882F5 /* DetectedAllOcrBoxes.swift */; }; + 48E4577B073DD9485BC62902 /* ImageCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA128D69F3C32A4EF67CF0 /* ImageCompressionTests.swift */; }; + 499BDA59FD973003028A867B /* iOSSnapshotTestCase in Frameworks */ = {isa = PBXBuildFile; productRef = F3D550995F9421BED1BE23A2 /* iOSSnapshotTestCase */; }; + 49E1959C680AD68D1D345D6B /* DeviceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB03A9253E8FAD07B739438 /* DeviceUtils.swift */; }; + 4ACBD9754F304CE4F70CAE43 /* MainLoopStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D550382D6320388A5BA75A1 /* MainLoopStateMachine.swift */; }; + 4B01B9F4FB647908AA5276E2 /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17FA90EE37CF1E6A49532491 /* StripeCoreTestUtils.framework */; }; + 4D1016654AFB3D9EE26092B7 /* PredictionUtilOcr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795E739553651C8AC40E6AC1 /* PredictionUtilOcr.swift */; }; + 52519CA3928967768049164E /* CardImageVerification_CardSet_200.json in Resources */ = {isa = PBXBuildFile; fileRef = BDD0C14472FA163BD395DBD3 /* CardImageVerification_CardSet_200.json */; }; + 5382B471868AA7D95BBD2B3A /* SSDOcrDetect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AEB912151CB564AE21D073C /* SSDOcrDetect.swift */; }; + 54469A1A5D77BAD54061BEA1 /* ScanStatsPayload+Tasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1CD398A6AFB6747D095C59 /* ScanStatsPayload+Tasks.swift */; }; + 5488DA1817A0C9F780EF69B2 /* CardVerifyFraudData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FA7635BAFDC7DD54B1A731 /* CardVerifyFraudData.swift */; }; + 59889C85D6D25B3222776B28 /* synthetic_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4D9820BC2FFB68C808FB9507 /* synthetic_test_image.jpg */; }; + 5B1749A19231B20E2A081EB9 /* OcrObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC83A169A82484B9F60E452 /* OcrObject.swift */; }; + 60AF52B9EFFF12EBABE4D221 /* CreditCardOcrImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B28BBCB062B6AE5A3B7CBC /* CreditCardOcrImplementation.swift */; }; + 65C1EB5447CD5DF162CDEC2B /* Bouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA16EAC68E04D4FDE67DB807 /* Bouncer.swift */; }; + 6E9908C612ECD2E0AEA3DFF8 /* AtomicPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B548B9626366BB0F110873 /* AtomicPropertyWrapper.swift */; }; + 70B5FF75EF5DC8FFB23B95F7 /* UxModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C038EFADF52B2477FC934EE0 /* UxModelTests.swift */; }; + 74330F51A91DC3A617F7AE42 /* SimpleScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E1DA01CB00CBDE36735EDD /* SimpleScanViewController.swift */; }; + 77443628E02F3AEFF112314D /* CardScanMockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746AAB9327BDE6791F9393B0 /* CardScanMockData.swift */; }; + 77BB4BE6E5E03626936453C6 /* STPAPIClient+CardImageVerification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E54DFA3C47D94639297588 /* STPAPIClient+CardImageVerification.swift */; }; + 79A96C88970E4E61BAC47412 /* ScanStatsPayloadAPIBindingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9ADC4CC0B4ADFE0A5427E /* ScanStatsPayloadAPIBindingsTests.swift */; }; + 7B0CF214778E17FEC721602E /* PredictionResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE2CADB8D8E98D6EEE10C0 /* PredictionResult.swift */; }; + 7C71166DACDB829F8CBC383D /* NMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD9FF5C9DD64BE92DB0927D /* NMS.swift */; }; + 7CED1B42C94A49D6BF98C9E4 /* VideoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42510E09D32547455CDDBEF /* VideoFeed.swift */; }; + 7D9C9C2A26EF11F0C5D67D7C /* CGrect+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD87F9971C25C4B99138318F /* CGrect+utils.swift */; }; + 8463F2DB039C481D26A24BAA /* PostDetectionAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9F1B20AEB433DE3CE0042C /* PostDetectionAlgorithm.swift */; }; + 84975E58102A5D8C62C6C4E5 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A2DB0837C11D8829C027CF /* String+Localized.swift */; }; + 858CDA751309CA04202B6203 /* MachineLearningResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF17FD18E52CB3508AD895E3 /* MachineLearningResult.swift */; }; + 86635536450EE7D583B8BD59 /* StripeCore+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A1E35C6DF336F34B6C7AEA /* StripeCore+Import.swift */; }; + 8CF112F4889C96617504A931 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6C27E58C1953533C68D43361 /* Localizable.strings */; }; + 8E4B117272F5B17A1E6AD609 /* UxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF173627F545C88CB1551B0 /* UxModel.swift */; }; + 8FC373E15C1169677F563901 /* OcrPriorsGen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED780E8B4C14EE39F853B3A1 /* OcrPriorsGen.swift */; }; + 9188179F13E5EA15E0419580 /* ScanStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A49708CF5D37C7CB267732 /* ScanStats.swift */; }; + 93D4785D8B91F12B6DDDE203 /* UxAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F85031ADAE5928D977465C2 /* UxAnalyzer.swift */; }; + 9FA5A312032FC54B35BB604E /* SoftNMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A93BFDEE4072A2A9C30397 /* SoftNMS.swift */; }; + A060350B93E3369463AE2898 /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E889051D7CD76D4FB9084A1 /* StripeCore.framework */; }; + A7014CF97250D97BC683FAC8 /* CreditCardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8735A24B634F3B9060CADE45 /* CreditCardUtils.swift */; }; + A7F92FB7213E66A7D15A1BE3 /* SSDOcr.mlmodelc in Resources */ = {isa = PBXBuildFile; fileRef = E73DC7DD9C3E3395DF3F5A29 /* SSDOcr.mlmodelc */; }; + A8D0A57687A7CF9F389D7BDB /* ScanAnalyticsManager+Managers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A337FE5C3BE4FAB001E5520 /* ScanAnalyticsManager+Managers.swift */; }; + AB21982DC43977754F237756 /* VerificationFramesData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C2345660A95FBF90BF4850 /* VerificationFramesData.swift */; }; + ACC3C1A295E43983F67F858E /* FrameData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C926149A434F4F3BC961D9 /* FrameData.swift */; }; + AE144927F21D5DF9A8C0EF79 /* ScannedCardDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15541EEEE784EECDD3E7399D /* ScannedCardDetails.swift */; }; + AFA334F5007A4C141A96FC2E /* VerifyFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FAA99A64BCD00A336A5E01 /* VerifyFrames.swift */; }; + AFB2482D559BCC08E96C3615 /* CardImageVerificationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5004FC0706B2660B4C6EA0 /* CardImageVerificationIntent.swift */; }; + B00954CF5F2A15B72CB94FA5 /* VerifyCardAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B38020E8E33CA893CBA6F3 /* VerifyCardAddViewController.swift */; }; + B2DF7862E5F80ACD8DEE9317 /* UxModel.mlmodelc in Resources */ = {isa = PBXBuildFile; fileRef = F4721524B6285C507681CC4D /* UxModel.mlmodelc */; }; + B47F583CE0F239881877E5D3 /* ZoomedInCGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A26712A38A91726C314329 /* ZoomedInCGImage.swift */; }; + B66697826FEA9FCE81DDD6F4 /* ActiveStateComputation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBC1F86A03CF9AAC6FE5825 /* ActiveStateComputation.swift */; }; + BDEA39DE5D3775AD2A4BFB6C /* CardImageVerificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24DEA029F94A359B7A571BB /* CardImageVerificationControllerTests.swift */; }; + BEE2BFA103DE985D057A0F9D /* ScanStatsPayload+Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028DA95BD9A0DA8AD69162CB /* ScanStatsPayload+Common.swift */; }; + BFFA3E8CE79BFFE47530FAF6 /* DetectedSSDBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2EF6799FCE0817D9E3A536B /* DetectedSSDBox.swift */; }; + C2C41F5E568BF767C8232E74 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E5A51A77928A2700E23321 /* XCTest.framework */; }; + C3B9A4A30443A38C2D17BE8E /* CardNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311A5A7EF789248E35BB1FEC /* CardNetwork.swift */; }; + C57BCF07EDF419D36ED4E690 /* ScanEventsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1FBA1B4676CC5E9F785D51 /* ScanEventsProtocol.swift */; }; + C5DBC36A41FB70C5DCCC97C7 /* SimpleScanViewController+Verify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F806DF921888A9657E2928 /* SimpleScanViewController+Verify.swift */; }; + C633115B02A0CE24AC55A747 /* OcrDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD9AF72B522A1E535A4EC81 /* OcrDD.swift */; }; + C7259DA76E6AB9BF53735D19 /* Array+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA457DF27DC45455B8E1345 /* Array+utils.swift */; }; + C7A941539DBCFA790DCDB0B0 /* NonNameWords.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96A85E323CFDA8BA35BC293 /* NonNameWords.swift */; }; + C8E2E98B7ED108804E0A0E24 /* SSDCreditCardOcr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B00395C6A2B14A6D56735B9 /* SSDCreditCardOcr.swift */; }; + CBA4FE649A3B21DAC3A61E5B /* CancellationReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C171A1336B7FB0C321D194 /* CancellationReason.swift */; }; + CC07F702B9EC043ACB0AC1E5 /* ScannedCardImageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE45D8AC7BBA5DC433A9383 /* ScannedCardImageData.swift */; }; + CCB7722FDB8E6E68E90F3D12 /* CreditCardOcrPrediction+expiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E72DD18B9FCB80ABEDE2689 /* CreditCardOcrPrediction+expiry.swift */; }; + CED42084C5FC46C17E357AD5 /* ScanAnalyticsManager+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97CA4D8286B196A49B8D535 /* ScanAnalyticsManager+Helpers.swift */; }; + D2030CA1B3B7DAF3229189AD /* Image+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE9C295E982F92DE2418AE1 /* Image+utils.swift */; }; + D27E02429E5DC8B28DFE8945 /* CardImageVerificationDetailsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256455DAEDCDDB1AEAF6A278 /* CardImageVerificationDetailsResponse.swift */; }; + D42EEFBB72CD0AB00C6B4728 /* String+Sha256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732D3AB4CE25CC5C26A352B1 /* String+Sha256.swift */; }; + D6F0E1C93997BB36AF0608C0 /* StringResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A72927DA8A946FDD03843DE /* StringResourceTests.swift */; }; + DB74787AF4EEF0E506DD0E1C /* Data+Sha256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63662689AD097C859B8759B1 /* Data+Sha256.swift */; }; + DBE3EE5DAEEEA44D6C7ECD4E /* SSDOcr+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505DF34D76A56FEC2B8453F9 /* SSDOcr+Utils.swift */; }; + DDBCE05A3794129A9A04D511 /* AppleCreditCardOcr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDF39435211B456B7579739 /* AppleCreditCardOcr.swift */; }; + DF352D12199B55B659A11795 /* DetectedSSDOcrBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D58BEEC9D88537E32260CBA /* DetectedSSDOcrBox.swift */; }; + E43FE03D43CB0D7BAA73745A /* StripeCardScan.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD6884D0B3347BBF2E61B1D6 /* StripeCardScan.framework */; }; + E4EC278027AF802C39C74C48 /* CardImageVerificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B313BA373EF6B567BF40A240 /* CardImageVerificationController.swift */; }; + E70A7E38A7D858031F900649 /* CardScanSheetError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4C0BF8E55D53B004FABCA /* CardScanSheetError.swift */; }; + E90CC9715EC99F6B7FAC98FA /* CardImageVerificationSheetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545E03D5167516D28C8A9F08 /* CardImageVerificationSheetConfiguration.swift */; }; + EA0F179DF887D94E19138A5E /* AsyncModelLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF53C194DDC96160505D54D5 /* AsyncModelLoading.swift */; }; + EA2DBA78722CD65ED599190D /* CardScanMisc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3AD0FDF25BE452AE4ED34 /* CardScanMisc.swift */; }; + EB061FA550AFFE2AB2E348DF /* ScanAnalyticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69E2630C0FC7C9DDD551FD67 /* ScanAnalyticsManager.swift */; }; + EE1CB7D3FA2B44DC17E6FF4C /* OcrMainLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E38717F47ED06C115E058DE /* OcrMainLoop.swift */; }; + EF96103F82491640651C49F6 /* AppInfoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362A27BB81840A22104C9A49 /* AppInfoUtils.swift */; }; + F0ED2BFE143DD07337D389E5 /* CornerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E012270975B6DC09A8199F /* CornerView.swift */; }; + F16FAC158C6B0A7687547B4B /* FadeInAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CAFFFE55918BB4939868B6 /* FadeInAnimation.swift */; }; + F4E4941F5AA2EC2C2CC4F7FB /* StripeCardScanBundleLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F97A4CE5104309B720D603 /* StripeCardScanBundleLocator.swift */; }; + F9F19A6F2245863FEA485335 /* CreditCardOcrResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE92803C69CE8253D73A25F /* CreditCardOcrResult.swift */; }; + FE55286899CFB0E34356D4D6 /* StrictModeFramesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6D4DB87B26D439686392AE /* StrictModeFramesTest.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F7C4E731844D6B46A4FBCACD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A6BF50E5B02355004AC6020 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03CF79975A56288F02F20E52; + remoteInfo = StripeCardScan; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 20B092D23CA44561EF997447 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 451E97BA25D9CE29EDAB7618 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00A93BFDEE4072A2A9C30397 /* SoftNMS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftNMS.swift; sourceTree = ""; }; + 0197A1615C3FA86907CB6186 /* StripeCardScan-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeCardScan-Debug.xcconfig"; sourceTree = ""; }; + 028DA95BD9A0DA8AD69162CB /* ScanStatsPayload+Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScanStatsPayload+Common.swift"; sourceTree = ""; }; + 03E2C12C0D172DD7DAE33399 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + 048ED08A6959593F896B3AC8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 05B28BBCB062B6AE5A3B7CBC /* CreditCardOcrImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardOcrImplementation.swift; sourceTree = ""; }; + 06F97A4CE5104309B720D603 /* StripeCardScanBundleLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeCardScanBundleLocator.swift; sourceTree = ""; }; + 091163ED5E37922332F502B1 /* ImageHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHelpers.swift; sourceTree = ""; }; + 0B00395C6A2B14A6D56735B9 /* SSDCreditCardOcr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSDCreditCardOcr.swift; sourceTree = ""; }; + 0BB35800FB840EFC76240DC7 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + 0C7BAE904C7E1D7A9FDF35F1 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 0D1FBA1B4676CC5E9F785D51 /* ScanEventsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanEventsProtocol.swift; sourceTree = ""; }; + 0DC83A169A82484B9F60E452 /* OcrObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcrObject.swift; sourceTree = ""; }; + 10311DC6AC19B7A73A8F930F /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; + 10A26712A38A91726C314329 /* ZoomedInCGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomedInCGImage.swift; sourceTree = ""; }; + 11D3465E527ABADBD2485A93 /* ms-MY */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ms-MY"; path = "ms-MY.lproj/Localizable.strings"; sourceTree = ""; }; + 12F09D21BAC44C461B1AD7CB /* CardImageVerificationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationSheet.swift; sourceTree = ""; }; + 1441A7A39C36C2A634A882F5 /* DetectedAllOcrBoxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedAllOcrBoxes.swift; sourceTree = ""; }; + 14795A45279E8F328AB54920 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + 14BC8B1C28C3F6C8330164E0 /* CardScanSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScanSheet.swift; sourceTree = ""; }; + 14E5A51A77928A2700E23321 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 15541EEEE784EECDD3E7399D /* ScannedCardDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannedCardDetails.swift; sourceTree = ""; }; + 17BCBB8B34EBA904BB245CBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 17FA90EE37CF1E6A49532491 /* StripeCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A337FE5C3BE4FAB001E5520 /* ScanAnalyticsManager+Managers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScanAnalyticsManager+Managers.swift"; sourceTree = ""; }; + 1C94EE6DB0E3114A38DD9CFE /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = ""; }; + 1FD9AF72B522A1E535A4EC81 /* OcrDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcrDD.swift; sourceTree = ""; }; + 236AE86CE7DCB0DFA3B7C978 /* CardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardType.swift; sourceTree = ""; }; + 24C2345660A95FBF90BF4850 /* VerificationFramesData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationFramesData.swift; sourceTree = ""; }; + 256455DAEDCDDB1AEAF6A278 /* CardImageVerificationDetailsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationDetailsResponse.swift; sourceTree = ""; }; + 2667E0E0C7DC6CDAF03F8B40 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; + 27F4BF66273070C2054BBAC0 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = ""; }; + 27F6ECCDC721E4D46DAC4F48 /* ScanAnalyticsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanAnalyticsManagerTests.swift; sourceTree = ""; }; + 2D61E754E520B4E21920D1DA /* ScanStatsPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStatsPayload.swift; sourceTree = ""; }; + 2E889051D7CD76D4FB9084A1 /* StripeCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F3E329C37141862BD4A4DE7 /* SSDOcr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSDOcr.swift; sourceTree = ""; }; + 2F9447632965B9BCE1E595E4 /* StripeCardScanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripeCardScanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 311A5A7EF789248E35BB1FEC /* CardNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardNetwork.swift; sourceTree = ""; }; + 313F5F7C2B0BE5BE00BD98A9 /* Docs.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Docs.docc; sourceTree = ""; }; + 34F806DF921888A9657E2928 /* SimpleScanViewController+Verify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SimpleScanViewController+Verify.swift"; sourceTree = ""; }; + 362A27BB81840A22104C9A49 /* AppInfoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoUtils.swift; sourceTree = ""; }; + 3938F9E3F0F267045D3FDDEB /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 3A9C2B8A3B96E194CED6841C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 3BC4F647EB512813078B9A6F /* STPAPIClient+CardImageVerificationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+CardImageVerificationTest.swift"; sourceTree = ""; }; + 3DEE8C18A3CECFF0751AB0D5 /* CreditCardOcrPrediction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardOcrPrediction.swift; sourceTree = ""; }; + 3E24CF248F312DDE025D662A /* AppleOcr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleOcr.swift; sourceTree = ""; }; + 42454BA950282D9ADB9C6557 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 4301B281F5E0A5BACBBD68CC /* VerifyFramesAPIBindingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyFramesAPIBindingsTests.swift; sourceTree = ""; }; + 45C9A1A26405DEAD8E11F13D /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + 4A1CD398A6AFB6747D095C59 /* ScanStatsPayload+Tasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScanStatsPayload+Tasks.swift"; sourceTree = ""; }; + 4B452EE8B96AF1B0691E7D43 /* PredictionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionAPI.swift; sourceTree = ""; }; + 4B89D81A4099CC71A3FBC133 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + 4BBE2CADB8D8E98D6EEE10C0 /* PredictionResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionResult.swift; sourceTree = ""; }; + 4CA3AD0FDF25BE452AE4ED34 /* CardScanMisc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScanMisc.swift; sourceTree = ""; }; + 4D9820BC2FFB68C808FB9507 /* synthetic_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = synthetic_test_image.jpg; sourceTree = ""; }; + 4DA27FFBD426C871C5BD254E /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; + 4F85031ADAE5928D977465C2 /* UxAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UxAnalyzer.swift; sourceTree = ""; }; + 505DF34D76A56FEC2B8453F9 /* SSDOcr+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SSDOcr+Utils.swift"; sourceTree = ""; }; + 53C171A1336B7FB0C321D194 /* CancellationReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationReason.swift; sourceTree = ""; }; + 545E03D5167516D28C8A9F08 /* CardImageVerificationSheetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationSheetConfiguration.swift; sourceTree = ""; }; + 58A5F2CEEAAB78AA425A5737 /* lt-LT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lt-LT"; path = "lt-LT.lproj/Localizable.strings"; sourceTree = ""; }; + 5969433D88B5BD3B7F56B2BD /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; + 5A6D5C8E51C23370E889F75F /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + 5A72927DA8A946FDD03843DE /* StringResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringResourceTests.swift; sourceTree = ""; }; + 5CF9ADC4CC0B4ADFE0A5427E /* ScanStatsPayloadAPIBindingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStatsPayloadAPIBindingsTests.swift; sourceTree = ""; }; + 5D58BEEC9D88537E32260CBA /* DetectedSSDOcrBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedSSDOcrBox.swift; sourceTree = ""; }; + 5E72DD18B9FCB80ABEDE2689 /* CreditCardOcrPrediction+expiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CreditCardOcrPrediction+expiry.swift"; sourceTree = ""; }; + 6186A0AAB96E236DEF0B5B79 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; + 63662689AD097C859B8759B1 /* Data+Sha256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Sha256.swift"; sourceTree = ""; }; + 63FA7635BAFDC7DD54B1A731 /* CardVerifyFraudData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVerifyFraudData.swift; sourceTree = ""; }; + 6418F87D5168A2A08D65A482 /* STPLocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPLocalizedString.swift; sourceTree = ""; }; + 64A1E35C6DF336F34B6C7AEA /* StripeCore+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StripeCore+Import.swift"; sourceTree = ""; }; + 6563E5CE5CD0439D5DBF35D7 /* ScanAnalyticsManager+Tasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScanAnalyticsManager+Tasks.swift"; sourceTree = ""; }; + 66A49708CF5D37C7CB267732 /* ScanStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStats.swift; sourceTree = ""; }; + 67C8AB382B81DE27C8614F6D /* PaymentCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentCard.swift; sourceTree = ""; }; + 69A2DB0837C11D8829C027CF /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; + 69B38020E8E33CA893CBA6F3 /* VerifyCardAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyCardAddViewController.swift; sourceTree = ""; }; + 69E2630C0FC7C9DDD551FD67 /* ScanAnalyticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanAnalyticsManager.swift; sourceTree = ""; }; + 6BEC906101069A1E3C50FA51 /* ErrorCorrection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCorrection.swift; sourceTree = ""; }; + 6C3EE6315FB1507E40F10D85 /* ScannedCardImageData+Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScannedCardImageData+Verification.swift"; sourceTree = ""; }; + 6EBC1F86A03CF9AAC6FE5825 /* ActiveStateComputation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveStateComputation.swift; sourceTree = ""; }; + 6F41FBE3D90B5F6518E42051 /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Localizable.strings; sourceTree = ""; }; + 732D3AB4CE25CC5C26A352B1 /* String+Sha256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Sha256.swift"; sourceTree = ""; }; + 746AAB9327BDE6791F9393B0 /* CardScanMockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScanMockData.swift; sourceTree = ""; }; + 7633166B4D9096FE995F08F0 /* CGRectExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtension.swift; sourceTree = ""; }; + 76C70F9A1879ED8B41639945 /* ScannedCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannedCard.swift; sourceTree = ""; }; + 77D0D8C2909455B76E246468 /* InterfaceOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceOrientation.swift; sourceTree = ""; }; + 795E739553651C8AC40E6AC1 /* PredictionUtilOcr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionUtilOcr.swift; sourceTree = ""; }; + 79A802EA46840CD45CBECEEA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 7D41E45A285FEF48E9528997 /* CardScanFraudData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScanFraudData.swift; sourceTree = ""; }; + 7D550382D6320388A5BA75A1 /* MainLoopStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoopStateMachine.swift; sourceTree = ""; }; + 7DBB233BD36CBAE2700A4511 /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + 7DDAF8E33A3D7CD020AC904A /* CardImageVerificationDetailsResponseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationDetailsResponseTest.swift; sourceTree = ""; }; + 7E38717F47ED06C115E058DE /* OcrMainLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcrMainLoop.swift; sourceTree = ""; }; + 8094FE7CCD3CDA8B914EC534 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + 84E54DFA3C47D94639297588 /* STPAPIClient+CardImageVerification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+CardImageVerification.swift"; sourceTree = ""; }; + 8735A24B634F3B9060CADE45 /* CreditCardUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardUtils.swift; sourceTree = ""; }; + 87448BAE438EF08C55AA4B29 /* UxAndOcrMainLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UxAndOcrMainLoop.swift; sourceTree = ""; }; + 88F89EC392AFBE060336B63F /* sl-SI */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sl-SI"; path = "sl-SI.lproj/Localizable.strings"; sourceTree = ""; }; + 89BBC06EC107C8BDEF79BB3D /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + 8AEB912151CB564AE21D073C /* SSDOcrDetect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSDOcrDetect.swift; sourceTree = ""; }; + 8C227CA761586744976C952D /* nn-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nn-NO"; path = "nn-NO.lproj/Localizable.strings"; sourceTree = ""; }; + 8CA457DF27DC45455B8E1345 /* Array+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+utils.swift"; sourceTree = ""; }; + 8EDF39435211B456B7579739 /* AppleCreditCardOcr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleCreditCardOcr.swift; sourceTree = ""; }; + 8FB602C9E909E31BAE6703D6 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/Localizable.strings"; sourceTree = ""; }; + 938958FCB9B918AD6CDF0F4E /* BlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = ""; }; + 9593D6788C4DBF554735E2B6 /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mt; path = mt.lproj/Localizable.strings; sourceTree = ""; }; + 96F3DDAD14A9F2DEB98E236E /* Expiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expiry.swift; sourceTree = ""; }; + 9A256BCB7822DD94572DDA07 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 9B3242DC2FCDF20D51D23AE1 /* CardVerifyStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVerifyStateMachine.swift; sourceTree = ""; }; + 9C3890F6A5A4FF68F95746DC /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; + 9E4CF9A676A86169070DD54D /* ro-RO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ro-RO"; path = "ro-RO.lproj/Localizable.strings"; sourceTree = ""; }; + A2EF6799FCE0817D9E3A536B /* DetectedSSDBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedSSDBox.swift; sourceTree = ""; }; + A3C926149A434F4F3BC961D9 /* FrameData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameData.swift; sourceTree = ""; }; + A612DDE1110EA970BF69B33D /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + A67EDE2BCCC081DEDD684AAC /* StripeiOS Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Release.xcconfig"; sourceTree = ""; }; + A6E1DA01CB00CBDE36735EDD /* SimpleScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleScanViewController.swift; sourceTree = ""; }; + A790FC6FAEC3F88FB2C26E27 /* ScanBaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanBaseViewController.swift; sourceTree = ""; }; + A7EE5FDCA3B075FE5BBAF42A /* DetectedAllBoxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedAllBoxes.swift; sourceTree = ""; }; + AB56DDBBF0E5BC10682C7D96 /* StripeCardScan.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeCardScan.h; sourceTree = ""; }; + AB64890F80D91D9D7B92197D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + ABB38869FD175D7BD6BB3890 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + AC9F1B20AEB433DE3CE0042C /* PostDetectionAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetectionAlgorithm.swift; sourceTree = ""; }; + ACD9FF5C9DD64BE92DB0927D /* NMS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NMS.swift; sourceTree = ""; }; + ADCB34B81919043FA35E8BC3 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + AEE9C295E982F92DE2418AE1 /* Image+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+utils.swift"; sourceTree = ""; }; + AF17FD18E52CB3508AD895E3 /* MachineLearningResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachineLearningResult.swift; sourceTree = ""; }; + B0A7946C3A1E943FB72F3FB7 /* bg-BG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bg-BG"; path = "bg-BG.lproj/Localizable.strings"; sourceTree = ""; }; + B2D9886B0ED119E8D1FB6448 /* UIImage+pixelBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+pixelBuffer.swift"; sourceTree = ""; }; + B313BA373EF6B567BF40A240 /* CardImageVerificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationController.swift; sourceTree = ""; }; + B3CAFFFE55918BB4939868B6 /* FadeInAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeInAnimation.swift; sourceTree = ""; }; + B53D407C0AC8D44ECC0C3C52 /* DetectedBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedBox.swift; sourceTree = ""; }; + B97CA4D8286B196A49B8D535 /* ScanAnalyticsManager+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScanAnalyticsManager+Helpers.swift"; sourceTree = ""; }; + BAE45D8AC7BBA5DC433A9383 /* ScannedCardImageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannedCardImageData.swift; sourceTree = ""; }; + BC0B91384C63CBFB790C8EE6 /* OcrDDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcrDDUtils.swift; sourceTree = ""; }; + BD87F9971C25C4B99138318F /* CGrect+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGrect+utils.swift"; sourceTree = ""; }; + BDD0C14472FA163BD395DBD3 /* CardImageVerification_CardSet_200.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CardImageVerification_CardSet_200.json; sourceTree = ""; }; + BEDE71D7051DDC98698C4057 /* CardBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBase.swift; sourceTree = ""; }; + BEEE30227F7D34546F0FBB58 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + C038EFADF52B2477FC934EE0 /* UxModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UxModelTests.swift; sourceTree = ""; }; + C264A676DA41344551BA6661 /* StripeCardScan-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeCardScan-Release.xcconfig"; sourceTree = ""; }; + C3E012270975B6DC09A8199F /* CornerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerView.swift; sourceTree = ""; }; + C827434739027B2DD43449EA /* Torch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Torch.swift; sourceTree = ""; }; + CA16EAC68E04D4FDE67DB807 /* Bouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bouncer.swift; sourceTree = ""; }; + CB48DDCA458A62DCE342F517 /* et-EE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "et-EE"; path = "et-EE.lproj/Localizable.strings"; sourceTree = ""; }; + CD5004FC0706B2660B4C6EA0 /* CardImageVerificationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationIntent.swift; sourceTree = ""; }; + CD6D4DB87B26D439686392AE /* StrictModeFramesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrictModeFramesTest.swift; sourceTree = ""; }; + CDB03A9253E8FAD07B739438 /* DeviceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtils.swift; sourceTree = ""; }; + CF3FCD1B2283A2B02EBAEF4F /* UxModel+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UxModel+Utils.swift"; sourceTree = ""; }; + CF90067085243D998A1D6030 /* CardImageVerification_CardAdd_200.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CardImageVerification_CardAdd_200.json; sourceTree = ""; }; + D0AA128D69F3C32A4EF67CF0 /* ImageCompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompressionTests.swift; sourceTree = ""; }; + D42510E09D32547455CDDBEF /* VideoFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFeed.swift; sourceTree = ""; }; + D532C440ADCD017AD326299B /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = ""; }; + D9B548B9626366BB0F110873 /* AtomicPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicPropertyWrapper.swift; sourceTree = ""; }; + DA3ECBF071E7507C9D41F232 /* EndToEndTestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndToEndTestDataSource.swift; sourceTree = ""; }; + DC12B6E0657203AAB765468C /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; + DCA2733CEC85F8E427CA99CB /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + DCF4C0BF8E55D53B004FABCA /* CardScanSheetError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScanSheetError.swift; sourceTree = ""; }; + DD6884D0B3347BBF2E61B1D6 /* StripeCardScan.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCardScan.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DDF173627F545C88CB1551B0 /* UxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UxModel.swift; sourceTree = ""; }; + E4FAA99A64BCD00A336A5E01 /* VerifyFrames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyFrames.swift; sourceTree = ""; }; + E73DC7DD9C3E3395DF3F5A29 /* SSDOcr.mlmodelc */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SSDOcr.mlmodelc; sourceTree = ""; }; + EA89DA7FF10BC3A7B2D102FB /* SSDOcrOutputExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSDOcrOutputExtensions.swift; sourceTree = ""; }; + ED780E8B4C14EE39F853B3A1 /* OcrPriorsGen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OcrPriorsGen.swift; sourceTree = ""; }; + EDE92803C69CE8253D73A25F /* CreditCardOcrResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardOcrResult.swift; sourceTree = ""; }; + EF53C194DDC96160505D54D5 /* AsyncModelLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncModelLoading.swift; sourceTree = ""; }; + F1FCC855CCB0816414A4BEC1 /* StripeiOS Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS Tests-Debug.xcconfig"; sourceTree = ""; }; + F24DEA029F94A359B7A571BB /* CardImageVerificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageVerificationControllerTests.swift; sourceTree = ""; }; + F3AF32D7DF7EE1420D16291D /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; + F4721524B6285C507681CC4D /* UxModel.mlmodelc */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UxModel.mlmodelc; sourceTree = ""; }; + F87D884128F85E7F16BCBA9B /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + F912323832888F88EA218BA9 /* VerifyCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyCardViewController.swift; sourceTree = ""; }; + F96A85E323CFDA8BA35BC293 /* NonNameWords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonNameWords.swift; sourceTree = ""; }; + FD40E90452A83FFBBF65C8D8 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = ""; }; + FF9618F7B15B33EC5B339A94 /* ScanConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanConfiguration.swift; sourceTree = ""; }; + FFE89E65E767D03B92383C76 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1639B69C00B8C21CEBF08A55 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2C41F5E568BF767C8232E74 /* XCTest.framework in Frameworks */, + E43FE03D43CB0D7BAA73745A /* StripeCardScan.framework in Frameworks */, + 4B01B9F4FB647908AA5276E2 /* StripeCoreTestUtils.framework in Frameworks */, + 499BDA59FD973003028A867B /* iOSSnapshotTestCase in Frameworks */, + 2F10596629118185A3A8F011 /* OHHTTPStubs in Frameworks */, + 12C11D2633C7819DF275CCC2 /* OHHTTPStubsSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 29BDDDCECDEC71451BAAB324 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A060350B93E3369463AE2898 /* StripeCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 01BEE24ED60FE104004E8F2C /* CompiledModels */ = { + isa = PBXGroup; + children = ( + E73DC7DD9C3E3395DF3F5A29 /* SSDOcr.mlmodelc */, + F4721524B6285C507681CC4D /* UxModel.mlmodelc */, + ); + path = CompiledModels; + sourceTree = ""; + }; + 0DFCF5271EC90615420288B3 /* Api */ = { + isa = PBXGroup; + children = ( + C8B14FA3DC29B84F382E6D1F /* Models */, + 256455DAEDCDDB1AEAF6A278 /* CardImageVerificationDetailsResponse.swift */, + 84E54DFA3C47D94639297588 /* STPAPIClient+CardImageVerification.swift */, + ); + path = Api; + sourceTree = ""; + }; + 0E245ED03746B2F8EE73B5F9 /* StripeCardScanTests */ = { + isa = PBXGroup; + children = ( + C8CC6DB5A60043E8A373BC6F /* Helpers */, + 40C830CD6973E7AD722C2213 /* Mock Data */, + 3172AE79E1A187D904BD1022 /* Resources */, + A35971ECF566C2769A608A06 /* Unit */, + 048ED08A6959593F896B3AC8 /* Info.plist */, + ); + path = StripeCardScanTests; + sourceTree = ""; + }; + 10507D99D0E81A181A25B4BC /* MLModels */ = { + isa = PBXGroup; + children = ( + EF53C194DDC96160505D54D5 /* AsyncModelLoading.swift */, + 2F3E329C37141862BD4A4DE7 /* SSDOcr.swift */, + 505DF34D76A56FEC2B8453F9 /* SSDOcr+Utils.swift */, + DDF173627F545C88CB1551B0 /* UxModel.swift */, + CF3FCD1B2283A2B02EBAEF4F /* UxModel+Utils.swift */, + ); + path = MLModels; + sourceTree = ""; + }; + 105B9420C58B9406F7939324 = { + isa = PBXGroup; + children = ( + 36C8C7335EC9F37DE4446270 /* Project */, + D275033F1B615793C2F8B8E0 /* Frameworks */, + 72B7637B9160BBA39556BA0A /* Products */, + ); + sourceTree = ""; + }; + 20CA55CAA02DC2833AD1C426 /* ML Models */ = { + isa = PBXGroup; + children = ( + C038EFADF52B2477FC934EE0 /* UxModelTests.swift */, + ); + path = "ML Models"; + sourceTree = ""; + }; + 25575C2DBB0C8AAA658EC26E /* JSON */ = { + isa = PBXGroup; + children = ( + CF90067085243D998A1D6030 /* CardImageVerification_CardAdd_200.json */, + BDD0C14472FA163BD395DBD3 /* CardImageVerification_CardSet_200.json */, + ); + path = JSON; + sourceTree = ""; + }; + 3172AE79E1A187D904BD1022 /* Resources */ = { + isa = PBXGroup; + children = ( + 4D9820BC2FFB68C808FB9507 /* synthetic_test_image.jpg */, + ); + path = Resources; + sourceTree = ""; + }; + 3228566B727B9D25D13A3BAB /* Resources */ = { + isa = PBXGroup; + children = ( + 01BEE24ED60FE104004E8F2C /* CompiledModels */, + 6A6547B8AC5A170A0092DCF5 /* Localizations */, + ); + path = Resources; + sourceTree = ""; + }; + 324D2CF5354B3283BC0C49ED /* CardVerify */ = { + isa = PBXGroup; + children = ( + 0DFCF5271EC90615420288B3 /* Api */, + BC0070EA31577F5573D7B787 /* Card Image Verification */, + 69D6875F84C12C2E18B96B01 /* Helpers */, + CA16EAC68E04D4FDE67DB807 /* Bouncer.swift */, + BEDE71D7051DDC98698C4057 /* CardBase.swift */, + 7D41E45A285FEF48E9528997 /* CardScanFraudData.swift */, + 4CA3AD0FDF25BE452AE4ED34 /* CardScanMisc.swift */, + 63FA7635BAFDC7DD54B1A731 /* CardVerifyFraudData.swift */, + 9B3242DC2FCDF20D51D23AE1 /* CardVerifyStateMachine.swift */, + B3CAFFFE55918BB4939868B6 /* FadeInAnimation.swift */, + A3C926149A434F4F3BC961D9 /* FrameData.swift */, + 67C8AB382B81DE27C8614F6D /* PaymentCard.swift */, + 34F806DF921888A9657E2928 /* SimpleScanViewController+Verify.swift */, + 06F97A4CE5104309B720D603 /* StripeCardScanBundleLocator.swift */, + 4F85031ADAE5928D977465C2 /* UxAnalyzer.swift */, + 87448BAE438EF08C55AA4B29 /* UxAndOcrMainLoop.swift */, + 69B38020E8E33CA893CBA6F3 /* VerifyCardAddViewController.swift */, + F912323832888F88EA218BA9 /* VerifyCardViewController.swift */, + 10A26712A38A91726C314329 /* ZoomedInCGImage.swift */, + ); + path = CardVerify; + sourceTree = ""; + }; + 36C8C7335EC9F37DE4446270 /* Project */ = { + isa = PBXGroup; + children = ( + 79C01535652A2F89EB22D9A9 /* BuildConfigurations */, + 6C3738C5D8A82E9A3C5AFC85 /* BuildConfigurations */, + DF93066F033C46A7B100FC82 /* StripeCardScan */, + 0E245ED03746B2F8EE73B5F9 /* StripeCardScanTests */, + ); + name = Project; + sourceTree = ""; + }; + 3C728AAC6B6FF615A064C757 /* CardUtils */ = { + isa = PBXGroup; + children = ( + 311A5A7EF789248E35BB1FEC /* CardNetwork.swift */, + 236AE86CE7DCB0DFA3B7C978 /* CardType.swift */, + 8735A24B634F3B9060CADE45 /* CreditCardUtils.swift */, + 96F3DDAD14A9F2DEB98E236E /* Expiry.swift */, + ); + path = CardUtils; + sourceTree = ""; + }; + 40C830CD6973E7AD722C2213 /* Mock Data */ = { + isa = PBXGroup; + children = ( + 25575C2DBB0C8AAA658EC26E /* JSON */, + ); + path = "Mock Data"; + sourceTree = ""; + }; + 620D5B89096A8B2B2202E6EB /* CreditCardOcr */ = { + isa = PBXGroup; + children = ( + 8EDF39435211B456B7579739 /* AppleCreditCardOcr.swift */, + 05B28BBCB062B6AE5A3B7CBC /* CreditCardOcrImplementation.swift */, + 3DEE8C18A3CECFF0751AB0D5 /* CreditCardOcrPrediction.swift */, + EDE92803C69CE8253D73A25F /* CreditCardOcrResult.swift */, + 6BEC906101069A1E3C50FA51 /* ErrorCorrection.swift */, + AF17FD18E52CB3508AD895E3 /* MachineLearningResult.swift */, + 7D550382D6320388A5BA75A1 /* MainLoopStateMachine.swift */, + F96A85E323CFDA8BA35BC293 /* NonNameWords.swift */, + 7E38717F47ED06C115E058DE /* OcrMainLoop.swift */, + 0DC83A169A82484B9F60E452 /* OcrObject.swift */, + 0B00395C6A2B14A6D56735B9 /* SSDCreditCardOcr.swift */, + ); + path = CreditCardOcr; + sourceTree = ""; + }; + 66F259BA8091A6C5D28C4963 /* MLRuntime */ = { + isa = PBXGroup; + children = ( + 6EBC1F86A03CF9AAC6FE5825 /* ActiveStateComputation.swift */, + DCA2733CEC85F8E427CA99CB /* AppState.swift */, + A7EE5FDCA3B075FE5BBAF42A /* DetectedAllBoxes.swift */, + 1441A7A39C36C2A634A882F5 /* DetectedAllOcrBoxes.swift */, + B53D407C0AC8D44ECC0C3C52 /* DetectedBox.swift */, + A2EF6799FCE0817D9E3A536B /* DetectedSSDBox.swift */, + 5D58BEEC9D88537E32260CBA /* DetectedSSDOcrBox.swift */, + ACD9FF5C9DD64BE92DB0927D /* NMS.swift */, + 1FD9AF72B522A1E535A4EC81 /* OcrDD.swift */, + BC0B91384C63CBFB790C8EE6 /* OcrDDUtils.swift */, + ED780E8B4C14EE39F853B3A1 /* OcrPriorsGen.swift */, + AC9F1B20AEB433DE3CE0042C /* PostDetectionAlgorithm.swift */, + 4B452EE8B96AF1B0691E7D43 /* PredictionAPI.swift */, + 4BBE2CADB8D8E98D6EEE10C0 /* PredictionResult.swift */, + 795E739553651C8AC40E6AC1 /* PredictionUtilOcr.swift */, + 00A93BFDEE4072A2A9C30397 /* SoftNMS.swift */, + 8AEB912151CB564AE21D073C /* SSDOcrDetect.swift */, + EA89DA7FF10BC3A7B2D102FB /* SSDOcrOutputExtensions.swift */, + ); + path = MLRuntime; + sourceTree = ""; + }; + 69D6875F84C12C2E18B96B01 /* Helpers */ = { + isa = PBXGroup; + children = ( + DA3ECBF071E7507C9D41F232 /* EndToEndTestDataSource.swift */, + 6418F87D5168A2A08D65A482 /* STPLocalizedString.swift */, + 69A2DB0837C11D8829C027CF /* String+Localized.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 6A6547B8AC5A170A0092DCF5 /* Localizations */ = { + isa = PBXGroup; + children = ( + 6C27E58C1953533C68D43361 /* Localizable.strings */, + ); + path = Localizations; + sourceTree = ""; + }; + 6C3738C5D8A82E9A3C5AFC85 /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + 14795A45279E8F328AB54920 /* Project-Debug.xcconfig */, + 7DBB233BD36CBAE2700A4511 /* Project-Release.xcconfig */, + F1FCC855CCB0816414A4BEC1 /* StripeiOS Tests-Debug.xcconfig */, + A67EDE2BCCC081DEDD684AAC /* StripeiOS Tests-Release.xcconfig */, + ); + name = BuildConfigurations; + path = ../BuildConfigurations; + sourceTree = ""; + }; + 72B7637B9160BBA39556BA0A /* Products */ = { + isa = PBXGroup; + children = ( + DD6884D0B3347BBF2E61B1D6 /* StripeCardScan.framework */, + 2F9447632965B9BCE1E595E4 /* StripeCardScanTests.xctest */, + 2E889051D7CD76D4FB9084A1 /* StripeCore.framework */, + 17FA90EE37CF1E6A49532491 /* StripeCoreTestUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + 79C01535652A2F89EB22D9A9 /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + 0197A1615C3FA86907CB6186 /* StripeCardScan-Debug.xcconfig */, + C264A676DA41344551BA6661 /* StripeCardScan-Release.xcconfig */, + ); + path = BuildConfigurations; + sourceTree = ""; + }; + 8AA53DD0D15C5BC1040EE334 /* Scan Analytics */ = { + isa = PBXGroup; + children = ( + 2D61E754E520B4E21920D1DA /* ScanStatsPayload.swift */, + 028DA95BD9A0DA8AD69162CB /* ScanStatsPayload+Common.swift */, + 4A1CD398A6AFB6747D095C59 /* ScanStatsPayload+Tasks.swift */, + ); + path = "Scan Analytics"; + sourceTree = ""; + }; + 9AE51241602B3092F45811E8 /* Extensions */ = { + isa = PBXGroup; + children = ( + 8CA457DF27DC45455B8E1345 /* Array+utils.swift */, + BD87F9971C25C4B99138318F /* CGrect+utils.swift */, + 7633166B4D9096FE995F08F0 /* CGRectExtension.swift */, + 5E72DD18B9FCB80ABEDE2689 /* CreditCardOcrPrediction+expiry.swift */, + AEE9C295E982F92DE2418AE1 /* Image+utils.swift */, + B2D9886B0ED119E8D1FB6448 /* UIImage+pixelBuffer.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 9FDF572663A7F311B8506E13 /* CardScan */ = { + isa = PBXGroup; + children = ( + A1C38CDC3470D8E7A9F39671 /* AppleOcr */, + 3C728AAC6B6FF615A064C757 /* CardUtils */, + 620D5B89096A8B2B2202E6EB /* CreditCardOcr */, + 9AE51241602B3092F45811E8 /* Extensions */, + 10507D99D0E81A181A25B4BC /* MLModels */, + 66F259BA8091A6C5D28C4963 /* MLRuntime */, + E2C9F4FF2603FAF1BD4567B4 /* UI */, + E86A9678BB840BD3EA2AEB94 /* Utils */, + ); + path = CardScan; + sourceTree = ""; + }; + A1C38CDC3470D8E7A9F39671 /* AppleOcr */ = { + isa = PBXGroup; + children = ( + 3E24CF248F312DDE025D662A /* AppleOcr.swift */, + ); + path = AppleOcr; + sourceTree = ""; + }; + A35971ECF566C2769A608A06 /* Unit */ = { + isa = PBXGroup; + children = ( + D6C5CA3B4336E2024BFB5037 /* API Bindings */, + 20CA55CAA02DC2833AD1C426 /* ML Models */, + F24DEA029F94A359B7A571BB /* CardImageVerificationControllerTests.swift */, + 7DDAF8E33A3D7CD020AC904A /* CardImageVerificationDetailsResponseTest.swift */, + D0AA128D69F3C32A4EF67CF0 /* ImageCompressionTests.swift */, + 27F6ECCDC721E4D46DAC4F48 /* ScanAnalyticsManagerTests.swift */, + CD6D4DB87B26D439686392AE /* StrictModeFramesTest.swift */, + 5A72927DA8A946FDD03843DE /* StringResourceTests.swift */, + ); + path = Unit; + sourceTree = ""; + }; + BC0070EA31577F5573D7B787 /* Card Image Verification */ = { + isa = PBXGroup; + children = ( + 53C171A1336B7FB0C321D194 /* CancellationReason.swift */, + B313BA373EF6B567BF40A240 /* CardImageVerificationController.swift */, + CD5004FC0706B2660B4C6EA0 /* CardImageVerificationIntent.swift */, + 12F09D21BAC44C461B1AD7CB /* CardImageVerificationSheet.swift */, + 545E03D5167516D28C8A9F08 /* CardImageVerificationSheetConfiguration.swift */, + DCF4C0BF8E55D53B004FABCA /* CardScanSheetError.swift */, + 69E2630C0FC7C9DDD551FD67 /* ScanAnalyticsManager.swift */, + B97CA4D8286B196A49B8D535 /* ScanAnalyticsManager+Helpers.swift */, + 1A337FE5C3BE4FAB001E5520 /* ScanAnalyticsManager+Managers.swift */, + 6563E5CE5CD0439D5DBF35D7 /* ScanAnalyticsManager+Tasks.swift */, + 76C70F9A1879ED8B41639945 /* ScannedCard.swift */, + BAE45D8AC7BBA5DC433A9383 /* ScannedCardImageData.swift */, + 6C3EE6315FB1507E40F10D85 /* ScannedCardImageData+Verification.swift */, + 64A1E35C6DF336F34B6C7AEA /* StripeCore+Import.swift */, + ); + path = "Card Image Verification"; + sourceTree = ""; + }; + C8B14FA3DC29B84F382E6D1F /* Models */ = { + isa = PBXGroup; + children = ( + 8AA53DD0D15C5BC1040EE334 /* Scan Analytics */, + 24C2345660A95FBF90BF4850 /* VerificationFramesData.swift */, + E4FAA99A64BCD00A336A5E01 /* VerifyFrames.swift */, + ); + path = Models; + sourceTree = ""; + }; + C8CC6DB5A60043E8A373BC6F /* Helpers */ = { + isa = PBXGroup; + children = ( + 746AAB9327BDE6791F9393B0 /* CardScanMockData.swift */, + 63662689AD097C859B8759B1 /* Data+Sha256.swift */, + 091163ED5E37922332F502B1 /* ImageHelpers.swift */, + 15541EEEE784EECDD3E7399D /* ScannedCardDetails.swift */, + 732D3AB4CE25CC5C26A352B1 /* String+Sha256.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + D275033F1B615793C2F8B8E0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 14E5A51A77928A2700E23321 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D6C5CA3B4336E2024BFB5037 /* API Bindings */ = { + isa = PBXGroup; + children = ( + 5CF9ADC4CC0B4ADFE0A5427E /* ScanStatsPayloadAPIBindingsTests.swift */, + 3BC4F647EB512813078B9A6F /* STPAPIClient+CardImageVerificationTest.swift */, + 4301B281F5E0A5BACBBD68CC /* VerifyFramesAPIBindingsTests.swift */, + ); + path = "API Bindings"; + sourceTree = ""; + }; + DF93066F033C46A7B100FC82 /* StripeCardScan */ = { + isa = PBXGroup; + children = ( + 313F5F7C2B0BE5BE00BD98A9 /* Docs.docc */, + 3228566B727B9D25D13A3BAB /* Resources */, + F28CFA0B72751113E838303F /* Source */, + 17BCBB8B34EBA904BB245CBD /* Info.plist */, + AB56DDBBF0E5BC10682C7D96 /* StripeCardScan.h */, + ); + path = StripeCardScan; + sourceTree = ""; + }; + E2C9F4FF2603FAF1BD4567B4 /* UI */ = { + isa = PBXGroup; + children = ( + 938958FCB9B918AD6CDF0F4E /* BlurView.swift */, + 14BC8B1C28C3F6C8330164E0 /* CardScanSheet.swift */, + C3E012270975B6DC09A8199F /* CornerView.swift */, + 77D0D8C2909455B76E246468 /* InterfaceOrientation.swift */, + DC12B6E0657203AAB765468C /* PreviewView.swift */, + A790FC6FAEC3F88FB2C26E27 /* ScanBaseViewController.swift */, + FF9618F7B15B33EC5B339A94 /* ScanConfiguration.swift */, + 0D1FBA1B4676CC5E9F785D51 /* ScanEventsProtocol.swift */, + 66A49708CF5D37C7CB267732 /* ScanStats.swift */, + A6E1DA01CB00CBDE36735EDD /* SimpleScanViewController.swift */, + C827434739027B2DD43449EA /* Torch.swift */, + D42510E09D32547455CDDBEF /* VideoFeed.swift */, + ); + path = UI; + sourceTree = ""; + }; + E86A9678BB840BD3EA2AEB94 /* Utils */ = { + isa = PBXGroup; + children = ( + 362A27BB81840A22104C9A49 /* AppInfoUtils.swift */, + D9B548B9626366BB0F110873 /* AtomicPropertyWrapper.swift */, + CDB03A9253E8FAD07B739438 /* DeviceUtils.swift */, + ); + path = Utils; + sourceTree = ""; + }; + F28CFA0B72751113E838303F /* Source */ = { + isa = PBXGroup; + children = ( + 9FDF572663A7F311B8506E13 /* CardScan */, + 324D2CF5354B3283BC0C49ED /* CardVerify */, + ); + path = Source; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + B4E70071212998150E28FF9A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B10222A5121C9B2C3479FAB /* StripeCardScan.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 03CF79975A56288F02F20E52 /* StripeCardScan */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3942B170A6D222D5B5128339 /* Build configuration list for PBXNativeTarget "StripeCardScan" */; + buildPhases = ( + B4E70071212998150E28FF9A /* Headers */, + 44F5D31B0E7A7BFCB9425841 /* Sources */, + 7FD545F62DB7C0787E9CD12D /* Resources */, + 451E97BA25D9CE29EDAB7618 /* Embed Frameworks */, + 29BDDDCECDEC71451BAAB324 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StripeCardScan; + productName = StripeCardScan; + productReference = DD6884D0B3347BBF2E61B1D6 /* StripeCardScan.framework */; + productType = "com.apple.product-type.framework"; + }; + DCC6FFCFBE0B51B33F4CBB26 /* StripeCardScanTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 89AE00D6B3B382DD3D3A3C70 /* Build configuration list for PBXNativeTarget "StripeCardScanTests" */; + buildPhases = ( + 201682B08469C4AC551E9BF8 /* Sources */, + 54119F2010F36CB483CA88CC /* Resources */, + 20B092D23CA44561EF997447 /* Embed Frameworks */, + 1639B69C00B8C21CEBF08A55 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 82DAA855443FCAA25F64C5C0 /* PBXTargetDependency */, + ); + name = StripeCardScanTests; + packageProductDependencies = ( + F3D550995F9421BED1BE23A2 /* iOSSnapshotTestCase */, + C96A49C871FDB73B36A91670 /* OHHTTPStubs */, + EA26D443783FE045B0D7888D /* OHHTTPStubsSwift */, + ); + productName = StripeCardScanTests; + productReference = 2F9447632965B9BCE1E595E4 /* StripeCardScanTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9A6BF50E5B02355004AC6020 /* Project object */ = { + isa = PBXProject; + attributes = { + }; + buildConfigurationList = CBB5D42795303F47D9D5F2F5 /* Build configuration list for PBXProject "StripeCardScan" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + "bg-BG", + "ca-ES", + "cs-CZ", + da, + de, + "el-GR", + en, + "en-GB", + es, + "es-419", + "et-EE", + fi, + fil, + fr, + "fr-CA", + hr, + hu, + id, + it, + ja, + ko, + "lt-LT", + "lv-LV", + "ms-MY", + mt, + nb, + nl, + "nn-NO", + "pl-PL", + "pt-BR", + "pt-PT", + "ro-RO", + ru, + "sk-SK", + "sl-SI", + sv, + tr, + vi, + "zh-HK", + "zh-Hans", + "zh-Hant", + ); + mainGroup = 105B9420C58B9406F7939324; + packageReferences = ( + 3B10FCF0EE5D8ADC4672F64E /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, + 1F624C14E7802E9748883013 /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */, + ); + productRefGroup = 72B7637B9160BBA39556BA0A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 03CF79975A56288F02F20E52 /* StripeCardScan */, + DCC6FFCFBE0B51B33F4CBB26 /* StripeCardScanTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 54119F2010F36CB483CA88CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0D62200AA65D71F040037CC8 /* CardImageVerification_CardAdd_200.json in Resources */, + 52519CA3928967768049164E /* CardImageVerification_CardSet_200.json in Resources */, + 59889C85D6D25B3222776B28 /* synthetic_test_image.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7FD545F62DB7C0787E9CD12D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A7F92FB7213E66A7D15A1BE3 /* SSDOcr.mlmodelc in Resources */, + B2DF7862E5F80ACD8DEE9317 /* UxModel.mlmodelc in Resources */, + 8CF112F4889C96617504A931 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 201682B08469C4AC551E9BF8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 77443628E02F3AEFF112314D /* CardScanMockData.swift in Sources */, + DB74787AF4EEF0E506DD0E1C /* Data+Sha256.swift in Sources */, + 3F5B9466E9FC13CA29218750 /* ImageHelpers.swift in Sources */, + AE144927F21D5DF9A8C0EF79 /* ScannedCardDetails.swift in Sources */, + D42EEFBB72CD0AB00C6B4728 /* String+Sha256.swift in Sources */, + 449DD2A5D1F3FF94524D3CD6 /* STPAPIClient+CardImageVerificationTest.swift in Sources */, + 79A96C88970E4E61BAC47412 /* ScanStatsPayloadAPIBindingsTests.swift in Sources */, + 25FAF64D4FE93BF38E807ECA /* VerifyFramesAPIBindingsTests.swift in Sources */, + BDEA39DE5D3775AD2A4BFB6C /* CardImageVerificationControllerTests.swift in Sources */, + 19EF4634631CFC121DE21D1B /* CardImageVerificationDetailsResponseTest.swift in Sources */, + 48E4577B073DD9485BC62902 /* ImageCompressionTests.swift in Sources */, + 70B5FF75EF5DC8FFB23B95F7 /* UxModelTests.swift in Sources */, + 0D062E99363099A5728F3DCD /* ScanAnalyticsManagerTests.swift in Sources */, + FE55286899CFB0E34356D4D6 /* StrictModeFramesTest.swift in Sources */, + D6F0E1C93997BB36AF0608C0 /* StringResourceTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 44F5D31B0E7A7BFCB9425841 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A9ECCF086685AD607D82492 /* AppleOcr.swift in Sources */, + C3B9A4A30443A38C2D17BE8E /* CardNetwork.swift in Sources */, + 0ADD10809FEABDCB59A8FA72 /* CardType.swift in Sources */, + A7014CF97250D97BC683FAC8 /* CreditCardUtils.swift in Sources */, + 050B462804602A9F4DB58A9D /* Expiry.swift in Sources */, + DDBCE05A3794129A9A04D511 /* AppleCreditCardOcr.swift in Sources */, + 60AF52B9EFFF12EBABE4D221 /* CreditCardOcrImplementation.swift in Sources */, + 1AD72764EA8BCB66A9D7B637 /* CreditCardOcrPrediction.swift in Sources */, + F9F19A6F2245863FEA485335 /* CreditCardOcrResult.swift in Sources */, + 17E34C9EF882AEAE47BECAB4 /* ErrorCorrection.swift in Sources */, + 858CDA751309CA04202B6203 /* MachineLearningResult.swift in Sources */, + 4ACBD9754F304CE4F70CAE43 /* MainLoopStateMachine.swift in Sources */, + C7A941539DBCFA790DCDB0B0 /* NonNameWords.swift in Sources */, + EE1CB7D3FA2B44DC17E6FF4C /* OcrMainLoop.swift in Sources */, + 5B1749A19231B20E2A081EB9 /* OcrObject.swift in Sources */, + C8E2E98B7ED108804E0A0E24 /* SSDCreditCardOcr.swift in Sources */, + C7259DA76E6AB9BF53735D19 /* Array+utils.swift in Sources */, + 10E840B00703A92CC487B324 /* CGRectExtension.swift in Sources */, + 7D9C9C2A26EF11F0C5D67D7C /* CGrect+utils.swift in Sources */, + CCB7722FDB8E6E68E90F3D12 /* CreditCardOcrPrediction+expiry.swift in Sources */, + D2030CA1B3B7DAF3229189AD /* Image+utils.swift in Sources */, + 1B71815D5B3EC8AC21E2A202 /* UIImage+pixelBuffer.swift in Sources */, + EA0F179DF887D94E19138A5E /* AsyncModelLoading.swift in Sources */, + DBE3EE5DAEEEA44D6C7ECD4E /* SSDOcr+Utils.swift in Sources */, + 213A91107E76434972A1F848 /* SSDOcr.swift in Sources */, + 189DEBAF38F2FB88CCA39EA2 /* UxModel+Utils.swift in Sources */, + 8E4B117272F5B17A1E6AD609 /* UxModel.swift in Sources */, + B66697826FEA9FCE81DDD6F4 /* ActiveStateComputation.swift in Sources */, + 47DCE237223D40B1EB183704 /* AppState.swift in Sources */, + 040020B8558B0489DF99F8B5 /* DetectedAllBoxes.swift in Sources */, + 48580A7E92CC8EBFD2FD4C91 /* DetectedAllOcrBoxes.swift in Sources */, + 20DF2E9D5A543360DF3A4DDA /* DetectedBox.swift in Sources */, + BFFA3E8CE79BFFE47530FAF6 /* DetectedSSDBox.swift in Sources */, + DF352D12199B55B659A11795 /* DetectedSSDOcrBox.swift in Sources */, + 7C71166DACDB829F8CBC383D /* NMS.swift in Sources */, + C633115B02A0CE24AC55A747 /* OcrDD.swift in Sources */, + 41366AB64512BB7E4248A904 /* OcrDDUtils.swift in Sources */, + 8FC373E15C1169677F563901 /* OcrPriorsGen.swift in Sources */, + 8463F2DB039C481D26A24BAA /* PostDetectionAlgorithm.swift in Sources */, + 0838FEA7071EDCE1B633F093 /* PredictionAPI.swift in Sources */, + 7B0CF214778E17FEC721602E /* PredictionResult.swift in Sources */, + 4D1016654AFB3D9EE26092B7 /* PredictionUtilOcr.swift in Sources */, + 5382B471868AA7D95BBD2B3A /* SSDOcrDetect.swift in Sources */, + 35F2BB10395405DB6C1E31B5 /* SSDOcrOutputExtensions.swift in Sources */, + 9FA5A312032FC54B35BB604E /* SoftNMS.swift in Sources */, + 164BF41B5C522B7338376BB2 /* BlurView.swift in Sources */, + 3736756FBB875060C86E2777 /* CardScanSheet.swift in Sources */, + F0ED2BFE143DD07337D389E5 /* CornerView.swift in Sources */, + 20DCEB955D9E0660EAB3ABEB /* InterfaceOrientation.swift in Sources */, + 30E3E90F9C8E3D3E8FCA869E /* PreviewView.swift in Sources */, + 3FCC183583090FD0246D9336 /* ScanBaseViewController.swift in Sources */, + 0D8CA0E3EFF9694CAE4FB8F7 /* ScanConfiguration.swift in Sources */, + C57BCF07EDF419D36ED4E690 /* ScanEventsProtocol.swift in Sources */, + 9188179F13E5EA15E0419580 /* ScanStats.swift in Sources */, + 74330F51A91DC3A617F7AE42 /* SimpleScanViewController.swift in Sources */, + 0EAA2314FEA8D05D24C498BD /* Torch.swift in Sources */, + 7CED1B42C94A49D6BF98C9E4 /* VideoFeed.swift in Sources */, + EF96103F82491640651C49F6 /* AppInfoUtils.swift in Sources */, + 6E9908C612ECD2E0AEA3DFF8 /* AtomicPropertyWrapper.swift in Sources */, + 49E1959C680AD68D1D345D6B /* DeviceUtils.swift in Sources */, + D27E02429E5DC8B28DFE8945 /* CardImageVerificationDetailsResponse.swift in Sources */, + BEE2BFA103DE985D057A0F9D /* ScanStatsPayload+Common.swift in Sources */, + 54469A1A5D77BAD54061BEA1 /* ScanStatsPayload+Tasks.swift in Sources */, + 19F7EC09C9B0ED11A4601E08 /* ScanStatsPayload.swift in Sources */, + AB21982DC43977754F237756 /* VerificationFramesData.swift in Sources */, + AFA334F5007A4C141A96FC2E /* VerifyFrames.swift in Sources */, + 77BB4BE6E5E03626936453C6 /* STPAPIClient+CardImageVerification.swift in Sources */, + 65C1EB5447CD5DF162CDEC2B /* Bouncer.swift in Sources */, + CBA4FE649A3B21DAC3A61E5B /* CancellationReason.swift in Sources */, + E4EC278027AF802C39C74C48 /* CardImageVerificationController.swift in Sources */, + AFB2482D559BCC08E96C3615 /* CardImageVerificationIntent.swift in Sources */, + 420FF119A7147335D802AB14 /* CardImageVerificationSheet.swift in Sources */, + E90CC9715EC99F6B7FAC98FA /* CardImageVerificationSheetConfiguration.swift in Sources */, + E70A7E38A7D858031F900649 /* CardScanSheetError.swift in Sources */, + CED42084C5FC46C17E357AD5 /* ScanAnalyticsManager+Helpers.swift in Sources */, + A8D0A57687A7CF9F389D7BDB /* ScanAnalyticsManager+Managers.swift in Sources */, + 0A4DCE2C98659B92DDB29B9A /* ScanAnalyticsManager+Tasks.swift in Sources */, + EB061FA550AFFE2AB2E348DF /* ScanAnalyticsManager.swift in Sources */, + 1DE075A700592250D87DF558 /* ScannedCard.swift in Sources */, + 05188062E522359CE24912CF /* ScannedCardImageData+Verification.swift in Sources */, + CC07F702B9EC043ACB0AC1E5 /* ScannedCardImageData.swift in Sources */, + 86635536450EE7D583B8BD59 /* StripeCore+Import.swift in Sources */, + 36525A0F774E7FA055D8B525 /* CardBase.swift in Sources */, + 2D042FD2E8A0C8236596CE54 /* CardScanFraudData.swift in Sources */, + EA2DBA78722CD65ED599190D /* CardScanMisc.swift in Sources */, + 5488DA1817A0C9F780EF69B2 /* CardVerifyFraudData.swift in Sources */, + 43AA83BFF8DEFC09D406669F /* CardVerifyStateMachine.swift in Sources */, + F16FAC158C6B0A7687547B4B /* FadeInAnimation.swift in Sources */, + ACC3C1A295E43983F67F858E /* FrameData.swift in Sources */, + 2F3FA5E8CCBF7F77106A8268 /* EndToEndTestDataSource.swift in Sources */, + 313F5F7D2B0BE5BE00BD98A9 /* Docs.docc in Sources */, + 45C8D17FED02529B7FC4E8C3 /* STPLocalizedString.swift in Sources */, + 84975E58102A5D8C62C6C4E5 /* String+Localized.swift in Sources */, + 41F2E4475B9B19350C31F255 /* PaymentCard.swift in Sources */, + C5DBC36A41FB70C5DCCC97C7 /* SimpleScanViewController+Verify.swift in Sources */, + F4E4941F5AA2EC2C2CC4F7FB /* StripeCardScanBundleLocator.swift in Sources */, + 93D4785D8B91F12B6DDDE203 /* UxAnalyzer.swift in Sources */, + 18B63245A933E292345C9410 /* UxAndOcrMainLoop.swift in Sources */, + B00954CF5F2A15B72CB94FA5 /* VerifyCardAddViewController.swift in Sources */, + 06711423AB563634EADFCCC0 /* VerifyCardViewController.swift in Sources */, + B47F583CE0F239881877E5D3 /* ZoomedInCGImage.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 82DAA855443FCAA25F64C5C0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = StripeCardScan; + target = 03CF79975A56288F02F20E52 /* StripeCardScan */; + targetProxy = F7C4E731844D6B46A4FBCACD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 6C27E58C1953533C68D43361 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + B0A7946C3A1E943FB72F3FB7 /* bg-BG */, + 8FB602C9E909E31BAE6703D6 /* ca-ES */, + 27F4BF66273070C2054BBAC0 /* cs-CZ */, + 4B89D81A4099CC71A3FBC133 /* da */, + FFE89E65E767D03B92383C76 /* de */, + D532C440ADCD017AD326299B /* el-GR */, + BEEE30227F7D34546F0FBB58 /* en */, + 6186A0AAB96E236DEF0B5B79 /* en-GB */, + 79A802EA46840CD45CBECEEA /* es */, + 5A6D5C8E51C23370E889F75F /* es-419 */, + CB48DDCA458A62DCE342F517 /* et-EE */, + 03E2C12C0D172DD7DAE33399 /* fi */, + 6F41FBE3D90B5F6518E42051 /* fil */, + 3A9C2B8A3B96E194CED6841C /* fr */, + F3AF32D7DF7EE1420D16291D /* fr-CA */, + 9C3890F6A5A4FF68F95746DC /* hr */, + 0C7BAE904C7E1D7A9FDF35F1 /* hu */, + 10311DC6AC19B7A73A8F930F /* id */, + ABB38869FD175D7BD6BB3890 /* it */, + 8094FE7CCD3CDA8B914EC534 /* ja */, + F87D884128F85E7F16BCBA9B /* ko */, + 58A5F2CEEAAB78AA425A5737 /* lt-LT */, + FD40E90452A83FFBBF65C8D8 /* lv-LV */, + 11D3465E527ABADBD2485A93 /* ms-MY */, + 9593D6788C4DBF554735E2B6 /* mt */, + 9A256BCB7822DD94572DDA07 /* nb */, + AB64890F80D91D9D7B92197D /* nl */, + 8C227CA761586744976C952D /* nn-NO */, + 1C94EE6DB0E3114A38DD9CFE /* pl-PL */, + 45C9A1A26405DEAD8E11F13D /* pt-BR */, + 2667E0E0C7DC6CDAF03F8B40 /* pt-PT */, + 9E4CF9A676A86169070DD54D /* ro-RO */, + 42454BA950282D9ADB9C6557 /* ru */, + 5969433D88B5BD3B7F56B2BD /* sk-SK */, + 88F89EC392AFBE060336B63F /* sl-SI */, + 89BBC06EC107C8BDEF79BB3D /* sv */, + 3938F9E3F0F267045D3FDDEB /* tr */, + 0BB35800FB840EFC76240DC7 /* vi */, + ADCB34B81919043FA35E8BC3 /* zh-Hans */, + A612DDE1110EA970BF69B33D /* zh-Hant */, + 4DA27FFBD426C871C5BD254E /* zh-HK */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5EA7D712D38589E2EE95AB3A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0197A1615C3FA86907CB6186 /* StripeCardScan-Debug.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeCardScan/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-card-scan"; + PRODUCT_NAME = StripeCardScan; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 7F230CA2FE5A1AE4D1DC64D7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7DBB233BD36CBAE2700A4511 /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + DCF5B81249E77660231925A7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F1FCC855CCB0816414A4BEC1 /* StripeiOS Tests-Debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCardScanTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCardScanTests; + PRODUCT_NAME = StripeCardScanTests; + SDKROOT = iphoneos; + }; + name = Debug; + }; + EDDF50C51DE9D6BEF2ACB9E5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C264A676DA41344551BA6661 /* StripeCardScan-Release.xcconfig */; + buildSettings = { + INFOPLIST_FILE = StripeCardScan/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = "com.stripe.stripe-card-scan"; + PRODUCT_NAME = StripeCardScan; + SDKROOT = iphoneos; + }; + name = Release; + }; + EE63655B6AEB11CB8934D19B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A67EDE2BCCC081DEDD684AAC /* StripeiOS Tests-Release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + INFOPLIST_FILE = StripeCardScanTests/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.stripe.StripeCardScanTests; + PRODUCT_NAME = StripeCardScanTests; + SDKROOT = iphoneos; + }; + name = Release; + }; + FBC92FF6082EA51664498AE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 14795A45279E8F328AB54920 /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3942B170A6D222D5B5128339 /* Build configuration list for PBXNativeTarget "StripeCardScan" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5EA7D712D38589E2EE95AB3A /* Debug */, + EDDF50C51DE9D6BEF2ACB9E5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 89AE00D6B3B382DD3D3A3C70 /* Build configuration list for PBXNativeTarget "StripeCardScanTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DCF5B81249E77660231925A7 /* Debug */, + EE63655B6AEB11CB8934D19B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CBB5D42795303F47D9D5F2F5 /* Build configuration list for PBXProject "StripeCardScan" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FBC92FF6082EA51664498AE4 /* Debug */, + 7F230CA2FE5A1AE4D1DC64D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1F624C14E7802E9748883013 /* XCRemoteSwiftPackageReference "ios-snapshot-test-case" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/uber/ios-snapshot-test-case"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; + 3B10FCF0EE5D8ADC4672F64E /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/davidme-stripe/OHHTTPStubs"; + requirement = { + branch = "stripe-mock"; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + C96A49C871FDB73B36A91670 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubs; + }; + EA26D443783FE045B0D7888D /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubsSwift; + }; + F3D550995F9421BED1BE23A2 /* iOSSnapshotTestCase */ = { + isa = XCSwiftPackageProductDependency; + productName = iOSSnapshotTestCase; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 9A6BF50E5B02355004AC6020 /* Project object */; +} diff --git a/StripeCardScan/StripeCardScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/StripeCardScan/StripeCardScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/StripeCardScan/StripeCardScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/StripeCardScan/StripeCardScan.xcodeproj/xcshareddata/xcschemes/StripeCardScan.xcscheme b/StripeCardScan/StripeCardScan.xcodeproj/xcshareddata/xcschemes/StripeCardScan.xcscheme new file mode 100644 index 00000000..b81b60a2 --- /dev/null +++ b/StripeCardScan/StripeCardScan.xcodeproj/xcshareddata/xcschemes/StripeCardScan.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StripeCardScan/StripeCardScan/Docs.docc/StripeCardScan.md b/StripeCardScan/StripeCardScan/Docs.docc/StripeCardScan.md new file mode 100644 index 00000000..48a428af --- /dev/null +++ b/StripeCardScan/StripeCardScan/Docs.docc/StripeCardScan.md @@ -0,0 +1,3 @@ +# ``StripeCardScan`` + +Placeholder \ No newline at end of file diff --git a/StripeCardScan/StripeCardScan/Info.plist b/StripeCardScan/StripeCardScan/Info.plist new file mode 100644 index 00000000..cd4a496b --- /dev/null +++ b/StripeCardScan/StripeCardScan/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/analytics/coremldata.bin b/StripeCardScan/StripeCardScan/Resources/CompiledModels/SSDOcr.mlmodelc/analytics/coremldata.bin new file mode 100644 index 0000000000000000000000000000000000000000..77dbb917577a88650119ccc74637b654826fab03 GIT binary patch literal 438 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�fBe;Cg} zU*oHs%l)6Cvv6yhZKL?c+?W6xFr+{wNnA(!Ss`P%FeM4k(wfHKFYtCE!!YW9~r> zgvKbFvsDop)+)<$yF$EpB*z$wr|AsMB!-8?0y~qrr3Jefo)X35%5OQZ%P7%Z7m) zG9ur=bgpa$K~ZkN9l zJNJU&!_c0ePjfGoo(_?zgZE`bJE_bOX}v!un)AUdAR zf_t0Z(oReNhz_=$vUW-Z-7)k4ID_#4V=V)@+`@sPKWZ`k4Lp&v-82MlDo&|AxZ!c_ zj0xB*azyc0zCCj~_eU!ezhBu?x=_+r(NcChZX;5NEFlMlD#(u_>3mvTQptaqTq!EO zKoe;uoyIzqZ(}TyRmN)CD%mfYfqfOzu})*Iu`WE3Yj19#RN)P5d%&IM{dg4rnf(nd z0Xkah?0X7?NZam|*&4?!g%%-Q!4~L3e!X&AX@G5^jLC^dhuXfdZs-%U4*pD4N?XxU z*3+WDq8GQ{x>q(H5=%pY6|#xqCVU(_m-)!80JDIW0L?8c8jI({z2R5vF!B^I0zPSa zUOE&A`_`hqYBAlUzGL%aQ@D|ElLk{1^Nq?a1YT-hA-^EhzX@B%KgY)NpJNr zGF6b671#>Ws^|rOhUVs9VxpNKi3Hoof94m9pU4uLF(t)Egf}vg--?`&Ei->YGs3e4 zqc~~h!7Movx&iLM_S*V$Cujy(q$Xnmgw2O|ay-p41<+vNBQrc=rZmrbLUOBgCNqK` ztiaIWcmmj36R2t!`Ti8%uu0 zvycy*lTIZmjxTfw3!r$#Pq2ipK`zUdAerDF$cS6R2h*G2M(CKRi^ZqdEXz17ur!M86Nwp)w#2Z>(5p6FR*APV!T ztOej4IIDtd+uf&*&_9uAdOlPqvWKi6cQYbl{Da$am}+h)Tw%Pck; z4Uc|uP-NVb(e`xf7M?V1;ew%#TTa89_eijXrWae&xN75H$xX`#=r{U77NBaPjupB= z?NFO=o$D^VLj*WOufG z@*gr9`GpO$-k^@d_t;jdv&cuOL3Px)5Prl5Gh>^Mp#G|7f{hUzGJ#a2foi~{0oP+R zW`MnKzFIgle0fY=#74ab28z@2W_W_U4^k=XDjbF{+A2!1kieKTpuy0^X5)|J9g)l2 zWtzlJvAdaR*k5Zma4Y1riu6=IN?2g3Qjm~9nWYDE-pWsM=lDRfLfm8x;}hh)Y#ZaA zKx0L|=MU5?#PzBX`oKrRWHCQ=*fn1xXeJpXN{CRTPd-&qvbg#$rt2Q@`BcPGim|SUc{v;yC_}*>Pw%*jq7ET7@C(NWgC!teObDh#OG)5;U^=NeQM! z)W9NG9`}o{&ksO5nyplGF?(|l=nuYZez|0|Ad)qOlu$$LRc5;K6kQ1?A^Q~wkikHh z-F}u@D6|1?V%My0>1NofERb#k#sbr*e)b<>I8uHLH8# z3v+IQ6HA`v*M&6lQA(?r@f{!34sXg==HF$zK-W-_GJze7eq@uv-n z9U2y=U|0wA2CKwvffW1AXz|D3CF4yfnd=Fol#E`f*c>gV*Fc?2bpTJvZftgJAGjyj z2g#8&5tYol{4nJ`=4RLhe0hEqz|jp^6Z}O!gzv>3ik8a)?7P8@Ye*h}t{}OxAigbDmp&u!$LFw8HX91!#Zv)38%T(2NsW;Hg0~=( zk^aynp;6p$k#ctvzsVI~hwu~`sq~bbAY1Wq(!89@Rsk&oy{au-L*rD@l@<)#j_VCt zut7y{;TUDLnUsNOHhP4MvXq)**vU~QkvBh)5!kNuDiBx1AOol|DwW{_9miF095R8e zwKOd1D|F-MQ%f!Dz-Bprm}IMwjaLj1K3QhQ8qDwEv*1*bVHc}9Cgi~<6n*Fk;AZNK zsjMIe)j?&ZwbCk~44urbgf^3%*`|iQ;)!lU+_3C(R+q9Dx!E>XN`q$=Fe^}@cmf6@ z3+0VsJchE84hEUxae>~HC~j!(Mz0`~HG*xh{5bkj^y22RP1IWHI&d1&1fszerB@)F z%tRZJuPt}-MhY%w3;Q+Vn6wo<1o?s7qBHpG*adQIq}^aBJ*zwwgGi3k-J=eIn~*2+ zmy`y5iw)XaVMxdBKoMXuz(oJ#+MsVkyBhBaiM02Gd)Vr6q#cVv7}vSt-J_9 zQ5(oZl)^k7jRracQPj$kAHZ7RE+Q*ksMNDZl>^PSIdZOAE}kF(71KHIt7RD1H?lp3 zAp$?&6oWoh>WofV3NxIlMLKdsoR`WGAjvoOx43=$E15*c$Que@A_Ihmu}`R>d}l6) z?+M)l8kT(BJ2kGBs%t6Zw8~fvz&G(zlp$j8CC*~_Jmj%zrEt2qE_*1hRtStE5ELo2 zTm{$1Mj$t!`!a_qDefNp#_$g4MyXt08jWds<8Y^Ea zf*G`Gd0*|?o;UrcsgK1){)y~4(kfhvHTkDC$iXYTDr#V5(&W_;6h}mSrF+FIcLHs| zxyti~*M%18I)>V(!iFIy;V2};w8y&0`W7^hPtg;6zo@t7WNsDt3Jf;V@SunhmN9IR zHI02`UV;rW+%*n}JHhQ^-+&)TksPP&8+UGQlI@v7i!CuTaVd!pXKs;JJl|$5L}pBTs@~WlKcP=T~wF zP{+2|{K7QGS#8mY?$$=^5;9tB%zmKQMK3z!r{1*J+al=E5!FzZas2jpE*Kn4J_oZ( zMky3Vyx_Xxr>qBA#rDgtM@|vef={A-mNm@bgCoUE0)Uv_zwIgYU1-nwNQb41ZJ5!DlE(;X)sV9d(U=6_9N@1K6GJG1uck8 zWBb`@g%JA0{4Zyxs+?J{PI_3H2Hqs|i=#wEyMZ~})&y*YjVS@e7Lup(6f(@X#V$1u zhO$G-@?S)30D59R>kgomGKw3GR-0eikM6E7PJ!cPdg^U{2>VcI82uL7#0G^(rI)aD zWm~eG5zrmnYbX}z$+%2=k>9{+)?seSwXsZ=R6~Kt1967c((%eR^cv}TDQLb$?Sy3f zq>y;3A73J~0!`61)CsDF?G1~kBJc(Gr1ZjpptwAx1^BI?p^tPje#4SYXNjpp2^7X> zD~1?st#rKu@>>1@{ ziosH;aIBem8r6XNLw<}iArt5y{0d|u7koKQTwC`Gb@>5V?OEH>Rn5o>?NbT|tLKt5X*DLRmO zh{W2N1_}nz8{+P;V}W?)f!VXC$Tptt0$-NwAkFkSrkH#c-H}~#Xb4P#|ALR=7+D}a zJ!ZLSw{Z(R6FhEVfpc7rRE6p2Cc>4HGxAk%*Vye~HZU@BIypr06k2Anq7jBn@QiVd zBOcj=HQ-=2L1u^Rz#I49wVnZ~;Bgd#ib*?BK?*ZnMP|vKD@)06W{PpG?M68 zXPO;l%+aavHF7IFS8|(N1im#7D%oT0AsNH7q*rz`)&b4qua-`yh5;Ys1bK-QJMPe2 zli4s;j+z!J8$gdET8eGpD;4$S^RVyO3-~zoA3g@?AGa1<5LG2|sZOKo*)@tH={j-a zIGOFig~5B{PKu7(1WCWR)8Hhnf5&qA-SXj)GN*rm0&6oZZY4Tyn*O*wY zF^a$^El0!+YbaI8b%J$*v|xpNvY7tZC(KY%(O0D%Sshnl$d(4u6C%fRA55zwpRzZ= zy5v@ym0fPBL3T6etb66Ns8sS{QI#Yc3&wrcBf0gVqx>W5!^lg(5W}^$SBw6kWNdGl z4(WxILB|R;a1ZO4us3WIw60-*sQL7eU$vE)IMWISD>!b2;H7YVBulwK)=W;J^Gmux z5(NR$NKZQ0vd4@mVin63>zKF5KqR;53z8s#4=#&a6%&jHDAL$s*ryB>NRjaPiY%8! zg0XOou$@{&o+=!mOe~y({bAl%H`=XVy84|d8e#0zY=okThIdFp4=N3Z9R zn))P}t&#W-=NOA)nn-?hO z(dV#NJ{}w|>Kp*q+vWpy;?q?pxZCJ^FxQd;^re=U99AB~&sfeeJw(=8J7JLZB#^7z zPF1RnumcR|C&dhqw@WgFtS60k27DK))%=X)9wvnS0GEle$W%d!<;CBEu&O3nGW!FZkg$st+7gCGjaRy z3a+`+Ciu|zWMliUxWW7nI1+fr%j_{w4_0fu4lhM@D^b>CeZ?Nyf)9eoU zOZx$=Ka^qS`6cK>`LM{ZGJnY+@%f1t?}-lWfXhvz#M9U$_K)Ef@P<5P8_E+RzwS4e zW2pibi;dH((OH&$=m)eJ`|{8W;~LN>e~cv|ZFk;_U8&qHTMBk!?~9p&!SZ)#dx~Ng zN|v&V6~l$El2>AzGT_txV8#Z^ls(``@J+%?vw_pzd~?S0595#?9+wq_&8xDo=Git zPBfkXTfueM*JOgNf;6cLs5k}zvr*7&M4ws@0cqyW*j9NDxLyIoWhr0qM%hQvk6EX9 zE8ea6H~R)-;Wg6Gm?UmoY^Yr>>d3WxnDwLpGBCW9{K=k!XIR?6Pw}DlLI~Vb7f6YI zEnh526P+V<;MJnDBG>o}Yc6_2Hi#`49M>eY4K*KruPDP8w%?$zh-zObx4;gGPw-pt z9lX`{mQkSDrN4p6l7+_eXqBqYUYi|b4`6ZZC#VnJR47Mwg7^7jWHY9b$kA&9ZsY=0 zabj}yF^l94VzzD|{0_~aFzEvNmK7Ez(h_WH;WcoXbPstszYqHx9;u#4epEiQMj-9% zo9JJp&aOpHP@ED(>za*X^Tb-9BR3;_RY4W_UL!*{P%|Un$w}1-Jij1{Nr7Xrcl-pL zG&)9(X4yl6O%#oP5QPcbqrPCPy*^)K1-yzJU$aK+r(hLUv2& zVcTuy1UXiD2K6a>%Ez=F$G)_t0G;50Y+>w?xG~}5k#E?1ZZEb6c5#!q(bNd?p}L1@ z4|@czAUn|kLa)NDnybh>cmQ;l?rdm||HrH_1sdv^1E{AZWkyoiAv6Y0vpZqfaLKX~ z`y!k*Rf$)53Ee5Og8!IaC{kD(e^PoCB+XyIAi52t1y=(t`3lPk92Z^xAo@J4I&Oer zIdq58q1tvsOEF5_t|eL~;PN@>82NmW1(XE)CD+0S2`$7^bbWR|(v3a}o3ELelMQR<#98LXQSCMogDwb&!ePh0Iy)=V)pWsWtU{M zZFgj?qAIp6tccXdUEt!8P`Eo-3oYgNE2+!61hPOto`z@l;##l z8655vt5!<*lk!aNIM*uj4fPl}Xnmll173kva_4|Mz;q-V>0Xjtl!&OI4)(9ujkx#l zeQ9&ZByCj8a6^H3ZjHFL#ke-MHqdj{A-kpQ9Wy&lCD6(Zu-mZ490L`=LoFfPaaknJ zTNc2%*h?WMBG>$!y~o~@9S<4EB|vSdX~j?J_kx|OBkaeHK}K_`#fV*Jrw0Fxn~`7LlTE?k}&5e5_R zNrpt4#&lz3ky2AU)kb*0?4anlvG7hhfHTvxrR%UyFo~R#?`89puMaI2w*qxbrDAUB zBldunaT1%290x2E2Ff?F=j9Vcrfe!O9SSl&f;h_6k}tkizLD_kBqan3F|-?F@3QS6sH)hcxwNFTwp(v1E@%_f9Xoq zG=5v$X;vLm$fbc3O#+JGYng}a6YEb~f5^`!n?_2fBFjXo?_T^ftffy2%h{j&Ec6Y~ z9^<(gTs1JwIvr~n5rJpgp7OD1}xuK{8KjqMtC zJ8}`!nvG)q6*NGexhoyYJ`|_ZYtp~SbIStYaonVX`$cl%c4;>9kuSF$vwz7;KJ*Px zo6QAHEtud%3)v^K-C}O_RAezQ-8qJ2FazJ7Qh+1*HgI?8A$|e80pDOADNPi&Nu8{x z%+KLfz-_K?dqc@5x!C^4Het8M)u9OTT;xJ)iI~|sj_LT-!gis7sgY$897s-UyPi)J zJF|j-2vL9gvRgwpwfzdk_ zkp9Kql;CSW_&554YvTSHLA0D}^3PS2)%^R0sOfllyuUAho0u5P*7Q9&G`*?iSSs1p zOuYYmn)pkcBZB|-@IB`%YBv2V)cn4=-LDa{iQN~&YIYs(N6b^^61z@IYAP4Cs@Ya| z4UrnunXsC-5RZ3GO>0J8NIQ9UyMJe=JmS~e_eAFQ^+eT}9n}L<9fZF<>|b1RFnwRg zWMb^O-@ZpP7y34|Q4qe7JBe90{`dzqEc4BsKBYRNOT+Yj$JW%W{M(pNpY5O4@H1T}H9fW)i9hXo z6VJc|VpoT%nlr^O{S)_gCZZ>S>GfJK^TX%&_%@B&M?^i$C!(5kAdc=#Co+e}5x>to z@g4av-Z!ve8{%iXN}@6@!$0Zj1!DI1fST%!i~aN7QN)C_J$~!u{b@r-wy62~E2gGi z^=jX))n4D!@b>;u&%%9A8h%SlKiDHZuYG3K`F*OIdXgsTZ(penFIoAU@XlE1mwNwJ zoAK_%#lx93w~j6M4gB@nuiABssI3!DbZ_#*H|uCb&Fy)a>Bmb}RqyQiIXwy+;Fnw+ z=Ic?uDZO#X2x4dO)SAEXvuZ9MDkP#0ob~nXTPN*Q+pMbUW{rr}zNR(t;dgwWnZ@ZI zuQWZl{(gVn_c8vK3p&&=J=&z#BrHn@QyclGjD17I5A^w)eeB}5s(fkn+GMBQy*sR? z)vprYM%ViES;>ouPb;_ktB2?KTdB1*#5<@aLp3w~=->B5;8Du|!Uhv@2im62JCH%# zKVRnKlaKm029NQlR0aDVEF4XAIX#r<|Iq0FvwW6+`|{uZPs@ok3pUXIv8+e>-%DAk zmxgZge}x_sO&7U*@}=47UB*50r}yrh4mR)Z5ADCn|D(Rtw-}w`Zv-_b-d+-X_cEsX z&Tf01zTxUgqVLmo#KG=>-``>ZA)vQ?tro5G<7W;N=l1NV$$GGic;9+$&9XA|@X~=& zA{*&Xyhy26Gwi=BzTgcJ#Fbt1(kZN^f9RD3{#$+t5prV)QKNc9e15W(c)cUbKjBp` z;$+>H{vT(ni8nz7zE>Z7#Q0}P#Q3qVh@uO}iKeT2`1^nBmEK@xTfa~`CEdIAKYxq6 z$Nl?}ODWjxOhPvO3o-o%K}`5HA~j>fNdLSp_5D|0wkGmrGQY9Aj3B*n{;_j2{nK`B zAVz+9OE~JC_j7GC)4`wT{4>(~q`Q`WByRkA;oCkWzuFWyuI5GTVZSxR;NMxX!N+<& z5;F#O_V@A*^BCf0bd#GU~e>GeH-iGc2`@5lP}etvK*0sUNCQ^(>W`c6(F-Ul`Hf#7bx z_R7JuRxx@0sCWPRRxnKxBiT_fkT^v4Nq8+}0hLIetu1#Y&Wc5$&g!wH_YDo0p`?RO z)|k{S;IW*XO6P}q6wVjrb6uUB3Rhhxre3JEf!$l1O@OD9Nhdu+H`?>H^rLH}be`*X ze446AUzhu=I*m!;s=`ljkbI7C)e~(S&-{@);KkYlDfOzp$JbG9c9*L1)w7Fa%ufp% z7EydHdas6wjL|GH7pXGhcb6U)HsCLG!RRc|qk)sIYAV<%`hVH6E(bh@yG0)J%v4WB zG|HYDtG0pYoB+%dOxvtN$Pnq&HUW4f%B#v!!m$(RHD-f1!BB^8AoaE580PLnh!zMe2C z=UCuG;19Ibq%!8Q%Z0PD`{;j+%=AjWh5tiEna9Ehb<2Uf45$50ML^+HXzHq>1LUie zoys)*yaWq&OG8GSa!piTmJBn-sM=}&BwgZ%(9z5a_NxW39#wVI?a`iZSgh;`T@)1N zJEer@3A;nNKH+TgNc{kn6!&9a5v`h!|CVsrJH*ptN2Yt8twniAS+cjOY>q4{vd8Yd z5iR4A(OCH!*zYZ^T3@QA5~+q-N<(Qz;v4DSMZ=?yySkd+7SQym*cLk-p5W3mMQhw+ z<6cWK+b`<^YNUD+z-lw>v@IMTEz~*}#s|5Y>oCtQNskVwW_;C_v=kke`oS?2Sq*Qk ztk6%^fvL6bmg=wA;)K>@iuRpmq%hNo+ExjPY!Ms4=es&VIpj`TJU$>#Pyv?2_Bk`qrE5M=5Py?z&u`ppe=w#Oc6U!g6XodDY9ceKmvs=Cm=FC3&E>zwKM zX^R);xnp&&6sI+lV}hg^+9{@{@|CIp8?5vyRyk`emvO`~#oYzx990gV!{Z9mzLebN zUfXKqUG2r@9wkU=kmoc|5}g8mZr8xHP~wX5<@^JM$-gN@jWS}FBw4%0)e(}aON2yc zExFz?PXOV%)NQ@nU{vkU*ArF<%{Zyu=qgb1g0B2@*>-k-JH@byz3OP<*`i8yc~BMY zQXR2-;pH(=rmjdJT@Ni%kECy@dK7mOnwj2%=Mo1pYYUfhzl)36zplQlTT6jm(X)89 zV;~2XkE4&`sQRO;FLr_N;_hqX;BG*DHlhsS4&XQODf;`#?Oc=0$5b1&MG8*Ui%muW zU0_75;3`UxUDK8p_zP>b-;GMga_yMP>FCSEi{+y{8LIit-iiZQ1HlzGgkBRrEP9h< zP-vQIcYV@=$|}7T*XT--y{7%>_R@LEliIcIW$^+xM0bauBTPJS1sUfZ zVQ+(!$DaaYJ+tw#j_vqX)iCK*?{&B@eWz?V`w89YIvae4s#diMA0Stff2`@$Z{#`1 zK$lZyqy3e<{~-OoF}k8wGxBt$LW#&dy@LV{KeRy)zsd2k$EKtffK) zf{w7&#||RZF}%08aH(ZqUtfGd{NVCVnhgCJ)f#<7F@ z6w{V2WztQHOAxw|MaQ_go!kfblW@!~U#QO%`(?g zV-#GSsDvY!&$@?jEQm!K72zdoN@qHcN#ATAB^y~7;AvKs;cTxHJlpZF&S6P+MOFH~ z$X{Hj>!vxXnr|6W2%6{1161$8H|!KmH*)Ikfx1*hXSygmrTu>QH)?O&MnZPmw^B7b zQdr7|FTKQ)svmML9}Zv#E5m@^XS2$rE_w zf&Breq0gunIv1kaO4#p9YOgU`_v({0Au0DOOh9Gnq^QMs7S(`!rm3e~COt~Vxb9I6 ziZdKw@ri=QK%n>g-G0I(P0T|!79}uZ8Pmtc`iR&@yIob9|OkOLmXNm0QscT>l|VhAeK3) zHEN?$^PQPgeMcl$#V<0OwBzwX+I5;dwOt>mUa8#)Z**^{c(Lsy@Fk64IxEht3v#I&+ zmLZYSv6c-*YSkOvA*>(Y!DDod@Hv%L_zw25Ew-$mHpzRU@_=uu)}H8068k%m-SHif zyUE|BLv25Gf5B()Id|{MX5Qn48+6aLzG#cRIN7B=>#j*(n-G#d+tk182m4lcMy=OR z^VTchT>jerLXriZR-2XfxS0u;m3!s$4E2P&Z1aNt@~h_YIRoN08=$DaO0|X*`e>TP z=Ue(B63ynyNuGn=uHe9wVzRDcw|=;J8$8yY0Cm=&?w=_QlHr8vv~AwU@Hg*wbdL6l zR)}37b`>?#JGoo&xm=*RlYNi+Fg(_^#6dwn%Ze=jhFl?k2oIqn(6Ix36}^Npz(Z-U z%0}-%W+C-sh`1=#B`uw_6;3QmO_{@UL$v>N*vR*#|BQSOsGasbiY4i|rc|UUW|wW@ePf9m_SC z_Bz}pv7>UCTAFg!_#^R<;REDIn<(vPd+8>qi=xx+DmNuoX6Cdr>}K2O@}@dVr=BSk zcrh`}eciL$U9ak=_p=>#f5eyTHtD-?IWc?jWjbEiz^>LcM_b~)P+SOF3h{nQZtfa5@?z9RjqJt|k4|f0*g; z0`CtwXUwYQ|8~dv^aEey#amJPb#`W*U>~l9>Gp@+$w2g9N z!VqNxQi*Icnej|S9zPX>(E&-B=sxXNRU3DFMXCIaXNk87vdrGoK0{b3uPOKH{vZo0 z8oSKw1*fL$uj&xqT+G4A;0~s29K|!y$<#6G8^6-I%{fOn=c;veD+A$wiyI^Dw5)k_ z@4e{EIh$yvQbXecdEQ2i+och<1skzXKPlZPWR1c9A{M8)|3B07!6; zNvPJXMi;=hlD=toxE5;Ja+@C0gvK`m+iDY||B)`x1SlMq&AhA~#BjnLu_~!0v zis}4yms!QThI_}w4|FYezt?{RB0Wa?G<}D%yUN$vI+{{lx$+;i#Lm)Rgmv-HRL>JG zfStsY*f3c^!nl-!QNOiDWSO(Mqd9ky9TNXJ#mJg8Yi)7%wQxP{W5jRCQH`svaxbdr z&UOW@@KH%u=8mjD(O&k&cA@k==PciqTVH!ec^g=j5U8RI|IycJrR!gNh%FP&R4#$O z0*OGz`t*{!s!h5Eu6(fxVwf<3;W=;zASo&;Z;8=DJB8sETgUK$iAl(oSMMu|nnMkd zwmn#C`X|xgYM5|=vFV$aFG~Ew+qLhD2eMm38|2HAu8SJX?^vv0EA|Gx1UF;DY&QfY zmIiiIT@-?>y|}mL1j}GcC*xW&$kvL<;%X^IV4Au$S+iYn}byA?}qimw<8T|%5Q2N0hpraFl6Nh@| zddGQ|^(l7%VTQkc-(qFw${aOrI6eVa?>(EzhVz~zY%W13iS`SQ# zI?Wn3)7gX_t)WkDz3_zjZKB0K6z5@HKc3?X0AR{8S(etZye3^<~>VeZ-RF zD*XXK(MmnQZ0kse+dIWj3(JzWlEpZc6svRN^Wv8?nXVo2 zA;~*6Eu3>Zj80sm>YZZYPgpUw+}#)%iq&yH5ffMso%^{*e3OW$;68HT*J;j=wdGw)&ffVTKgTxHpLe}j*N`I zk`&{9-)ZkFLiscax}vE+hw zYw;3xBh85U@Y%X$AYc%`Uao%~ncP+GD_=$0^0$yW`D8^0Y^*95U5`{CpOs-+NkZ%L za9^1AR?_Z(E>xy3aU-CI0nhB})MRn7F4yOyzdbSGGljsvssS@L3KBXYcB znzx@;#sIFFuIubK`ZO1jxIwox;jrtHy}hfM__vhkjq)$96Sf=j_5y&cFPflSz*lMy z!+`p@<0ZFNwb^{Myt8^TcC#{GqM&5ql&i(`%6X zGH8DT^dE+Yb$x9_ z*$n(cLYXGpGv8HKhI<2@8P40t3TQC3$bGq@EcKT(R6k$ev??+2A6s8x7&n`jdakmP zlI~SY!Px*N1yzaKZxv}3tINM)Q*>gAl&R%IkRC`w$fjM-cI8RQ6wh7vZT@J&TW3!3 zsD$61R5VMUOK!1A5`%R&kk$Gt@G1LR@)Mwf4)S{~IM)^Lns`umMF~J6rqgm$9a5a> z_^s+Idr0xITQ$d+f6CId?Q|=go`jzTrA*)I6EWTFx5~O>$@EPfA&rgG#h0;fnOgh_ zu$SEm<-(&C61ttf0T!lRV+eEoBNx3a+Ioqkdp<02uF}ZM4%pUc6*`r+q1W%8Q8C}` zb*=O4cP;ZmC91N@a=;Q3vKjU2_tbq)cpyZ%$HEt!i{hU^Ad z;>%*n5T~W`xHl=mYi^U!w#@3D=`Pn#i0`QX>yqQ+yc@Mf{ZAK(K95^t8O(=qTW#lr zBGX>`6`Pv;qmfiBu6XP*;*F|N=jmie!Y+tSb(eja=Y-_Un8mK~?Yznf#Uau_3p~6|MpC*|kWFmPG_2_Fw z>U1Z}7Miv1AvwSrqDjYw;uj9iRWzoKOPY8i44Lzod}3HsOsT4=1)3j<2((=N1Wv_gVQ29LszC0( z&|4UV+|fU5qPq>3^qziOaj5`^ulZBi`;b5PPKiiQUGVzSNOz zs!ms5_h*goOMC=35rmYCt z>|0wqHl=0M(6mSLrhd)lSw7{v`ozoY!M+{+rRgJn#QWxK_WPr^mQ`OLSxf}(wh`-h z8GQ>(4gCAn&(d~!&-r%W>sI~m$7G^!3_(2HoIp$oyIJ}9PrPr#f3Be>;fh zZBU|i3GSOYF311!)jlHTctZNsz%2g|yn}yPt6bmHlhf0ukSD7v>g@1cS=PwErTuw= zc^r|R-=J>#pZQ(=L#N*HrPpq*I!K24@0VSy9#*}M=;QMH&M&X2-X_#MBHydBg0wv=_YxOw)+Ywdd`pZb2l_{LUrn?? zzobn~@DLMIE>xv{Ym%-C=;>Qiqw_av;jWG-wxksn_pRnL_xe{B2>#`>)}?l;cc(Hm zyPdzm;N!%<-nxF>ih}eH!hWLLkBezWe@}n4flJ%)ykUCse^#Qwtu_Ao>P^0vdm5%6 z81Sk(H{fXX;RDx*py%&>KYj)gudupAyYHsdbpfY*HeN;CFB#--7@{M*moWe2m<&IB zC&I7V-ywZiVO(0o{?ODJBToCg2gKS&Pbu-}6YuL#ydgF5oYY_c3g|yyQP+RE=^J0* znpRW9i|vE` zdx8cNSE8E`o$|h=Ce_fi zfq&fF0DtY4-=(nr-jN^6Mir0sIXSZU$7ll;n$Fz;ao#+%o61~yi^Zc>u zC>u?*8*-2^x2p6>+g`42{CiMZ z!jK%ow!Vk&?ad3m-SLBoMLou+Od*~Bs!!GNRqrdm zC1rYl*bNoE(@Rs@5pycPxQZ&&wy=)hljPL5B*L@T)!)9`IaoHuxyiKznZ&*kW(tqk zUuCK8FxzN{*{K&YxrSo1PD)f7sU#07(&M|sm^v0q28-~!1*7pFD3})s-qKVgzCgrs z#1c9CMSU%*h4gjOj)GeDs{Tt{ne?LAd7v(fM%)RDIthx8`hSwr+^xCUp?rm`Y*>8# z^5+&iJ5GX@-XO2bf)yMU1U##LsJ-le1dd4MD^DiBb_aEAqT5^rmp8NjD67KnM%=cx zwKYucQPCZl>dJ8`%Kl|9SIqXD^|+D#&REOx#47TtZnl16{0;3d?F!X|_(u8($aMIX z>Nr+i25Y?D?sS?etn6NSoA^95jANZvq*#`r?-*yxS&4it@}L7?NAh!9F4jPMM0wh# zLf7eX^j001=&N|8?^3nE^R)uu&nK(gvrt)TMuk7Mx-3h-*x63o&iyG~mD(;oR_v0U zY@3FSwBu5}R4#g-c3?S)`_*CI&5piFm))&Xe*12E1FGvJHr@T?&;ho*WNt)n-V|$; zw4y6KbXi_}TKVm;E3Q1JT~nvJw`Z?rrlW~wx2gbNj8%i{sE&ov<`V*35{FDokXtld zyVQr;X7S_V_i3Nwi2AbajpM8Pg6=yMulKOg`e}|~vd!glH7f3K(nYU?{yRCyw%t%g2?5F!#xu$v%Zc6LtvRCS>N-BH_hYFH3rJ@`9wK@t& zhMrqfV~I+G6Lr*9o_1`AI#~0a>kr20PdQk}b@7Bw>P_)sYDr=IbXbrFR+|%p>PQ+jCn2S_6Gs=ItTWg!tNVGd+rWNTu02=1)?R~3k zqMxE0%yzXE3o_|E)%cWIhwd5{YByESM;mBED~DE?s-3CdQYEQJp)2Zku|J)Wo^1Cz zWi!>7s_|et^#Xf|=2kRQMMR&q^-9C{#AB7ApyGci* zFRG>hPrUyl?X2IN#@;qQxVsLFOEqniG<9Wgcb8?+g~8q3oxuhepSDv;(s+~V;O_45 zWfyl?bb+`3!`okST}j&HI*pv?ocsRVJ=trKD%OG2E@(+~AeTY!<4T3=#P~>prGEHF z>SoPvW0HBOjFw!4?}A9;^i*&W)H3j!b%c{NGD=}Q zEllCD%5@>uQvt=}&NUmuv$#(5af>`u3wGQ3hId+bAiao*)K;_1Turl)nI09wtE>kN z&rJZaDJaNG)zfXI^jYW6y;T8C1ddzdBaznI1Pji&mV zGw@c6%Jfa2$0edSn9(d;HV8{AdjTFc^i_Tb?wPjXV-*MrO8+88#Kc4v*CF~^d_*$~ zm5|>H(&|bTX`4?-U|HTif)K|)aSFEpei%sjHH_-uhZFmS)-#$HZ zPm^K1YIqtM6}c}ov!rY$l%r5F)C_kWKTKODV+pRCuGrUKrFYZr; z7a@CAp>D6@Fa8mp$IM|N{yDshKN-4M_ER}BbYHTP*M}I@b@WBfD`tcEe^eqO=d&#P z3o`L@;^FQurrG`xt>@}zd!7&p*2TKM;0pU09m923ddUe)4gFXyq#I~%N7Z1)8rw6S zLXF|oFdWVzj$(Ug6$3CUA=1!X_mE!>1=zQmPe2*OY5q0lgPjRS_7Xoy-b6YDHT-5) zq9)k<~(TuqL$(^82dt2l3l14T&^nSJNS8Mrmcmg-;uv`kHZ6Fb@)et!q5rcrAg8bWX#&5 zx)Y2`7t`I)>RY_=)Xn~t)-v9pKDp6i1T69Znj%l46~2l7dY%Qu0`ds8N>f9T$eqKp z{X5`f;d$sYk*1l)tTK+Uogv<8MvHf$ijad|Z_l+YAWwr$j1PU?xm@_FvE|+sq$-d@ z`RFHs$&hPC8t(`RiSBF9_s^`HZIH7dY+BTnRW@YBKR8dl;|1qaWzN$?;$6ArQ$Wlcee5a4W_Qti3IYhP98GI^i)oYU2ip{Z{_i_geOH3r--;nCb3K_N6_hATG7n`S?gTv3gQ=Xb9%=KBQX zx5TcGs2|ghvpG&6zXZkL0O*4=iN0QOFOVJAeS8lb3_XLtqmQ|HWIIt%vRqq=+!7vg zANg}!Pj)8zoGc}_GiJj({R#cwft7sgU^X;azZQBMxX4<`wQMOIc9v%KC%&r|guAoP z!Vor-PBHZ|&iAVQxA{`ODW8Wqp=rn#u7bn3_2%TjYj8yX)!ehb4Xoi8L?&x=P|wf} zj;%z^Pw9)1`XNo)HS(ipm~^ftUC}ywrs=0B0g)!x1|{UzQa891S%jR22L!&j=2|+? zx1BXWnSMe%PbpcHsoT%2M81|R;$oh<`br@Hx{#>mSzrfsm9=Vi_-wUJK}ldqq)0ex zpQP_-nT_<;?_^;*242%Ep$E|umiwmq_8LxQncjU}?vzEomfDqEN{8<`X>6V;+^Wl;p*W8p&y@0U&DS!e_Kbk+jnS_ z{i4w<%ps?B8pdxtIi9I+!j*4~wS`{hV)iZ>>P=X`+$f;ccXIU+{jPVKpg_l z(oLD)r67BRn_#S|O@>#o7NS9TS}=#H-LAP|pCMI~ELx(^MUu5MLiMb7^-Z#8tA~(# zr8Zz*S2ARxlHvhD=)mPCCJ!A;lH9veJ;jnf?(!@YLeVu~m zLPMtz-l2QO)>kjU|IjazFz&4xk8pv05Nv1YQaTao&5M|xcnu8;4c0obv*-wCJMJ>g zlFiaKLG6W?%uci_Ifs7_CNT?;rp9^1DQdC)2ArhsqwTI}!Y}2&Q=Q;%`k>}vAXk9+ z5$q!No->D(LK_tmbv0FMl?shn+ubl;(?j2bU1A*1H4|Xte^{;}mrlip6Q9ldwU^*8 z(b^F-Bx0uF{fUcYF+VrB5V@iKF28DW`v7}%xQ~vFTq3Dr{t0gBbPqE+h#)u7%N=I9=kTM`u`IBm{lu$QhyOg=Q#Z(h@ zfnt5S-q==>1>Vvi>J@_C-rlr0IMjSb7#W&qbsJZ!S89^T4*nq~rF^g4tnP@+@mulr zjyi=mh)2|0?hx6GZwj_yXZS+;2)kNV$R6biSqC~fUCW)%tV+KODzh)RTYC=WM6GQt zoi&g6vcQ)1o!U*Y>Kp5dge$DZ z-ds}_{Ai!g)|3pywo4ag_6e9oD-x5LxpAlWhOFM1vo%l4#>VMlzeNw*G+>@H3bu~c z?GN&sEvw)fK(*vP`k;pe|3n_Zr4>=)ta~WY6>6>b2$;(wBv_)_e2$-9cO1NDW!%=5za`7YW~`agkH$VG3}NL_dx{|7sg zI8M=qQ*?g>a9)y6)F0)m(Oubdp=01==A1mLWi^M9a=z490VI_F1I<>pl3WE0tcJAu zryy!DT_Z)7ThypqWFVDQVa`_Nnya&;$!><#LNE<4DsvSx13VY` zSHMlOJ=>uqRo290%NDZA?a7J^cMbiGz&1mjV0B6#?PBO;+`;~k$eDKrkeEt|y#MNU z8h(JIg0)RO^edUV`d!*Na8yt8F#pJ!?4Qd26+RjGBvg~sAS8OTrLn$~v72TD|CoGd zzA353e-1U~*CVs_KaeeAg-4IRQmK%Mx_oqmZlCrneOI-L-Qim-MVO`vzx$SXLq&aG z2U&A06;Ab?h*YW}xQWqb-lM)?IBM_(no{j`4@e6B*E)f}Z&^g*@IG>+W(>Up>F4MO zcN4GP52ainW~C0;9VK&DXQt0*qn;2~U8jSVfiBfp^=GIhKS1wLKQ#rd)e$&qY1fNeU)~M!Ktb2U^~l`p)>%gRO$T0a(=nc@D+H)BfkM8S=_@nMeo*-jxrL>a zgY+ZIKPYAf9>ss{4!tXs1R~Ih?Cbiq`VP7T{uuk6eJeEM8qkmUXXGCGZ9pJnJjE|W z+JiUA#cT$1Uff!N zk=aQ>%m^VzV2C|hL+~~6#XAIIm`35|$Q^YNI7#TjoJ~qKjnEGDHHnl9v!aiY*2-et zIZF;7jVtx0o0nN;!wngK@MZ8WSVO4C+Sq#SjJnxcv1zXUXlRYLiUHG8&^uvexM!&~ zYz@>!-f8Uki|`<*7Q2pjhW^#`3->ZS_tpsjp%CAe`xN|B*I0W^ZV%N*y9$#-gXpk+ zaj2cX+`!3ii8GOIs$Ae38)Y4=hdKcjiEG7aWDT{g40>#|=HIf;iD~AveT$Pa(eV{o z-X)&m(D%UMU_>V)LC*`$l1?!_^p)TM**n8emkQOwZdvb)Z+Nn97ot~ThS8+aQ>o9? zq?T7L-$~z*)mOw-uHL3=Or;z3XALoQoOuPAob}z*NBL2*KhQMiwWcYwH_HjBu@tTx zZc@0Lo?#rzYG@>DwyKVPp0TU3wd{|S2CXKr8#O-=mE>VR&?;7 z?e(pasPy;Cp!2@5GW79I?99W2%0+9!v0bOv*d&+EMtZMaN0Cwn})Wt-~=S z*5LDE+lUO@3i%t`jx@`RU2kWIh=%OwL2%+ zHdn3;Tf8ybSxrs*iVI_6U(R-k{`IP7tb9q$*s+!gvEQfL#kSSd+G^LRW*7RVMBd!i zSjX@-%RhEoGwWc-t>)+n{TUU3+*YijsNdrG_N0Lm8Zt6i^>IC{kUFj^YFbk zu`)gSco=N|xZz23V_qxkBemQnv$?I;2D~(;Hrp24`c`J!;Qki<*=I*|`0rNH*k3oI zrZ4eve9Z~lt*L3Tygl3< zhcX{p)7wO1fxchDU4M!q`(6RjLoXl47QWvZeNp)^_Gr(X*yKLC=uO9`=%)V|>jmbs z?ZR-U)!2QnrSy*`wxpq}qucM?jD3~ox2%^9d!nxgX2#B1I#f1{Op4X+bky2}MQlF~%c2NYE4uITPn&&SIC^;Z zF6*}Lr=$NZ9A;ewevUdD{jh!Obv#;HjkdjdW{hNNeuY*=x7ezD868b~bRc^4Okq@c zyQyuK_ls?D(aPxBtBg7ETvALld5U%7sI}pP$5+|1`rV9Fd;2^(@&35jlQ;dXlZfGw zC*nYCx~IAAeD;6lnTvWxsm`xVjTk;M+V;!3}mz8a7 zOaHLMHmf&AdsSSp?oTBmjw}7_D+Z2=X&zRsoLsYy&E%;bn=}ZuQ9rIkcTW_BzyB?< zb^h_#TJyQc&K?X}J{DrJ>EKE$Bpqb0`pOYAi>5}>Bu;x*x@C;FIigp7b&Ca9U94V3 zQS@$yx6ypkY}k!l;;zh-nUGbobOHCRUFX_30gL)_Pae zy5Ou$ysegPe!4Sq^Zvh));-f~*(o(E7kcu;SLEwrAL8>5O%{xd?S0lHdME3t<)r*p ztO}QFYu#~o%q3fI`*%MR{okkj$Paa;wfgc#wjEtY$Cmlr(a}TRS{FUMZoM$aX8WwHa4r_3G2+7n{EI5FeCPatYSTKw=%YKtRZGUaxr$}MN}~)nel_ zaHv@7Z9`-P8U$w$TgWfq5YgkDe?p1K zPU9YKd+bW_{zF6{Bm6(iqVb_qDt?&q95KayWM|1s)1~NH3sl@Fr|;pIwvld`y((Fg zEY2bgcKviSu2NNe7r!D=XNh=V zEHl-7n77Xd%PNb>bXS7&l%CZl>pQI5ia64v214IQKamV6#s@N@M}_*~evxCy>fqPl zpTOzRKzMR`6X*-IE!-N&Vn-hi(g7wCI}DWwx_#muc@lDysS`?!yiNJ&L`9a z*>=%b?syXIzztcIjf8sgw#PidGGjDkgl-$A0LO?HNLywu_>$~oyZ}#UR)nj>;G^pz z{~3XHp7i_HF`6gFhuE(ix817auMzNPID&4N&$PUr~lP`dVnxPX%J0F{s zGuzhum=xV{xH2BkDb;_fd=oZ{+k{8O)WOB2r(IXAmC9wAva}nr)aGA+_DlzD67gNo z8V^=fhAmK4#6#;j0)7a6(B2Da?AHo4y%0X6HeB^c$(c*i&0X7l7tSj#_rA^5s!W7_O>> zX#g%M~rGyA^Lj-@{T8LtknS=z{L^nUjm7%zA$^n#q?# zS#3i8I|<{EkZ~Nr8vfM(&*dbP!U=MJaHcFvr~%P>n7u>}6cPg|%+BJi+$=y!x1vg5 z+WFK>xC3!+j!$J&mBCVJhxD~U(6Bbh3kX%9Z6K@x`}o!pc~Ah~=Mv%l>6_GOd{I2X zy0>&MH!J3`dt$^#jOMnn-W*8Ia1@sAAA-3^R=(e18U`cUSeblh~^yCThER)P0q z@Ayo*79f(eQcW?ebF4E;q`M^-^^@eO=+4lrwx*KZAOr2yO^|P6CI#}T*80t=x4=AJ z@2Tq5GR4Juao<3`=&-hz<|?uwu5tYjszRnqAW|wFqWR!A2kVD-h&~=#U|t(616W;4 z3g5Q`!p?`$V{BdtJxSv>4u~$1gmCsVasl;z@mo@i< z3*rIXu^<9eRsII+Gi#CKETxu&r}FvyS7PNMNO3pLLh7IK8#3D(R!FikJg2C=@{V+l zBERH?CXvXfn8?gG{b7`cE=4x$tL5-vdHiTAoieo-eh=?5-InI~#_EEKS%E&pYG#}s z(p;y_`m0ceW(U++yOD|Dm)NZcEmubR zSN4S{R{*8mB!n^7M|P5NbVvzjdnG~}lEI|j!Su22_siF$yt05OS@ zw4TzJ;6i*mcOw2~x<+P^r?r2m)47MHw;EpA3_1ZeWY=(w`Mfs!B+uY9R)qQ4K*}4< zsFJIWB=QBn&Yi4lWNFCnH2Xrmtxpf<8e2ixrjqt6V2Am-wiovb*-Fo2K4G^cy?_kO zfr`Y;&xY?#ne-g|G;4^tq}{R3wT>LHjnphLEVgFa|BmJ%FNiy_2kZ*aW7t4$bbE!( z`Z{J#qd4}*p;qm%j<{K)))ObN6m5ZWt?D*12)lIP5;Md-3LD1RsY~1kXdv=UH$#2L z{y%eV^&v-3}J3C7&BV@mQYglk(S}{n!CX8NKd3A_FDaC3zIq6 z{DEQw@>w}WCBxz*P4{tb7d3~=VUEdn5oARIvyZ9eD^WfrY{gB?pmb3N41s0ym5;PK z{}Eaan;@BjG`h`Ol*2U>)DQJ`q84&cQw)iU;z*mjzLRtr|8%K0Op)fi^eoa^+dme0z`28O!hE4FMVx;32}&!()2G{c90 zf0><8m|Pvfbp6BM0&;E=lxDkPzoi|A9HTNd7vVaE9(9U%j<^6?s;Ex2w^RBy!pZOr zPG_$X{t}pi{&H_7hVDG0I}`^)i&oEiD4J}=`G&|oL(rE5 zSu2IgKM$>=4v_=paujDGT>rQ~={Z$_yiWdYtQxTePH_QtIP;V&@zgIb*3E<7Lhsph zY)ViB&gIkfgS7WyjjpzCRD9HUm|h|z!b{;}%tb>H+bJBB?GsuDG)jZ1n(z&Qp3bqWJ+95I~2Wx zwGj^wCy06!9}3TP*U#?E4H4tOX5$ZIcV$22dHOJWn?E0_mK~xYBqcnP--P}myMp&i z@6s;wK;iE|53YUWBGf{&DpKT6!|dKojEY!5?V+Z|^M{{n5#3YMF~KXe=0=2JVIP0V zd_8<^ch^{j@KSr(rctlai_9}%sq&89gtv~Yu%se~4ZGRa{NCU`q!ZFvJuvR%X^U+2 z%18+9MWkaGwZ@rWF+iNB3h+&Vt#kq}2ev~k=mFRduNdm<8-JQCX59>90lI5S{)3!_lMF*^pUT zwfvrBChzm)@K(Y{zfc@U&Qq7cu_&QSQk{aA!Hx6}*axa>!i%!3(q>3|U)W$nPNwyO zpNRS#eC(O&%W}h!g_2I$dqOkwQF<=;gr38EqhNi1g-LxD!84DO4a_ZAns%z@@noCD zc*kH&RW7vjeUy3Bb*Pynht`#E%`R5|%G~4w+=Cor442F5xEqIb+5d%};S z<_I>$QEnR8LSNtbl`5oc44Ku~zYBpqoa45%pM}a2IqQ;Lq+=@@aSeo(!G7raBl!4E!XVS$n40gxUEt1A&hYDiPF4>oh3CN6-TRTP6}8db z{$t*C^cgCZKh3uDS0y%ORndI$PnVBXeV4xSPRPP+$Gtx!o3%-jm}H-%DSd!RVKCA9 z(h+TJDH+t?c&JQfy(aycepp$P?5jFUPK)d2H~J4SHK^N^lIv$#XuEbq#Vocjw^S;( zSGI{OmA{kI5gC6>w?sXOYh_v?%;x7C|1-@~Z4JB$oTR51PU@3{^Zbp}QL-WEAaNqb zF*dr5^)FK^GecP50p@g}jqo3Oo2AW*^fqgbsWY;db7NzyNyO;gJ;BFp$H2A7Xe3Ef z0UiWh!fawTZr4-|Ew$FG^je2&17}8oV>%q~9FIdkYB==w(VOg9JV%|#`dD%CKdPwS z4aI#xKm&a%I$t%JX@UYYqlQ&|fMsNE@P+z@FrNGhv{&1tJCHZ@c=s3ia`RmM8Ac&3hCq@8HEfWA~5kgBq;Lsx*x z;(dkC?Ka?*r3PdUD zH-`C^&rDhH5A#oaDO9I&0Kpb)^-ZFdNET*a;HtS_WH*575h_IuMKflr%jd}8zR?k5 za?Dn_GyL7U(jPt4D%7fS!Xa<+c{hmAW;T3UhRm<1(CtXD@F zm%nlLLjJSpceaAx?fOKl0LDW-9k`>G!Npt_5vrv?ZpCKI89K|Dm=%1d;B;jn@Exih z>LjZHNYowRmdZ1#UAp~bUst^*pA2_PZKaQmw~PKNsYeRA)vYPa@@6km1rwhMvq1pu>bK#`5pdBrUx+^Tqf){T0(;j{|U$0#zLZedu2nt z%jz?qH(NX8+Rt=Yf3gw!2}<*6*g{o*)i++jB{9QHC^ENvYK#aRR~yP+xQKMmxQgGRKE@Jwr>C~CHnzmX{vf@n$<5Z4 ztfrQOJ%N1nLS=0l=0{UWL__|RvWVHjzwz}-pUHeIfr=mL<@+X=9xz-?&M7Ux#^cKo z0USrZBW`nz8`J12_@k>a+EYIPX#w$Uoj@@+h+P$W&uhbXn8(5lZYVfkJ0e&EdCr%( zSci-pO1c9I0HiO^YyvWqdJy>{gfYajkPGJ&5Ge1-fa><>P#Y_Hi1 zHwbqKbWnVY|LYe1JzDM?&Xu@q=?wSN)vA?JU#@q78-@7_u}Y!eKy_jcxHLhhX}bD| zvxO1lNp75Zx=_Kl6gDY#GC_Vm*c@ESZqRI_JCR3RzGIVp+u+@Np}U6hoI0YNDp=Ul z>?EYU4%4PdMkr;@3vAu}#&R)7_{YY5Xxq!TADnKqmQHR9k$|`^cFBC+k{_=w6tM*| z2Y1YPl(}q}i3|h!C{ld=nELuZu@Qmp5*FU0KZcln|Fnqd@?ak4h$jB~$c41#5;A;8 zlIs?QyHUMZl4ysIcR!~fvZv#{zbpw+Ehpa)RV1JJ6ba!-R()_+(X=*$kk*_Z#=(M+ z&vKI6N}}us)q8xDatZS#5af5jWhS$>Cp1Z)q4}apR6Qf#d#p%-VV$Qll0$8GH52oO4<6d7()=yeBp0MdbUdzMsA!c1cnfMga znCqf=LEDK>G#99jfZRjf4E8NQM`#i;1atL`=t3lU{}pYn?xJC!Ru9iWK4+CfPr*9@ zQfSAo(7#u$Qa%VcsArLN*2R^bJ4|iADB7_SahnKh@R{%cu?wAnIB%aW#}GCO*l-)X z0uQU`V3GKMp3s9D1V4my)4;$bO>IlI@(QmDUDF)UcryTUlWI@+XmE!vLLA#Um<$F~ z62vo|zt6lSYVZ=co9?)(zUnY_kU0*kRT{7ux-S00lo=;!N#irk3H3dlm)Vy#Qj`hc zghjT50;sB>%X3;DBsKnhDj&I(bbH2eAGz`DDH4|##QC=K!)1~Gxu*$jBgYg21Z7wc z_or8}qlFw~iOH&;$1k+t>^k7RW{_ecI+^NHxelG0!I(QCJ>1KTiAa6-5;he6py}x` z3aP~UU@>vT%>Z4rv&+YccH5&)1#w2Nlgg2%+(-Hy*AIDzu6EbK(5w~aMZj5nrtPGA zwyLANk#3^)km-eB58hMbx?eiPm=CwKTZnqZY(tB{cI|F_i1M(Tv8oPG;5&7S?;3hh zSxLs+M)V#9p;d_g+)?iW`VG{P7MqQ_UfTYeCE6cIEBy=Y57i-FFynAuGy~b_Dk*tl zOIUHxl8_)o5>~V@C)le;6IPfl36*7$1bc-gVZ|Iv!ozCA_*ng@$cSnT`jB)$b|TBHOwDpqCGOBAM9Dx;-Yj>h$_+LYO7jO;bP9g zk1+K>_jttG#VxY%6Htrn{!~4B3vA@4%ZDQa4ZrkBQn)OV{YCm)wOD&ow-uX!h``dg zH+&%U(6g^(v$CfDo6(@%9cMGX01>j4`5y8MuYr8=Z7@}(+Dpf%2c{;17nC;BK=5_i zBKQSc3vxIINVjRURC8mFM~n3$@vNU7jry1JGyeptOXooSByXf2H4T}=+EQXX>{HH@ zw+BDR`OKqqi_pC$5=4r|$J=$S@+o+j+z%Q}r}+Z97=42Nhu$d5*JByq}bX`e{fGYnT1vLUdz!JKg$tj@nd}lllZlvuCGSOgOvI z-#Sf4&5kqiS~7n2t+*GNjy2UqNSqGfi^!As8)$WT^{hJCr(_eUImAF}vv3Y1nTh7B zpaE=0egp8L#K3ZO%zH~1%Z$fwVAt@!@m+pBm7*O(Jribe)%+!rjO!mScnXFtr0#-+pu{c)^maH7rMLg>r^lC)lM^%UU7)>p_#O^?JQ72yA@mLW}c}4gO9?yXXYyVHk<3N z>%Z(4VxuOVS>ZjWXo?Lamw`>>j{-x8fALaLru#}3%^4(3<3!sF*g@b% z$w_vOKim5gXbJ$_IN}&-5;lv@#~Cu$iM2RStDQfHh5m;Azg=No3!e*&$=XS*)DB~o zF^%JX)iuDqq6~j43=aGaoM+zX8}Yp*4)>}?OJ$Gvw(x&t`viu$1^=5d&tdmxF${JG z#Qe_$ikU-w^zhy#^b4>GZ3~Txx83)W&RMl23kZVFXL2Rq3saa=;Ki(=`NPS~BFxjq z-$Gu*eh)Mb9Ae5P57Z&zDeZ^;Q-8p!qcyP-QcOL@|8?}m187sSb8!XTn2E?Yk;&ee zdl!+(T=(@RZi3V3+yDn>qI|Gkq+7NCiMYO^f3tdeRxq6GlC-^43#^b4%z^{2iS3Hz z{2KbT?5(>?*UZNqely#Sw{oRm0kD$?6nV;jeJ!E` zWG^$@c$Tqiv(^@@DC>qk^-K&NV5UoM606J1paVV64#OL?8AV*+Vc!&Dv{!Bvlj0n?M==FM!Ig1q#_4St^SM)!Fi7ED+8G!W;1mQ^fJ@VxGfyQaB_dk1 zf2lB|_^D^QqPcuCIT5oJ{4JgqMBpGZ-Fqc}Cz~jJKz2sM6?QOCAxh7n7RJwGP1ap@ zFgd1Rf%`5sH)AV#Oc7uPOU}?$(d~*}vX!VOvI3o_l*-$~pN1D+aP-z$>1{nYm13YGbgT ztHAp}R!&VSoERq#rsMndW7k(CI1!^`~**eOY;%15IJb2ERM zS{bisPb)FcSSIFJTJfR$2yr?4WMB(1xvc`)NSq1mBxf^y!fi;HRD$2Bu4oNU7ugsB ziIbUMi1x7*)RcMSGSai-K6M8Lw%s62V0!unNhhHHz%e=(EtH@1e~yp4otDBStJwkW z-y*Sp6I0K<#X}H}v!w+RvK6|Ix`%ZUC9-2AlxSx9C^FZ1DBgyHN_R`<%af(&T(hwN z>(hv-4B}+ClxYGsX!S^F2ekDrByY*jackK&E-AnR|54=?=Vc7lJMf-OkzOPJ%Xk9O zz610s|L5{)2ZJu!953G@)K6VsS^>2vL-s8MOBLknpmGm-eAxK1F+pIk*m z4fX(j5ueHJ z9LPqzAF%=GCU9)F20Ki5^VDSzJGyb>y&BhMf+qe9^(LM>WvG}}I-L#?c?j69TE_AC zI6V8{4(26cag@dNt=k9|=|JmQhj2H*ONQYg3Bz2+^O7abo!W8a0$D2}i%NFS=R@MN z6eK_3O=W|i*Yz=bIF=?mkk;bhF!TYrszeeW_xFOoxQU*1_+s7-d?4QVyCZ_R>CclQl| zQZB;|`-TNBmL<#P;Ipvr#9Fo?wLuzFG?Xr+2g?$Zwux@j;v_SjOX!Gyz!ZA0W1)LZ ztJf6{yeIJ_bWGk8t@N3cHSqzf#TWsurmDyJvjN9wnu#ZwHN-9hwY-|r)%aebu4o?D zfQ&0q6;J&Lw~*7sa}>J;o;LZiX|$h#XZ!!gCP0SdBbnP&<-{YRb(~?)mURP*{1TuN zc`K<=?oed-=DI(DdhkzfUXhzf#2&DT+-+e*JjExmU=DATO&5(Milnop2DTLzAxz5A zz9xV~(uKtsnr)h1qI%;h3mhWb^40nI(pMF-tahzeK%FWI;#8?W$fvF)frr#kr%Li) z-~hRXzgSUUH3Dxgp^5u=hL`m>B;IHl>@c3r zKJ%ZIPElxt=U_#43cb*K%vS_9%c!RwMQzsPv8z?5y`q9*s;+dW+UXxpw)Z~4_t2MV zvFuV{PPSP)SE9u|Ouql4Vn0@;q=Bl4ws5Bva|3%&GB6f92290HrN;Op@`+@2$j=Uo z->#qY51=<_z;9)15-0gB>>5(yo#!FgJZ5IrT5cXr6^=)@_`kU}tC@JxmmFN|vGWr> zDTyjzSv+y21-O7P0^49$>c)_c?2bMQyNNNFQ)Wb69y4CObR`8zhB9wxoo4~X6R%`F z>2GvOJZ+jPlcn`1JJEZfhDpQpCy2?+ELDQDt?=59m-SMgWEC!#XgPVsb6b2sT#vXa ze;`>!Mf~l^k4e9${uKTNk*~`vOv<6*v-H5 zeHO@gLhMg(3RMQSW~J#rOVK3#f?IimPeahp<5i##{|Z!+5ATjY!RF4(Jz z%}@qVEjUSX2cyS&6y`micf=%@n38; zD1EZ$oaDW#4;7*I2C`*;$-8+Qc<Kn_`e(GMkT74+ZD6Lftvfg4HoYe>L)j&{|==V=1>o zHVB42KmDYuC7w?8c63HuxWFs{i1^GX;eI0>A;#pzsG2Ym!`Sygir^zjs2)<&uc=%t zNE98^&jUGev|Z#u6@pZ6C;6Bj}VcmEATA54PKKW{3 z2{SWP-`{TUZgnqb2lgN!5jFSk7p`S32kVgs3wrFIMz_T0vE!IQ6}8sS4F0fAB znAk6e$(7|r*iG9q(WNKxcs%K3MHkY-_oZTL@z#+6f$Xq5Z}d&;v5I7aV^)9b82b9{khsFbD$5ka=y7Ya#>pM;0thDXt~_S3!EplAGkJg8DCXW#Ek{Cz)7Nt_!+L{tLLA`7nIf^ z-?}=oIqVt9@ytentNs~6d(T)Iz{+G30#zysr8UVb!~v`d`iy->U=o5kr*6;4#f7r1 zRJ^k14dkP&e0Vkx<_$FD-6{n_U6g)&Gk%3S&-_ommKmW0JZFVH;4aw{CPGeRaJ2Gfgag!N+@zcV-SBM8VCe5o8*X%$5ueB# zC{WPXF$s8GaglimeUK$d*2DiOOR~GVHae#Jd$DW9Vp1i0jOGVafJbsu{Di6Ns1h83 z>j(-1$=~o!DjOo0S6D80njh%ZvN7zZ_@XG+cM+(DHF0kSy0iDV6#r>qv1lietRBg! z;`427h25+Zp6poZUrRJ55#`~)Apc~(E}sh4!pHbK6T9L(wRcR8Cask5_w=^F#xpyp zOUREsRVgyi4*WB-o%0d>+HDbUC;yeshnui%*tw*a8Wf7c9hv?>Et)P$!n!qFB9q`G zt!60fA58=AesnIquJ*``9+&~x@BKx12%SH$zbOT9*P*7;8U!8v$!WyRkTYE7fj6R2 z{?l26rybG9)7nwg_NL@MbC~MXdR4qn_f(WeaNtMehx@s>I+93kZ`@z9C0lU&9SQ6* zwv_qCR8=Rd{_#DKw8C>FLmVpdHBn7^nVd!J6h8&>m}#TVrQpLCOqOXM%omqoWFeJj2b#l9tqTC7f<$p1&s4;2Eris}qWyys5in~AO7v4K&l1ko+9Ezu@xIv?kC zxv@B5bSjaYZy?M3ZOB(_9y^qgD%`3Dk|H!gw3usHQ3~!BcR+d<_o}ExH>DbSCHyGm zk+MOqRC*a#AI^5K5zk{^5w*B+crJYgR}(iR2ZflrF&iIiP}z`C7nC5KAM_}O

9WBC#|K6aDitDim!t)(vTUGSy@BV@dEmj8>dn?IqTpL#CW0%(hp3a_ri z!AoV`L4#b#IwD_sV6#vrna$3n&Xa#KKf?Vv%D-0fm>(h!ksMJE>Pj|;ru%}aB}x-p zQ_&NG7&PEvkNK`LR8cS4JlP-2eDYE{N63JQ#ERfy@1u(A=rm%v?+ybpjj`X-!DIvS zFQHaEHN2IwQtD+}?VRnd>Te@6diLRAz7KgCXven6`beGj)pCtsYlFl2!F#5N;kku=@B8+m-Te z->KTQJz>q1qPCJQ%ybWkKjPGQR>cHxIJtot54GLDshnXx<1qCZ-R5SAAi0nJTL{A& z*bStJJqnK4_mT;C4uNIjUijyOH?R=X4cx^{qok^;qC>4md+*09Ne*1@e=3||OBEK! zM=C;QF$aKij6k}1Lx9q4C~c}pVXLxMA`nP$l*w6u1eo#Ot+Q?ib6m-8TMYMP&C@rkSfiT$_eCgJHsvHxPc z|1+tuiBIWwD>Vm0)C24sGgZ=#JS%If=t<3CelkD9eox+R6W0lEA216urOn(`;EA+L zohNDQtDe0K>z-X02r(D1iZl~-5IZBBm46^Mwg>_%nK#rCNi}jeTst!#ohSTa$9Pu3 z4^@9KFZ|=mx?(NmLxUD#QpVl1TB>bO6L`B1S4<6-0~6`( z3i3myl^#bBC8W_RGl*NvSc66p;U^%+)y;sH2acyTRi4lsQ?Fv@pD{|j~K0BaM&JZ1>@(%Kte;ae^a z!PDh<8xIo9BdMO&Y0O-{20tn0b-Wck0ap_4P^oY$`2+sa{s(%jOlGp;t>QJJe*z~N z@XEwzq!M4#@kxB>2y+~>4Y5nQpQvb06RWAANGdWfbOZ3k^+fGilUoz$0{bZ#!WZB* zm0V>i*<&pEG{)x3R6$)q&sWMkR9I%fvGLt#!Mlt^~pFpbX1*AvJEQt*vc6zSnZl z>Xo|Le&efc?S(#k`}mFIE$d+Zk6}dINogdy*ERuvmT-tMgWa@Gk%(8;?ywC#VlPar ztGth^VuxcZ8h1sQ8v`G=R|O}qGspp9g|Yz|BnKQ_9Y$mo_bsWC}JJmMb_Xl-Z z>c?Gh+_Z++Ut&oK)hTP7HYG#>-_>EqUG`n9>|SJCYU``!GfxVxuOz71_B-@K!n*eDPla`$Pqd z0n_jo(gArVwnrMpCBRO4Ei{kW7KC^jg3i@*h+MDx;5Fxhnv?Pxus5h+?4o>-Nwpylk`Cg#f=^sjM;=OS5ztP-zC z+k`6cJYSc%XhhXQRUdLy>85lf8-nMm1Emr;*|A-1T5m5nyrY6FtB|OVs^@56zatzK z=b=XvZh}7cM#{5{>7a{1g2PG*7)duokMJEs5JckJiZTQavXR!|JHS4*L~KTmR5rQo_7{b%W0xK( z20Z&cIGRsjGlPA7UC71uEJIP|FLax}6L_r53MO(q_flCCT_5)HjA5}Q$Nf|+VN+ZU zWQ2W-}8UC-FEd_}zmH=cR&D2hqfvdEs@Og{v$P)9-1m1eKsuMp!dxqw)y;BkY$7U3=x8n&ZTx{^Tz+ZOG1+d4ccY zdn{{|wWfzI3zv-ME4Q>JXyD%_RA9E3HWYeG4NMsv%keD2v=BPstE5%JjYF3YP)Y;q zG|-qYrUy`+;I7JIXrQo+Y$UD__X}pKJKGD?K$fw``D3Q1#@46!ME zjJ-yzQ7o#U=$dz>bmn(zH?9P0$D8?ps>dA)Aa!UPcx$%gTV`sn5eD_RV}TX)c(@zY zmRlfNh_-ktcFLQq)Z`kQF9b@NzDfaLm5IDg-^!IER3iGD`}(qB)RzmYC?6yY30j=~ z7-hb>8{P^@!b8Py{2VtA%!e=VN%?;2BACEsdB2i9!8a@yEJ0gBcKcbeKw1cn3)QeZ zcXztjGE*QmZu_9$re%Ter+uWU+k=SKOUqe}TR#s>=aA z47=%N%zYcCplU7b0UwBoWo9K-6zcKQ*i*pgchdLyq1JO|y_kW_U}xYR>7U_?r1P$O zBxe}F4OBC@GsYY0AEt-tifb$wBMsx8;t%7hE2EGy0Lyx=F%N@F>h0*VSZbtuuC|OyAkbybF}uh3RoUmEdNGVTQQkJk3>V3GF0U=Kxfu% z9H)*_wt;X(C#FF)9j);P!7)}mc{^XHx;F7>FGN z%^WMjn!78g&)`&kT4Zyk=k9CnG-ROvqN57B*zzUDZq&KP;YR|0#58;ql_$9Ii}WU5 z%w3aoqt@bQq`%20XoT7X&_V!R9rKsm617O_ z(O-pMd{@Ivv>tQ9)e@V_?ocDpc;RE-Ev}V&lI{S!KkOj#T?`FWkW#WUjecpY)YvLB zsd7~%-_wXha>IUrbUf*fsx& zaZkw;>XKK}-t?GlSVBwwDSm*hgSVg=<~TYDDlqGaCVXFUilznb+Z5%QsR5LYG?S35e8(2#80_G?BSEPG(%ITc+~SUv7KuFD z5yX2W=>XhFOo!)k2Zd%*e|Qx85zLIYAUDOn=BL~fCRUqT9YA?0#FSv)4B6#rl8S`; z?8r(hq@1&-uxZk2b|;G2YshajZL7KeRqlMDnKGdugQ^sFleH+#)x*lDLddt)cF