diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000..201b4f9 --- /dev/null +++ b/Cartfile @@ -0,0 +1 @@ +github "stripe/stripe-ios" == 24.1.0 diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..1e35897 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,257 @@ +# Changelog + +## v4.1.0 (Dec 3, 2024) + +### Updates +- `OPPaymentCardDetailsView`: Added new `becomeFirstResponder` method to allow for setting a specific field as first reponder. +- `OPPaymentCardDetailsForm`: Added new `becomeFirstResponder` method to allow for setting a specific field as first reponder. +- `OPPaymentCardCvvView`: Added `intrinsicContentSize` to properly account for the error message size. + +### Bug Fixes +- `OPPaymentCardDetailsView`: Fix `intrinsicContentSize` to properly account for the error message size. + +### Dependency Updates +- Updated to Stripe iOS v24.1.0 + +## v4.0.3 (Oct 25, 2024) + +### Bug Fixes +- Fix issue with SDK assets not loading properly when using Swift Package Manager + +## v4.0.2 (Jun 20, 2024) + +### Updates +- `OPPaymentCardDetailsView`: Add new property for setting alignment of the built in error message text +- `OPPaymentCardCvvView`: Add new property for setting alignment of the built in error message text +- `OPPaymentCardCvvView`: Fixed issue with text color not updating immediately after being set +- TestHarness: Fixed custom error message for invalid card numbers + +### Bug Fixes +- Fixed crash on SDK initialization/setup + +### Dependency Updates +- Updated to Stripe iOS v23.27.3 +- Xcode 14 is [no longer supported by Apple](https://developer.apple.com/news/upcoming-requirements/?id=04292024a). Please upgrade to Xcode 15 or later. + +## v4.0.1 (Mar 20, 2024) + +### Updates +- Added Swift Package Manager Support +- Deprecated `OPSetupParameters.freshSetup` +- Added public getter for `OloPayAPI.environment` + +#### Dependency Updates +- Updated to Stripe iOS v23.24.1 + +## v4.0.0 (Oct 27, 2023) + +#### Breaking Changes +- `OloPayAPI`: Removed previously deprecated versions of `createPaymentMethod(...)` +- Changed all references of `CVC` to `CVV` + - See: `OPCardErrorType` + - See: `OPCardField` + - See: `OPStrings` +- Removed `OPCardErrorType.incorrectNumber` and merged it's use case with `OPCardErrorType.invalidNumber` +- Removed `OPCardErrorType.incorrectZip` and merged it's use case with `OPCardErrorType.invalidZip` +- Changed the method signature of `OPCardErrorMessageBlock` + - See: `OPPaymentCardDetailsView.errorMessageHandler` +- `OPPaymentCardDetailsView` + - `OPPaymentCardDetailsView.getPaymentMethodParams(...)` no longer throws an exception if card details are invalid and instead returns `nil` + +#### Updates +- Added support for CVV tokenization + - See: new `OPPaymentCardCvvView` control + - See: `OloPayAPI.createCvvUpdateToken(...)` +- Added new callback methods to delegates that do not contain a UI parameter to allow for better separation of UI and data layers + - See: `OPPaymentCardDetailsFormDelegate` + - See: `OPPaymentCardDetailsViewDelegate` +- Improved support for handling unsupported card brands +- Added `OPPaymentMethodProtocol.environment` property to know what environment was used to create a payment method +- `OPPaymentCardDetailsView` + - Updated default error font to respect user's font scaling settings + - Added `OPPaymentCardDetailsView.fieldStates` property (and `OPPaymentCardDetailsView.fieldStatesObjc` for Obj-c support) + - Deprecated all properties that make use of `CVC` in favor of new ones that make use of `CVV` + - Changed default placeholder for postal code field to "Postal Code" regardless of country setting + - Improvded algorithm for detecting and displaying error messages to the user +- Test Harness Improvements + - New tabbed interface for each main aspect of the Olo Pay SDK: Credit Cards, Apple Pay, CVV Tokenization + - Updated to use MVVM architecture + +#### Bug Fixes +- `OPPaymentCardDetailsForm`: Fixed bug in `becomeFirstResponder()` that prevented the control from becoming the first responder +- `OPPaymentCardDetailsView` + - Fixed bug with error message displaying when calling `OPPaymentCardDetailsView.clear(...)` + - Fixed bug preventing an error from displaying when pressing the back button on the keyboard + +#### Dependency Updates +- Updated to Stripe iOS v23.17.2 + +## v3.0.0 (July 14, 2023) + +#### Breaking Changes +- `OPApplePayContextProtocol:` Added `throws` to signature of `presentApplePay(...)` +- `OPApplePayContext:` Added `throws` to signature of `presentApplePay(...)`, which will now throw errors for an empty or missing merchant id or company label + +#### Updates +- `OPApplePayContext`: General improvements to the Apple Pay flow +- `OPApplePayContextProtocol:` Added `presentApplePay(...)` overload that also takes a merchant id and company label as parameters +- `OPApplePayContext:` Added `presentApplePay(...)` overload that also takes a merchant id and company label as parameters +- `OPApplePayContextError:` Added `emptyCompanyLabel` and `emptyMerchantId` enum values + +## v2.1.6 (Jun 16, 2023) + +#### Updates +- Improved caching mechanism when switching between Test and Production environments during development +- Fix incorrect title for CocoaPods Setup documentation + +#### Dependency Updates +- Updated to Stripe iOS v23.9.0 + +## v2.1.5 (Dec 9, 2022) + +#### Bug Fixes +- Fixed bug with CocoaPods referencing an older version of Stripe iOS SDK + +## v2.1.4 (Dec 5, 2022) + +#### Updates +- Reverted Podspec back use https instead of ssh (as recommended by CocoaPods) +- Updated CocoaPods, Carthage, and Manual setup documentation + +#### Dependency Updates +- Updated to Stripe iOS v23.2.0 (see updated setup instructions) + +## v2.1.3 (Nov 15, 2022) + +#### Updates +- Fixed typo in podspec source url + +## v2.1.2 (Nov 15, 2022) + +#### Updates +- Changed Podspec to use ssh instead of https +- Fixed missing setup guides in documentation + +## v2.1.1 (Nov 9, 2022) + +#### Updates +- Added missing podspec file to fix CocoaPods usage +- Updated Carthage usage guide with proper tag syntax + +## v2.1.0 (Nov 7, 2022) + +#### Breaking Changes +- Added Carthage support (Note: Build path of Stripe dependencies changed) + +#### Updates +- Added CocoaPods support +- `OPPaymentCardDetailsView`: Add US and CA postal code validation + +## v2.0.0 (Sep 27, 2022) + +#### Breaking Changes +- Removed `OloPaySDK-Dev` target +- Added `OPEnvironment` enum +- `OPSetupParams`: Added environment parameter and reordered existing parameters + +#### Bug Fixes +- Fixed Xcode 14 compilation error +- `OPPaymentCardDetailsView`: Fixed postal code error message displaying when it shouldn't + +#### Dependency Updates +- Update to Stripe iOS v22.8.1 + +## v1.2.1 (May 18, 2022) + +#### Bug Fixes +- Fixed typo in error message for empty CVC fields +- Fixed test harness incorrectly logging whether the `OPPaymentCardDetailsForm` is valid or not + +#### Updates +- `OPPaymentCardDetailsView`: Error message now displays if `getPaymentMethodParams()` is called and card details are invalid +- `OPPaymentCardDetailsView`: Added `hasErrorMessage(...)` +- `OPPaymentCardDetailsView`: Added `getErrorMessage(...)` +- `OPCardBrand`: Added `unsupported` value +- Updated card error messages to distinguish between invalid card numbers and unsupported card numbers +- Added "Log Form Valid Changes" option to Test Harness settings +- Removed Carthage files to reduce SDK download size + +## 1.2.0 (Feb 28, 2022) + +#### Bug Fixes +- Fixes for hybrid app solutions (e.g. React Native) +- Test Harness compilation fixes after updating Stripe SDK + +#### Updates +- Official React Native bridging files support +- Added SDK setup steps to documentation + +#### Dependency Updates +- Update to Stripe iOS v21.12.0 + +## 1.1.3 (Dec 17, 2021) + +#### Updates +- `OloPayAPI`: Added safeguard to `createPaymentRequest(...)` for Apple Pay merchant id and company name +- `OloPayAPI`: : Added more robust retry mechanism in `createPaymentMethod(...)` +- `OloPayApiInitializer`: Added completion handler to `setup(...)` +- `OPError`: Added public constructor +- `OPPaymentMethod`: Added `country` property +- `OPPaymentMethodProtocol`: Added `country` property +- Added unit tests + +## 1.1.2 (Oct 22, 2021) + +#### Updates +- Minor Xcode project tweaks + +## 1.1.1 (Oct 22, 2021) + +#### Updates +- Minor Xcode project tweaks + +## 1.1.0 (Oct 21, 2021) + +#### Breaking Changes +- Removed `OloPayAPIGateway` (`OloPayAPI` should now be used directly) +- `OloPayAPI`: Removed `setup()` method +- `OloPayApiInitializer`: Added `setup()` method + +#### Bug Fixes +- Fixed issue with Carthage always pulling the latest version of the Stripe SDK +- `OPPaymentCardDetailsView`: Added missing `@objc` annotations + +#### Updates +- Added missing documentation +- New Classes/Protocols/Enums + - Added `OloPayApiInitializer` + - Added `OloPayAPIProtocol` + - Added `OPPaymentCardDetailsForm` + - Added `OPPaymentMethodProtocol` + - Added `OPPaymentMethodParams` + - Added `OPPaymentMethodParamsProtocol` + - Added `OPPaymentCardDetailsForm` + - Added `OPCardFormStyle` + - Added `OPStrings` +- `OloPayAPI`: Added `createPaymentMethod(...)` that takes an instance of `OPPaymentMethodParamsProtocol` +- `OloPayAPI`: Deprecated `createPaymentMethod(...)` that takes an instance of `OPPaymentCardDetailsForm` +- `OloPayAPI`: Deprecated `createPaymentMethod(...)` that takes an instance of `OPPaymentCardDetailsView` +- `OloPayAPI`: More robust error handling in `createPaymentMethod(...)` +- `OPPaymentCardDetailsView`: Added `expirationIsValid` convenience property +- `OPPaymentCardDetailsView`: Added `expirationIsEmpty` convenience property +- `OPPaymentCardDetailsView`: Added `postalCodeIsEmpty` convenience property +- `OPPaymentCardDetailsView`: Added ability to provide custom error messages via `errorMessageHandler` property +- `OPPaymentCardDetailsView`: Added ability to turn off display of error messages +- `OPPaymentCardDetailsView`: Added `errorMessage` property +- `OPPaymentCardDetailsView`: Added `getUserFacingMessage(...)` to get an error message for a specific field +- `OPPaymentCardDetailsView`: Added `getPaymentMethodParams(...)` + +## 1.0.1 (Sep 24, 2021) + +#### Updates +- `OPPaymentCardDetailsView`: `isValid` property now returns false if the card type isn't supported by Olo Pay +- `OPPaymentCardDetailsView`: New properties to check the validity of each card field +- `OloPayAPI`: `createPaymentMethod(...)` returns an error if the card type isn't supported by Olo Pay + +## 1.0.0 +- Initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6a7d9fd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +**Olo Pay Software Development Kit License Agreement** + +Copyright © 2022 Olo Inc. All rights reserved. + +Subject to the terms and conditions of the license, you are hereby granted a non-exclusive, worldwide, royalty-free license to (a) copy and modify the software in source code or binary form for your use in connection with the software services and interfaces provided by Olo, and (b) redistribute unmodified copies of the software to third parties. The above copyright notice and this license shall be included in or with all copies or substantial portions of the software. + +Your use of this software is subject to the Olo APIs Terms of Use, available at https://www.olo.com/api-usage-terms. This license does not grant you permission to use the trade names, trademarks, service marks, or product names of Olo, except as required for reasonable and customary use in describing the origin of the software and reproducing the content of this license. + +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. + diff --git a/OloPaySDK.podspec b/OloPaySDK.podspec new file mode 100644 index 0000000..094a0f5 --- /dev/null +++ b/OloPaySDK.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |spec| + spec.name = "OloPaySDK" + spec.version = "4.1.0" + spec.summary = "A CocoaPods library for the Olo Pay SDK written in Swift" + spec.description = <<-DESC + Olo Pay is an E-commerce payment solution designed to help restaurants grow, protect, and support their digital ordering and delivery business. Olo Pay is specifically designed for digital restaurant ordering to address the challenges and concerns that weʼve heard from thousands of merchants. + DESC + + spec.homepage = "https://github.com/ololabs/olo-pay-ios-sdk-releases" + spec.license = { :type => "Olo Pay SDK License", :file => "LICENSE.md" } + spec.author = "Olo, Inc." + + spec.platform = :ios + spec.ios.deployment_target = "13.0" + spec.swift_version = "5.0" + + spec.source = { :git => "https://github.com/ololabs/olo-pay-ios-sdk-releases.git", :tag => "#{spec.version}" } + spec.source_files = "**/src/OloPaySDK/OloPaySDK/**/*.{h,m,swift}" + spec.public_header_files = "**/src/OloPaySDK/OloPaySDK/**/*.h" + spec.dependency "Stripe", "24.1.0" +end diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..79250d0 --- /dev/null +++ b/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.7.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "OloPaySDK", + platforms: [.iOS(.v13)], + products: [ + .library( + name: "OloPaySDK", + targets: ["OloPaySDK"] + ), + ], + dependencies: [ + .package(url: "https://github.com/stripe/stripe-ios.git", exact: "24.1.0") + ], + targets: [ + .target( + name: "OloPaySDK", + dependencies: [ + .product(name: "Stripe", package: "stripe-ios"), + ], + path: "src/OloPaySDK/OloPaySDK" + ), + .testTarget( + name: "OloPaySDKTests", + dependencies: ["OloPaySDK"], + path: "src/OloPaySDK/OloPaySDKTests" + ), + ], + swiftLanguageVersions: [.v5] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c62e32 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Olo Pay iOS SDK + +## About Olo Pay +[Olo Pay](https://www.olo.com/solutions/pay/) is an E-commerce payment solution designed to help restaurants grow, protect, and support their digital ordering and delivery business. Olo Pay is specifically designed for digital restaurant ordering to address the challenges and concerns that weʼve heard from thousands of merchants. + +## About the SDK +The Olo Pay iOS SDK allows partners to easily add PCI-compliant credit card and Apple Pay functionality to their checkout flow and seamlessly integrate with the Olo Ordering API. This repo contains source code and documentation for the Olo Pay iOS SDK. Use of the SDK is subject to the terms of the [Olo Pay SDK License](https://github.com/ololabs/olo-pay-ios-sdk-releases/blob/main/LICENSE.md). + +The Olo Pay SDK supports Carthage, CocoaPods, and Swift Package Manager. + +## SDK Documentation + +Documentation for the [current version](https://ololabs.github.io/olo-pay-ios-sdk-releases/index.html) of the Olo Pay SDK, including setup instructions and full class documentation, are available online. Follow the steps in the `Setup` and `Getting Started` sections to begin integrating the SDK into your app. Documentation for [older versions](https://ololabs.github.io/olo-pay-ios-sdk-releases/versions/index.html) of the SDK are also available if needed. + +For more information about integrating Olo Pay into your payment solutions, refer to our [Olo Pay Dev Portal Documentation](https://developer.olo.com/docs/load/olopay) _(Note: requires an Olo Developer account)_. + +## Example App + +This repo contains a fully-functional example app that can be used as a guide for integrating and working with the Olo Pay iOS SDK. Source code for the example app can be found [here](https://github.com/ololabs/olo-pay-ios-sdk-releases/tree/main/src/TestHarness/iOS). Details about using and building the example app can be found in the `SDK Test Harness App` section of our [Olo Pay Dev Portal's iOS SDK Documentation](https://developer.olo.com/docs/load/olopay#section/Native-iOS/Using-the-iOS-SDK) _(Note: requires an Olo Developer account)_. \ No newline at end of file diff --git a/carthage-update.sh b/carthage-update.sh new file mode 100755 index 0000000..a322f8d --- /dev/null +++ b/carthage-update.sh @@ -0,0 +1,2 @@ +#!/bin/bash +carthage update --use-xcframeworks --no-use-binaries --platform iOS diff --git a/src/OloPaySDK/OloPaySDK.xcodeproj/project.pbxproj b/src/OloPaySDK/OloPaySDK.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1ea1b0b --- /dev/null +++ b/src/OloPaySDK/OloPaySDK.xcodeproj/project.pbxproj @@ -0,0 +1,820 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0621EC05276911C900153AB9 /* CardBrandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0621EC04276911C900153AB9 /* CardBrandTests.swift */; }; + 0621EC0A2769133200153AB9 /* PaymentStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0621EC092769133200153AB9 /* PaymentStatusTests.swift */; }; + 0621EC1527691A2300153AB9 /* ErrorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0621EC1427691A2300153AB9 /* ErrorTypeTests.swift */; }; + 0621EC2027691D4600153AB9 /* CardFormStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0621EC1F27691D4600153AB9 /* CardFormStyleTests.swift */; }; + 0621EC2527692FC500153AB9 /* CardFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0621EC2427692FC500153AB9 /* CardFieldTests.swift */; }; + 0648C09B26DFCD6D0053966D /* OPPaymentMethodParamsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0648C09A26DFCD6D0053966D /* OPPaymentMethodParamsProtocol.swift */; }; + 0648C0A626DFF75C0053966D /* OPPaymentMethodProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0648C0A526DFF75C0053966D /* OPPaymentMethodProtocol.swift */; }; + 065C04B2263B3DAF002D9AF0 /* OloPaySDK.h in Headers */ = {isa = PBXBuildFile; fileRef = 065C04A4263B3DAF002D9AF0 /* OloPaySDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 06B0CB932761185A00BBBD06 /* CardErrorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B0CB922761185A00BBBD06 /* CardErrorTypeTests.swift */; platformFilter = ios; }; + 1B17E58C2A267C3300CE2107 /* OPMetadataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E58B2A267C3300CE2107 /* OPMetadataGenerator.swift */; }; + 1B17E58E2A26873900CE2107 /* OPPaymentMethodSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E58D2A26873900CE2107 /* OPPaymentMethodSource.swift */; }; + 1B17E5922A26BA9700CE2107 /* OPMetadataStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E5912A26BA9700CE2107 /* OPMetadataStrings.swift */; }; + 1B17E5962A28F96900CE2107 /* OPSdkWrapperInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E5952A28F96900CE2107 /* OPSdkWrapperInfo.swift */; }; + 1B17E5982A28FDB000CE2107 /* OPSdkBuildType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E5972A28FDB000CE2107 /* OPSdkBuildType.swift */; }; + 1B17E59A2A28FDBD00CE2107 /* OPSdkWrapperPlatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E5992A28FDBD00CE2107 /* OPSdkWrapperPlatform.swift */; }; + 1B17E59F2A2AAAD600CE2107 /* MetadataGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B17E58F2A269D9700CE2107 /* MetadataGeneratorTests.swift */; }; + 1B50D4AB29D5E8F20023F06C /* OPStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B50D4AA29D5E8F20023F06C /* OPStorageTests.swift */; }; + 1BC72D9B2C516B1500457E76 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC72D9A2C516B1500457E76 /* UIViewExtensions.swift */; }; + D001D85C2AC751410005E65B /* CardStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001D85B2AC751410005E65B /* CardStateTests.swift */; }; + D00E1049264ADB3800E708AB /* OPPaymentCardDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00E1048264ADB3800E708AB /* OPPaymentCardDetailsView.swift */; }; + D00EF3902747029E00BDA729 /* OloPayAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EF38F2747029E00BDA729 /* OloPayAPITests.swift */; platformFilter = ios; }; + D04422F62AAFAF69007858B2 /* OPCvvTokenParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04422F52AAFAF69007858B2 /* OPCvvTokenParams.swift */; }; + D04422F82AAFB009007858B2 /* OPPaymentMethodParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04422F72AAFB009007858B2 /* OPPaymentMethodParams.swift */; }; + D04422FA2AAFB05E007858B2 /* OPCvvUpdateToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04422F92AAFB05E007858B2 /* OPCvvUpdateToken.swift */; }; + D04422FC2AAFB2E4007858B2 /* OPPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04422FB2AAFB2E4007858B2 /* OPPaymentMethod.swift */; }; + D0495AC4268E7BE200825588 /* OPCardField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0495AC3268E7BE200825588 /* OPCardField.swift */; }; + D04B12FE28A2C1FC00A79092 /* OPEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B12FD28A2C1FC00A79092 /* OPEnvironment.swift */; }; + D04FC02F28F894080065BF1A /* Stripe.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC02E28F894080065BF1A /* Stripe.xcframework */; }; + D04FC03128F894130065BF1A /* Stripe3DS2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03028F894130065BF1A /* Stripe3DS2.xcframework */; }; + D04FC03328F8941E0065BF1A /* StripeCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03228F8941E0065BF1A /* StripeCore.xcframework */; }; + D04FC03528F894290065BF1A /* StripeApplePay.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03428F894290065BF1A /* StripeApplePay.xcframework */; }; + D04FC03728F894330065BF1A /* StripeUICore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03628F894330065BF1A /* StripeUICore.xcframework */; }; + D05B450B267A9E5C0074C9C9 /* OPStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B450A267A9E5C0074C9C9 /* OPStorage.swift */; }; + D05B450F267A9E7F0074C9C9 /* OPPublishableKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B450E267A9E7F0074C9C9 /* OPPublishableKey.swift */; }; + D05B4513267A9EED0074C9C9 /* OPHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B4512267A9EED0074C9C9 /* OPHelpers.swift */; }; + D05B451A267AA84D0074C9C9 /* OPApplePayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B4519267AA84D0074C9C9 /* OPApplePayContext.swift */; }; + D05B451E267AA9730074C9C9 /* OPTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B451D267AA9730074C9C9 /* OPTypeAliases.swift */; }; + D05B4529267AB0090074C9C9 /* OPPaymentStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B4528267AB0090074C9C9 /* OPPaymentStatus.swift */; }; + D05B4532267B8F6E0074C9C9 /* OPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B4531267B8F6E0074C9C9 /* OPError.swift */; }; + D05B4537267B9A4D0074C9C9 /* OPSetupParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B4536267B9A4D0074C9C9 /* OPSetupParameters.swift */; }; + D061F2F62A436D6800016443 /* OPApplePayLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D061F2F52A436D6800016443 /* OPApplePayLauncher.swift */; }; + D0677C56293AC02700ECE164 /* StripePayments.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C55293AC02700ECE164 /* StripePayments.xcframework */; }; + D0677C57293AC02700ECE164 /* StripePayments.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C55293AC02700ECE164 /* StripePayments.xcframework */; }; + D0677C59293AC06C00ECE164 /* StripePaymentsUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C58293AC06C00ECE164 /* StripePaymentsUI.xcframework */; }; + D0677C5A293AC06C00ECE164 /* StripePaymentsUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C58293AC06C00ECE164 /* StripePaymentsUI.xcframework */; }; + D06826762654575C008CF7A0 /* OPPaymentCardDetailsInternalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06826752654575C008CF7A0 /* OPPaymentCardDetailsInternalView.swift */; }; + D0697BE02A86A845004D59D6 /* OPPaymentCardCvvView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BDF2A86A845004D59D6 /* OPPaymentCardCvvView.swift */; }; + D0697BE32A86A8AF004D59D6 /* OPPaymentCardCvvTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BE22A86A8AF004D59D6 /* OPPaymentCardCvvTextField.swift */; }; + D0697C102A93CE65004D59D6 /* OPCardFieldState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C0F2A93CE65004D59D6 /* OPCardFieldState.swift */; }; + D0697C122A93CEB3004D59D6 /* OPCardFieldStateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C112A93CEB3004D59D6 /* OPCardFieldStateProtocol.swift */; }; + D0697C142A93D800004D59D6 /* OPCvvState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C132A93D800004D59D6 /* OPCvvState.swift */; }; + D0697C162A941F97004D59D6 /* CvvStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C152A941F97004D59D6 /* CvvStateTests.swift */; }; + D06EDCAD27175FCF00DF77B1 /* OloPayApiInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06EDCAC27175FCF00DF77B1 /* OloPayApiInitializer.swift */; }; + D077D3502756F911008E0A05 /* OloPayApiInitializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D077D34F2756F911008E0A05 /* OloPayApiInitializerTests.swift */; platformFilter = ios; }; + D08AAEBC26A776C900A171DE /* OPStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08AAEBB26A776C900A171DE /* OPStrings.swift */; }; + D08AAEC126AA1F8B00A171DE /* OPPaymentCardDetailsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08AAEC026AA1F8B00A171DE /* OPPaymentCardDetailsForm.swift */; }; + D08AAEC426AA231400A171DE /* OPCardFormStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08AAEC326AA231400A171DE /* OPCardFormStyle.swift */; }; + D08F49BC2662048400C9B204 /* OPCardBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08F49BB2662048400C9B204 /* OPCardBrand.swift */; }; + D0959BED268D24AE00EC7BE0 /* OPErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0959BEC268D24AE00EC7BE0 /* OPErrorType.swift */; }; + D0959BF1268D24E100EC7BE0 /* OPCardErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0959BF0268D24E100EC7BE0 /* OPCardErrorType.swift */; }; + D0959BF9268E288400EC7BE0 /* OPApplePayContextError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0959BF8268E288400EC7BE0 /* OPApplePayContextError.swift */; }; + D095E0102CD115C0001768A3 /* OPSdkBuild.swift in Sources */ = {isa = PBXBuildFile; fileRef = D095E00F2CD115C0001768A3 /* OPSdkBuild.swift */; }; + D095E0122CD11C13001768A3 /* OPSdkVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D095E0112CD11C13001768A3 /* OPSdkVersion.swift */; }; + D0A82A6526744B9900370561 /* OloPayAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A82A6426744B9900370561 /* OloPayAPI.swift */; }; + D0A82A692674508F00370561 /* OPStorageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A82A682674508F00370561 /* OPStorageWrapper.swift */; }; + D0C62CCA2A7D69A200A2308A /* OPCvvTokenParamsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C62CC92A7D69A200A2308A /* OPCvvTokenParamsProtocol.swift */; }; + D0C62CCD2A7D6EB600A2308A /* OPCvvUpdateTokenProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C62CCC2A7D6EB600A2308A /* OPCvvUpdateTokenProtocol.swift */; }; + D0C6E03C2AC7191B0006E6D1 /* OPCardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C6E03B2AC7191B0006E6D1 /* OPCardState.swift */; }; + D0C6E03F2AC719670006E6D1 /* OPValidStateChangedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C6E03E2AC719670006E6D1 /* OPValidStateChangedDelegate.swift */; }; + D0F3E1B828A4338700DFEA21 /* OloPaySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 065C04A1263B3DAF002D9AF0 /* OloPaySDK.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D0F3E1B628A4337B00DFEA21 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 065C0498263B3DAF002D9AF0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 065C04A0263B3DAF002D9AF0; + remoteInfo = OloPaySDK; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0621EC04276911C900153AB9 /* CardBrandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBrandTests.swift; sourceTree = ""; }; + 0621EC092769133200153AB9 /* PaymentStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStatusTests.swift; sourceTree = ""; }; + 0621EC1427691A2300153AB9 /* ErrorTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTypeTests.swift; sourceTree = ""; }; + 0621EC1F27691D4600153AB9 /* CardFormStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormStyleTests.swift; sourceTree = ""; }; + 0621EC2427692FC500153AB9 /* CardFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFieldTests.swift; sourceTree = ""; }; + 0648C09A26DFCD6D0053966D /* OPPaymentMethodParamsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentMethodParamsProtocol.swift; sourceTree = ""; }; + 0648C0A526DFF75C0053966D /* OPPaymentMethodProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPPaymentMethodProtocol.swift; sourceTree = ""; }; + 065C04A1263B3DAF002D9AF0 /* OloPaySDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OloPaySDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 065C04A4263B3DAF002D9AF0 /* OloPaySDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OloPaySDK.h; sourceTree = ""; }; + 065C04A5263B3DAF002D9AF0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 06B0CB922761185A00BBBD06 /* CardErrorTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardErrorTypeTests.swift; sourceTree = ""; }; + 1B17E58B2A267C3300CE2107 /* OPMetadataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMetadataGenerator.swift; sourceTree = ""; }; + 1B17E58D2A26873900CE2107 /* OPPaymentMethodSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentMethodSource.swift; sourceTree = ""; }; + 1B17E58F2A269D9700CE2107 /* MetadataGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataGeneratorTests.swift; sourceTree = ""; }; + 1B17E5912A26BA9700CE2107 /* OPMetadataStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMetadataStrings.swift; sourceTree = ""; }; + 1B17E5952A28F96900CE2107 /* OPSdkWrapperInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSdkWrapperInfo.swift; sourceTree = ""; }; + 1B17E5972A28FDB000CE2107 /* OPSdkBuildType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSdkBuildType.swift; sourceTree = ""; }; + 1B17E5992A28FDBD00CE2107 /* OPSdkWrapperPlatform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSdkWrapperPlatform.swift; sourceTree = ""; }; + 1B50D4AA29D5E8F20023F06C /* OPStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPStorageTests.swift; sourceTree = ""; }; + 1BC72D9A2C516B1500457E76 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; + D001D85B2AC751410005E65B /* CardStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStateTests.swift; sourceTree = ""; }; + D00E1048264ADB3800E708AB /* OPPaymentCardDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentCardDetailsView.swift; sourceTree = ""; }; + D00EF3822746D9EE00BDA729 /* OloPaySDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OloPaySDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D00EF3862746D9EE00BDA729 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D00EF38F2747029E00BDA729 /* OloPayAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloPayAPITests.swift; sourceTree = ""; }; + D04422F52AAFAF69007858B2 /* OPCvvTokenParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCvvTokenParams.swift; sourceTree = ""; }; + D04422F72AAFB009007858B2 /* OPPaymentMethodParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentMethodParams.swift; sourceTree = ""; }; + D04422F92AAFB05E007858B2 /* OPCvvUpdateToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCvvUpdateToken.swift; sourceTree = ""; }; + D04422FB2AAFB2E4007858B2 /* OPPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentMethod.swift; sourceTree = ""; }; + D0495AC3268E7BE200825588 /* OPCardField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardField.swift; sourceTree = ""; }; + D04B12FD28A2C1FC00A79092 /* OPEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPEnvironment.swift; sourceTree = ""; }; + D04FC02E28F894080065BF1A /* Stripe.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Stripe.xcframework; path = ../../Carthage/Build/Stripe.xcframework; sourceTree = ""; }; + D04FC03028F894130065BF1A /* Stripe3DS2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Stripe3DS2.xcframework; path = ../../Carthage/Build/Stripe3DS2.xcframework; sourceTree = ""; }; + D04FC03228F8941E0065BF1A /* StripeCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeCore.xcframework; path = ../../Carthage/Build/StripeCore.xcframework; sourceTree = ""; }; + D04FC03428F894290065BF1A /* StripeApplePay.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeApplePay.xcframework; path = ../../Carthage/Build/StripeApplePay.xcframework; sourceTree = ""; }; + D04FC03628F894330065BF1A /* StripeUICore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeUICore.xcframework; path = ../../Carthage/Build/StripeUICore.xcframework; sourceTree = ""; }; + D05B450A267A9E5C0074C9C9 /* OPStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPStorage.swift; sourceTree = ""; }; + D05B450E267A9E7F0074C9C9 /* OPPublishableKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPublishableKey.swift; sourceTree = ""; }; + D05B4512267A9EED0074C9C9 /* OPHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPHelpers.swift; sourceTree = ""; }; + D05B4519267AA84D0074C9C9 /* OPApplePayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPApplePayContext.swift; sourceTree = ""; }; + D05B451D267AA9730074C9C9 /* OPTypeAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPTypeAliases.swift; sourceTree = ""; }; + D05B4528267AB0090074C9C9 /* OPPaymentStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentStatus.swift; sourceTree = ""; }; + D05B4531267B8F6E0074C9C9 /* OPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPError.swift; sourceTree = ""; }; + D05B4536267B9A4D0074C9C9 /* OPSetupParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSetupParameters.swift; sourceTree = ""; }; + D061F2F52A436D6800016443 /* OPApplePayLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPApplePayLauncher.swift; sourceTree = ""; }; + D0677C55293AC02700ECE164 /* StripePayments.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripePayments.xcframework; path = ../../Carthage/Build/StripePayments.xcframework; sourceTree = ""; }; + D0677C58293AC06C00ECE164 /* StripePaymentsUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripePaymentsUI.xcframework; path = ../../Carthage/Build/StripePaymentsUI.xcframework; sourceTree = ""; }; + D06826752654575C008CF7A0 /* OPPaymentCardDetailsInternalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentCardDetailsInternalView.swift; sourceTree = ""; }; + D0697BDF2A86A845004D59D6 /* OPPaymentCardCvvView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentCardCvvView.swift; sourceTree = ""; }; + D0697BE22A86A8AF004D59D6 /* OPPaymentCardCvvTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentCardCvvTextField.swift; sourceTree = ""; }; + D0697C0F2A93CE65004D59D6 /* OPCardFieldState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardFieldState.swift; sourceTree = ""; }; + D0697C112A93CEB3004D59D6 /* OPCardFieldStateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardFieldStateProtocol.swift; sourceTree = ""; }; + D0697C132A93D800004D59D6 /* OPCvvState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCvvState.swift; sourceTree = ""; }; + D0697C152A941F97004D59D6 /* CvvStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CvvStateTests.swift; sourceTree = ""; }; + D06EDCAC27175FCF00DF77B1 /* OloPayApiInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloPayApiInitializer.swift; sourceTree = ""; }; + D077D34F2756F911008E0A05 /* OloPayApiInitializerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloPayApiInitializerTests.swift; sourceTree = ""; }; + D08AAEBB26A776C900A171DE /* OPStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPStrings.swift; sourceTree = ""; }; + D08AAEC026AA1F8B00A171DE /* OPPaymentCardDetailsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPPaymentCardDetailsForm.swift; sourceTree = ""; }; + D08AAEC326AA231400A171DE /* OPCardFormStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardFormStyle.swift; sourceTree = ""; }; + D08F49BB2662048400C9B204 /* OPCardBrand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardBrand.swift; sourceTree = ""; }; + D0959BEC268D24AE00EC7BE0 /* OPErrorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPErrorType.swift; sourceTree = ""; }; + D0959BF0268D24E100EC7BE0 /* OPCardErrorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardErrorType.swift; sourceTree = ""; }; + D0959BF8268E288400EC7BE0 /* OPApplePayContextError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPApplePayContextError.swift; sourceTree = ""; }; + D095E00F2CD115C0001768A3 /* OPSdkBuild.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSdkBuild.swift; sourceTree = ""; }; + D095E0112CD11C13001768A3 /* OPSdkVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPSdkVersion.swift; sourceTree = ""; }; + D0A82A6426744B9900370561 /* OloPayAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloPayAPI.swift; sourceTree = ""; }; + D0A82A682674508F00370561 /* OPStorageWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPStorageWrapper.swift; sourceTree = ""; }; + D0C62CC92A7D69A200A2308A /* OPCvvTokenParamsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCvvTokenParamsProtocol.swift; sourceTree = ""; }; + D0C62CCC2A7D6EB600A2308A /* OPCvvUpdateTokenProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCvvUpdateTokenProtocol.swift; sourceTree = ""; }; + D0C6E03B2AC7191B0006E6D1 /* OPCardState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPCardState.swift; sourceTree = ""; }; + D0C6E03E2AC719670006E6D1 /* OPValidStateChangedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPValidStateChangedDelegate.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 065C049E263B3DAF002D9AF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D04FC03328F8941E0065BF1A /* StripeCore.xcframework in Frameworks */, + D0677C56293AC02700ECE164 /* StripePayments.xcframework in Frameworks */, + D04FC03528F894290065BF1A /* StripeApplePay.xcframework in Frameworks */, + D04FC02F28F894080065BF1A /* Stripe.xcframework in Frameworks */, + D04FC03128F894130065BF1A /* Stripe3DS2.xcframework in Frameworks */, + D0677C59293AC06C00ECE164 /* StripePaymentsUI.xcframework in Frameworks */, + D04FC03728F894330065BF1A /* StripeUICore.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00EF37F2746D9EE00BDA729 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0F3E1B828A4338700DFEA21 /* OloPaySDK.framework in Frameworks */, + D0677C57293AC02700ECE164 /* StripePayments.xcframework in Frameworks */, + D0677C5A293AC06C00ECE164 /* StripePaymentsUI.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 065C0497263B3DAF002D9AF0 = { + isa = PBXGroup; + children = ( + 065C04A3263B3DAF002D9AF0 /* OloPaySDK */, + D00EF3832746D9EE00BDA729 /* OloPaySDKTests */, + 065C04A2263B3DAF002D9AF0 /* Products */, + 06B4ABB92670000C008AD27B /* Frameworks */, + ); + sourceTree = ""; + }; + 065C04A2263B3DAF002D9AF0 /* Products */ = { + isa = PBXGroup; + children = ( + 065C04A1263B3DAF002D9AF0 /* OloPaySDK.framework */, + D00EF3822746D9EE00BDA729 /* OloPaySDKTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 065C04A3263B3DAF002D9AF0 /* OloPaySDK */ = { + isa = PBXGroup; + children = ( + 065C04A5263B3DAF002D9AF0 /* Info.plist */, + D06EDCAB27175FAC00DF77B1 /* API */, + D05B4535267B99A10074C9C9 /* ApplePay */, + D0A82A7C267462B700370561 /* Internal */, + D08F49AE2661F83600C9B204 /* Data */, + D00E1047264AD34000E708AB /* Controls */, + 065C04A4263B3DAF002D9AF0 /* OloPaySDK.h */, + ); + path = OloPaySDK; + sourceTree = ""; + }; + 06B4ABB92670000C008AD27B /* Frameworks */ = { + isa = PBXGroup; + children = ( + D04FC03428F894290065BF1A /* StripeApplePay.xcframework */, + D0677C55293AC02700ECE164 /* StripePayments.xcframework */, + D0677C58293AC06C00ECE164 /* StripePaymentsUI.xcframework */, + D04FC03628F894330065BF1A /* StripeUICore.xcframework */, + D04FC03228F8941E0065BF1A /* StripeCore.xcframework */, + D04FC03028F894130065BF1A /* Stripe3DS2.xcframework */, + D04FC02E28F894080065BF1A /* Stripe.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1BC72D992C516AF200457E76 /* Extensions */ = { + isa = PBXGroup; + children = ( + 1BC72D9A2C516B1500457E76 /* UIViewExtensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + D00E1047264AD34000E708AB /* Controls */ = { + isa = PBXGroup; + children = ( + D00E1048264ADB3800E708AB /* OPPaymentCardDetailsView.swift */, + D08AAEC026AA1F8B00A171DE /* OPPaymentCardDetailsForm.swift */, + D0697BDF2A86A845004D59D6 /* OPPaymentCardCvvView.swift */, + ); + path = Controls; + sourceTree = ""; + }; + D00EF3832746D9EE00BDA729 /* OloPaySDKTests */ = { + isa = PBXGroup; + children = ( + D001D85B2AC751410005E65B /* CardStateTests.swift */, + D0697C152A941F97004D59D6 /* CvvStateTests.swift */, + 1B17E58F2A269D9700CE2107 /* MetadataGeneratorTests.swift */, + D077D34F2756F911008E0A05 /* OloPayApiInitializerTests.swift */, + D00EF38F2747029E00BDA729 /* OloPayAPITests.swift */, + D00EF3862746D9EE00BDA729 /* Info.plist */, + 06B0CB922761185A00BBBD06 /* CardErrorTypeTests.swift */, + 0621EC04276911C900153AB9 /* CardBrandTests.swift */, + 0621EC092769133200153AB9 /* PaymentStatusTests.swift */, + 0621EC1427691A2300153AB9 /* ErrorTypeTests.swift */, + 0621EC1F27691D4600153AB9 /* CardFormStyleTests.swift */, + 0621EC2427692FC500153AB9 /* CardFieldTests.swift */, + 1B50D4AA29D5E8F20023F06C /* OPStorageTests.swift */, + ); + path = OloPaySDKTests; + sourceTree = ""; + }; + D05B4503267A9B350074C9C9 /* ApplePay */ = { + isa = PBXGroup; + children = ( + D061F2F52A436D6800016443 /* OPApplePayLauncher.swift */, + ); + path = ApplePay; + sourceTree = ""; + }; + D05B4535267B99A10074C9C9 /* ApplePay */ = { + isa = PBXGroup; + children = ( + D05B4519267AA84D0074C9C9 /* OPApplePayContext.swift */, + ); + path = ApplePay; + sourceTree = ""; + }; + D06EDCAB27175FAC00DF77B1 /* API */ = { + isa = PBXGroup; + children = ( + D0A82A6426744B9900370561 /* OloPayAPI.swift */, + D06EDCAC27175FCF00DF77B1 /* OloPayApiInitializer.swift */, + ); + path = API; + sourceTree = ""; + }; + D08F49AE2661F83600C9B204 /* Data */ = { + isa = PBXGroup; + children = ( + D0959BF8268E288400EC7BE0 /* OPApplePayContextError.swift */, + D08F49BB2662048400C9B204 /* OPCardBrand.swift */, + D0959BF0268D24E100EC7BE0 /* OPCardErrorType.swift */, + D0495AC3268E7BE200825588 /* OPCardField.swift */, + D0697C112A93CEB3004D59D6 /* OPCardFieldStateProtocol.swift */, + D08AAEC326AA231400A171DE /* OPCardFormStyle.swift */, + D0C62CC92A7D69A200A2308A /* OPCvvTokenParamsProtocol.swift */, + D0C62CCC2A7D6EB600A2308A /* OPCvvUpdateTokenProtocol.swift */, + D04B12FD28A2C1FC00A79092 /* OPEnvironment.swift */, + D05B4531267B8F6E0074C9C9 /* OPError.swift */, + D0959BEC268D24AE00EC7BE0 /* OPErrorType.swift */, + 0648C09A26DFCD6D0053966D /* OPPaymentMethodParamsProtocol.swift */, + 0648C0A526DFF75C0053966D /* OPPaymentMethodProtocol.swift */, + D05B4528267AB0090074C9C9 /* OPPaymentStatus.swift */, + 1B17E5972A28FDB000CE2107 /* OPSdkBuildType.swift */, + 1B17E5952A28F96900CE2107 /* OPSdkWrapperInfo.swift */, + 1B17E5992A28FDBD00CE2107 /* OPSdkWrapperPlatform.swift */, + D05B4536267B9A4D0074C9C9 /* OPSetupParameters.swift */, + D08AAEBB26A776C900A171DE /* OPStrings.swift */, + D05B451D267AA9730074C9C9 /* OPTypeAliases.swift */, + ); + path = Data; + sourceTree = ""; + }; + D0A82A7C267462B700370561 /* Internal */ = { + isa = PBXGroup; + children = ( + 1BC72D992C516AF200457E76 /* Extensions */, + D05B4503267A9B350074C9C9 /* ApplePay */, + D0A82A7E267462CA00370561 /* Data */, + D0A82A7D267462C000370561 /* Controls */, + ); + path = Internal; + sourceTree = ""; + }; + D0A82A7D267462C000370561 /* Controls */ = { + isa = PBXGroup; + children = ( + D06826752654575C008CF7A0 /* OPPaymentCardDetailsInternalView.swift */, + D0697BE22A86A8AF004D59D6 /* OPPaymentCardCvvTextField.swift */, + ); + path = Controls; + sourceTree = ""; + }; + D0A82A7E267462CA00370561 /* Data */ = { + isa = PBXGroup; + children = ( + D0697C0F2A93CE65004D59D6 /* OPCardFieldState.swift */, + D0C6E03B2AC7191B0006E6D1 /* OPCardState.swift */, + D0697C132A93D800004D59D6 /* OPCvvState.swift */, + D04422F52AAFAF69007858B2 /* OPCvvTokenParams.swift */, + D04422F92AAFB05E007858B2 /* OPCvvUpdateToken.swift */, + D05B4512267A9EED0074C9C9 /* OPHelpers.swift */, + 1B17E58B2A267C3300CE2107 /* OPMetadataGenerator.swift */, + 1B17E5912A26BA9700CE2107 /* OPMetadataStrings.swift */, + D04422FB2AAFB2E4007858B2 /* OPPaymentMethod.swift */, + D04422F72AAFB009007858B2 /* OPPaymentMethodParams.swift */, + 1B17E58D2A26873900CE2107 /* OPPaymentMethodSource.swift */, + D05B450E267A9E7F0074C9C9 /* OPPublishableKey.swift */, + D05B450A267A9E5C0074C9C9 /* OPStorage.swift */, + D0A82A682674508F00370561 /* OPStorageWrapper.swift */, + D0C6E03E2AC719670006E6D1 /* OPValidStateChangedDelegate.swift */, + D095E00F2CD115C0001768A3 /* OPSdkBuild.swift */, + D095E0112CD11C13001768A3 /* OPSdkVersion.swift */, + ); + path = Data; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 065C049C263B3DAF002D9AF0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 065C04B2263B3DAF002D9AF0 /* OloPaySDK.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 065C04A0263B3DAF002D9AF0 /* OloPaySDK */ = { + isa = PBXNativeTarget; + buildConfigurationList = 065C04B5263B3DAF002D9AF0 /* Build configuration list for PBXNativeTarget "OloPaySDK" */; + buildPhases = ( + 065C049C263B3DAF002D9AF0 /* Headers */, + 065C049D263B3DAF002D9AF0 /* Sources */, + 065C049E263B3DAF002D9AF0 /* Frameworks */, + 065C049F263B3DAF002D9AF0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OloPaySDK; + productName = OloPaySDK; + productReference = 065C04A1263B3DAF002D9AF0 /* OloPaySDK.framework */; + productType = "com.apple.product-type.framework"; + }; + D00EF3812746D9EE00BDA729 /* OloPaySDKTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D00EF38E2746D9EE00BDA729 /* Build configuration list for PBXNativeTarget "OloPaySDKTests" */; + buildPhases = ( + D00EF37E2746D9EE00BDA729 /* Sources */, + D00EF37F2746D9EE00BDA729 /* Frameworks */, + D00EF3802746D9EE00BDA729 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D0F3E1B728A4337B00DFEA21 /* PBXTargetDependency */, + ); + name = OloPaySDKTests; + productName = OloPaySDKTests; + productReference = D00EF3822746D9EE00BDA729 /* OloPaySDKTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 065C0498263B3DAF002D9AF0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1240; + TargetAttributes = { + 065C04A0263B3DAF002D9AF0 = { + CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1420; + }; + D00EF3812746D9EE00BDA729 = { + CreatedOnToolsVersion = 12.5.1; + }; + }; + }; + buildConfigurationList = 065C049B263B3DAF002D9AF0 /* Build configuration list for PBXProject "OloPaySDK" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 065C0497263B3DAF002D9AF0; + productRefGroup = 065C04A2263B3DAF002D9AF0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 065C04A0263B3DAF002D9AF0 /* OloPaySDK */, + D00EF3812746D9EE00BDA729 /* OloPaySDKTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 065C049F263B3DAF002D9AF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00EF3802746D9EE00BDA729 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 065C049D263B3DAF002D9AF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D061F2F62A436D6800016443 /* OPApplePayLauncher.swift in Sources */, + 0648C09B26DFCD6D0053966D /* OPPaymentMethodParamsProtocol.swift in Sources */, + D08AAEC126AA1F8B00A171DE /* OPPaymentCardDetailsForm.swift in Sources */, + D0959BED268D24AE00EC7BE0 /* OPErrorType.swift in Sources */, + D05B451E267AA9730074C9C9 /* OPTypeAliases.swift in Sources */, + D04422F62AAFAF69007858B2 /* OPCvvTokenParams.swift in Sources */, + D05B450F267A9E7F0074C9C9 /* OPPublishableKey.swift in Sources */, + D0C6E03F2AC719670006E6D1 /* OPValidStateChangedDelegate.swift in Sources */, + 1B17E5982A28FDB000CE2107 /* OPSdkBuildType.swift in Sources */, + D0697C142A93D800004D59D6 /* OPCvvState.swift in Sources */, + D08F49BC2662048400C9B204 /* OPCardBrand.swift in Sources */, + D05B4513267A9EED0074C9C9 /* OPHelpers.swift in Sources */, + D04B12FE28A2C1FC00A79092 /* OPEnvironment.swift in Sources */, + 1B17E5962A28F96900CE2107 /* OPSdkWrapperInfo.swift in Sources */, + D0697BE02A86A845004D59D6 /* OPPaymentCardCvvView.swift in Sources */, + D05B450B267A9E5C0074C9C9 /* OPStorage.swift in Sources */, + 1BC72D9B2C516B1500457E76 /* UIViewExtensions.swift in Sources */, + D08AAEC426AA231400A171DE /* OPCardFormStyle.swift in Sources */, + D04422F82AAFB009007858B2 /* OPPaymentMethodParams.swift in Sources */, + 1B17E58C2A267C3300CE2107 /* OPMetadataGenerator.swift in Sources */, + D05B4532267B8F6E0074C9C9 /* OPError.swift in Sources */, + D06EDCAD27175FCF00DF77B1 /* OloPayApiInitializer.swift in Sources */, + 1B17E58E2A26873900CE2107 /* OPPaymentMethodSource.swift in Sources */, + D0C62CCA2A7D69A200A2308A /* OPCvvTokenParamsProtocol.swift in Sources */, + D05B4537267B9A4D0074C9C9 /* OPSetupParameters.swift in Sources */, + D095E0122CD11C13001768A3 /* OPSdkVersion.swift in Sources */, + D0959BF9268E288400EC7BE0 /* OPApplePayContextError.swift in Sources */, + D0A82A6526744B9900370561 /* OloPayAPI.swift in Sources */, + D0697C122A93CEB3004D59D6 /* OPCardFieldStateProtocol.swift in Sources */, + D0959BF1268D24E100EC7BE0 /* OPCardErrorType.swift in Sources */, + D0495AC4268E7BE200825588 /* OPCardField.swift in Sources */, + D00E1049264ADB3800E708AB /* OPPaymentCardDetailsView.swift in Sources */, + D0C62CCD2A7D6EB600A2308A /* OPCvvUpdateTokenProtocol.swift in Sources */, + 1B17E5922A26BA9700CE2107 /* OPMetadataStrings.swift in Sources */, + 1B17E59A2A28FDBD00CE2107 /* OPSdkWrapperPlatform.swift in Sources */, + D06826762654575C008CF7A0 /* OPPaymentCardDetailsInternalView.swift in Sources */, + D05B4529267AB0090074C9C9 /* OPPaymentStatus.swift in Sources */, + D04422FC2AAFB2E4007858B2 /* OPPaymentMethod.swift in Sources */, + D0697BE32A86A8AF004D59D6 /* OPPaymentCardCvvTextField.swift in Sources */, + D05B451A267AA84D0074C9C9 /* OPApplePayContext.swift in Sources */, + D095E0102CD115C0001768A3 /* OPSdkBuild.swift in Sources */, + D0A82A692674508F00370561 /* OPStorageWrapper.swift in Sources */, + 0648C0A626DFF75C0053966D /* OPPaymentMethodProtocol.swift in Sources */, + D08AAEBC26A776C900A171DE /* OPStrings.swift in Sources */, + D0697C102A93CE65004D59D6 /* OPCardFieldState.swift in Sources */, + D04422FA2AAFB05E007858B2 /* OPCvvUpdateToken.swift in Sources */, + D0C6E03C2AC7191B0006E6D1 /* OPCardState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00EF37E2746D9EE00BDA729 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B17E59F2A2AAAD600CE2107 /* MetadataGeneratorTests.swift in Sources */, + 0621EC2527692FC500153AB9 /* CardFieldTests.swift in Sources */, + D001D85C2AC751410005E65B /* CardStateTests.swift in Sources */, + 0621EC0A2769133200153AB9 /* PaymentStatusTests.swift in Sources */, + 0621EC05276911C900153AB9 /* CardBrandTests.swift in Sources */, + 06B0CB932761185A00BBBD06 /* CardErrorTypeTests.swift in Sources */, + 0621EC2027691D4600153AB9 /* CardFormStyleTests.swift in Sources */, + 1B50D4AB29D5E8F20023F06C /* OPStorageTests.swift in Sources */, + D00EF3902747029E00BDA729 /* OloPayAPITests.swift in Sources */, + 0621EC1527691A2300153AB9 /* ErrorTypeTests.swift in Sources */, + D077D3502756F911008E0A05 /* OloPayApiInitializerTests.swift in Sources */, + D0697C162A941F97004D59D6 /* CvvStateTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D0F3E1B728A4337B00DFEA21 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 065C04A0263B3DAF002D9AF0 /* OloPaySDK */; + targetProxy = D0F3E1B628A4337B00DFEA21 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 065C04B3263B3DAF002D9AF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + 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_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 065C04B4263B3DAF002D9AF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + 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_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 065C04B6263B3DAF002D9AF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = OloPaySDK/Info.plist; + "INFOPLIST_FILE[sdk=*]" = OloPaySDK/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.1.3; + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + 065C04B7263B3DAF002D9AF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = OloPaySDK/Info.plist; + "INFOPLIST_FILE[sdk=*]" = OloPaySDK/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.1.3; + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; + D00EF38A2746D9EE00BDA729 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D00EF38C2746D9EE00BDA729 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 065C049B263B3DAF002D9AF0 /* Build configuration list for PBXProject "OloPaySDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 065C04B3263B3DAF002D9AF0 /* Debug */, + 065C04B4263B3DAF002D9AF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 065C04B5263B3DAF002D9AF0 /* Build configuration list for PBXNativeTarget "OloPaySDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 065C04B6263B3DAF002D9AF0 /* Debug */, + 065C04B7263B3DAF002D9AF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D00EF38E2746D9EE00BDA729 /* Build configuration list for PBXNativeTarget "OloPaySDKTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00EF38A2746D9EE00BDA729 /* Debug */, + D00EF38C2746D9EE00BDA729 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 065C0498263B3DAF002D9AF0 /* Project object */; +} diff --git a/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDK.xcscheme b/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDK.xcscheme new file mode 100644 index 0000000..3884914 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDK.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDKTests.xcscheme b/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDKTests.xcscheme new file mode 100644 index 0000000..c2861d1 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK.xcodeproj/xcshareddata/xcschemes/OloPaySDKTests.xcscheme @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OloPaySDK/OloPaySDK/API/OloPayAPI.swift b/src/OloPaySDK/OloPaySDK/API/OloPayAPI.swift new file mode 100644 index 0000000..c80f42f --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/API/OloPayAPI.swift @@ -0,0 +1,221 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPayAPI.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/11/21. +// + +import Foundation +import PassKit +import UIKit +import Stripe + +/// Protocol for mocking/testing purposes. See `OloPayAPI` for documentation +@objc public protocol OloPayAPIProtocol : NSObjectProtocol { + /// See `OloPayAPI.createPaymentMethod(...)` for documentation + @objc(createPaymentMethodWithPaymentMethodParams:completion:) + func createPaymentMethod(with params: OPPaymentMethodParamsProtocol, completion: @escaping OPPaymentMethodCompletionBlock) + + /// See `OloPayAPI.createCvvUpdateToken(...)` for documentation + @objc(createCvvUpdateTokenWithTokenParams:completion:) + func createCvvUpdateToken(with params: OPCvvTokenParamsProtocol, completion: @escaping OPCvvTokenUpdateCompletionBlock) + + /// See `OloPayAPI.deviceSupportsApplePay()` for documentation + @objc func deviceSupportsApplePay() -> Bool + + /// See `OloPayAPI.createPaymentRequest(...)` for documentation + @objc func createPaymentRequest(forAmount amount: NSDecimalNumber, inCountry country: String, withCurrency currency: String) throws -> PKPaymentRequest +} + +/// Represents the OloPayAPI and functionality related to it +/// - Important: Prior to calling methods in this class be sure to initialize the SDK by calling `OloPayApiInitializer.setup(...)` +@objc public class OloPayAPI : NSObject, OloPayAPIProtocol { + /// Creates an `OPPaymentMethodProtocol` instance with the provided parameters + /// + /// - Parameters: + /// - params: The `OPPaymentMethodParamsProtocol` supplied either by an `OPPaymentCardDetailsView` or `OPPaymentCardDetailsForm`. + /// - completion: The callback to run with the returned `OPPaymentMethodProtocol` instance, or an error. + @objc(createPaymentMethodWithPaymentMethodParams:completion:) + public func createPaymentMethod(with params: OPPaymentMethodParamsProtocol, completion: @escaping OPPaymentMethodCompletionBlock) { + createPaymentMethod(with: params, firstTry: true, completion: completion) + } + + /// Creates an `OPCvvUpdateTokenProtocol` instance with the provided parameters + /// - Parameters: + /// - params: The `OPCvvTokenParamsProtocol` supplied by an `OPPaymentCardCvvView`. + /// - completion: The callback to run with the returned `OPCvvUpdateTokenProtocol` instance, or an error + @objc(createCvvUpdateTokenWithTokenParams:completion:) + public func createCvvUpdateToken(with params: OPCvvTokenParamsProtocol, completion: @escaping OPCvvTokenUpdateCompletionBlock) { + createCvvUpdateToken(with: params, firstTry: true, completion: completion) + } + + /// Whether or not this device can make Apple Pay payments via a supported card network + /// Supported ApplePay card networks are: American Express, Visa, Mastercard, Discover + /// + /// - Important: While this should be used in determining if an Apple Pay button can be displayed, it should not be the **_only_** determining factor. It is also + /// important to determine whether a restaurant/vendor supports Apple Pay as a payment method, which can be determined using the Olo Ordering API. + /// + /// - Returns: `true` if the device is currently able to make Apple Pay payments via one + /// of the supported networks, or `false` if the user does not have a saved card of a + /// supported type, or other restrictions prevent payment (such as parental controls). + @objc public func deviceSupportsApplePay() -> Bool { StripeAPI.deviceSupportsApplePay() } + + /// A convenience method to build a `PKPaymentRequest` with sane default values. + /// + /// - Important: `OloPayApiInitializer.setup(...)` must have been called with both the Apple Pay merchant id and company name prior to calling this method + /// + /// - Parameters: + /// - forAmount: The amount to charge + /// - inCountry: The two-letter code for the payment country (Defaults to "US") + /// - withCurrency: The three-letter code for the currency (Defaults to "USD"). ApplePay interprets the amounts provided by the summary items attached to this request as amounts in this currency. + /// + /// - Returns: a `PKPaymentRequest` with proper default values + /// + /// - Throws: Throws `OPApplePayContextError.missingMerchantId` or `OPApplePayContextError.missingCompanyLabel` + @objc public func createPaymentRequest(forAmount amount: NSDecimalNumber, inCountry country: String = "US", withCurrency currency: String = "USD") throws -> PKPaymentRequest { + guard let merchant = OPApplePayContext.merchantId, !merchant.isEmpty else { + print("OPApplePayContext: merchantId must be set before calling createPaymentRequest()") + throw OPApplePayContextError.missingMerchantId + } + + guard let company = OPApplePayContext.companyLabel, !company.isEmpty else { + print("OPApplePayContext: companyLabel must be set before calling createPaymentRequest()") + throw OPApplePayContextError.missingCompanyLabel + } + + let request = StripeAPI.paymentRequest(withMerchantIdentifier: merchant, country: country, currency: currency) + request.paymentSummaryItems = [ PKPaymentSummaryItem(label: company, amount: amount) ] + + return request + } + + private func createCvvUpdateToken(with params: OPCvvTokenParamsProtocol, firstTry: Bool, completion: @escaping OPCvvTokenUpdateCompletionBlock) { + guard let tokenParams = params as? OPCvvTokenParams else { + completion(nil, OPError(errorType: .invalidRequestError, description: OPStrings.incorrectCvvTokenParamsType)) + return + } + + if tokenParams.cvv == "" { + completion(nil, OPError(cardErrorType: .invalidCvv, description: OPStrings.emptyCvvError)) + return + } + + let client = STPAPIClient.shared + client.createToken(forCVCUpdate: tokenParams.cvv) { token, error in + var oloToken: OPCvvUpdateTokenProtocol? = nil + var wrappedError: NSError? = nil + + if token != nil { + oloToken = OPCvvUpdateToken(token!) + } + + wrappedError = OPError.wrapIfNeeded(from: error as NSError?) + if firstTry && self.invalidPublishableKey(with: wrappedError) { + OloPayAPI.updatePublishableKey { + self.createCvvUpdateToken(with: params, firstTry: false, completion: completion) + } + + return + } + + completion(oloToken, wrappedError) + } + } + + private func createPaymentMethod(with params: OPPaymentMethodParamsProtocol, firstTry: Bool = true, completion: @escaping OPPaymentMethodCompletionBlock) { + guard let paymentParams = params as? OPPaymentMethodParams else { + completion(nil, OPError(cardErrorType: OPCardErrorType.unknownCardError, description: OPStrings.generalCardError)) + return + } + + let client = STPAPIClient.shared + client.createPaymentMethod(with: paymentParams.paymentMethodParams) { paymentMethod, createPaymentMethodError in + var oloPaymentMethod : OPPaymentMethod? = nil + if (paymentMethod != nil) { + oloPaymentMethod = OPPaymentMethod(paymentMethod: paymentMethod!) + + guard oloPaymentMethod?.cardType != .unknown && oloPaymentMethod?.cardType != .unsupported else { + let errorMessage = oloPaymentMethod?.cardType == .unsupported ? OPStrings.unsupportedCardError : OPStrings.invalidCardNumberError + completion(nil, OPError(cardErrorType: OPCardErrorType.invalidNumber, description: errorMessage)) + return + } + } + + let wrappedError = OPError.wrapIfNeeded(from: createPaymentMethodError as NSError?) + + // Attempt to redownload the publishable key and try the call again, if needed + if firstTry && self.invalidPublishableKey(with: wrappedError) { + OloPayAPI.updatePublishableKey { + self.createPaymentMethod(with: params, firstTry: false, completion: completion) + } + + return + } + + completion(oloPaymentMethod, wrappedError) + } + } + + func invalidPublishableKey(with error: NSError?) -> Bool { + guard let error = error as? OPError else { + return false + } + + return error.errorType == OPErrorType.authenticationError + } + + static var publishableKey: String { + get { OPStorage.getPublishableKey(environment: environment) } + set { + OPStorage.setPublishableKey(environment: environment, value: newValue) + StripeAPI.defaultPublishableKey = newValue + } + } + + /// The environment the SDK is configured for + public internal(set) static var environment: OPEnvironment { + get { OPEnvironment.convert(from: OPStorage.environment) } + set { OPStorage.environment = newValue.description } + } + + /// :nodoc: + public static var sdkWrapperInfo: OPSdkWrapperInfo? + + static func updatePublishableKey(completion: OPVoidBlock? = nil) { + guard let url = environment.publishableKeyUrl else { + print("Publishable key url not found") + return + } + + let task = updatePublishableKey(for: url) { + if let completion = completion { + completion() + } + } + + task.resume() + } + + static func updatePublishableKey(for url: URL, completion: OPVoidBlock? = nil) -> URLSessionDataTask { + let task = URLSession.shared.dataTask(with: url) {(data, response, error) in + guard let data = data else { + if let completion = completion { + completion() + } + return + } + + if let keyData = try? JSONDecoder().decode(OPPublishableKey.self, from: data) { + self.publishableKey = keyData.key + } + + if let completion = completion { + completion() + } + } + + return task + } +} diff --git a/src/OloPaySDK/OloPaySDK/API/OloPayApiInitializer.swift b/src/OloPaySDK/OloPaySDK/API/OloPayApiInitializer.swift new file mode 100644 index 0000000..1dd47cb --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/API/OloPayApiInitializer.swift @@ -0,0 +1,46 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPayApiInitializerProtocol.swift +// OloPaySDK +// +// Created by Justin Anderson on 10/13/21. +// + +import Foundation +import Stripe + +/// Protocol for mocking/testing purposes. See `OloPayApiInitializer` for documentation +@objc public protocol OloPayApiInitializerProtocol : NSObjectProtocol { + /// Set up the Olo Pay API. See `OloPayApiInitializer.setup(...)` for method documentation + @objc func setup(with parameters: OPSetupParameters?, completion: OPVoidBlock?) +} + +/// Class to set up and initialize the Olo Pay API +@objc public class OloPayApiInitializer : NSObject, OloPayApiInitializerProtocol { + /// Setup the Olo Pay API + /// - Important: This should be called as early as possible in the app, preferably in the AppDelegate or SceneDelegate. + /// + /// - Parameters: + /// - parameters: Optional parameters to customize the Olo Pay API + /// - completion: Optional completion handler for when the SDK is fully initialized + @objc public func setup(with parameters: OPSetupParameters? = nil, completion: OPVoidBlock? = nil) { + OloPayAPI.environment = OPEnvironment.production + + if let setupParams = parameters { + OloPayAPI.environment = setupParams.environment + OPApplePayContext.merchantId = setupParams.applePayMerchantId + OPApplePayContext.companyLabel = setupParams.applePayCompanyLabel + } + + if OloPayAPI.publishableKey == "" { + OloPayAPI.updatePublishableKey { + if let completion = completion { completion() } + } + } + else { + StripeAPI.defaultPublishableKey = OloPayAPI.publishableKey + if let completion = completion { completion() } + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/ApplePay/OPApplePayContext.swift b/src/OloPaySDK/OloPaySDK/ApplePay/OPApplePayContext.swift new file mode 100644 index 0000000..17d445a --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/ApplePay/OPApplePayContext.swift @@ -0,0 +1,203 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPApplePayContext.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// +import Foundation +import Stripe +import PassKit + +/// Protocol to hook into important events in the ApplePay flow +/// +/// __Required:__ Implement `applePaymentMethodCreated`to get payment method details that need to be submitted to Olo's Ordering API when submitting a basket with OloPay +/// +/// __Optional:__ Implement `applePaymentCompleted` to know when the ApplePay sheet is dismissed +@objc public protocol OPApplePayContextDelegate: NSObjectProtocol { + /// Called after the customer has authorized ApplePay and a payment method has been created. Implement this method to pass the payment method ID to Olo's Ordering API + /// when submitting a basket. If the API call returns an error, return that error so the ApplePay payment sheet can be dismissed appropriately + /// - Parameters: + /// - context: The apple pay context instance that caused this callback to be called + /// - paymentMethod: The PaymentMethod that represents the customer's Apple Pay payment method. + /// - Returns: `nil` if basket submission was successful, or an Error from Olo's Ordering API if submission was unsuccessful + @objc func applePaymentMethodCreated(_ context: OPApplePayContextProtocol, didCreatePaymentMethod paymentMethod: OPPaymentMethodProtocol) -> NSError? + + /// 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: + /// - context: The apple pay context instance that caused this callback to be called + /// - status: The status of the payment + /// - error: The error that occurred, if any. This will generally be `OPError`. If the error has an `errorType` of `cardError` it will also contain a user-friendly message + /// that can be used to help the user understand why the payment couldn't be completed. + @objc optional func applePaymentCompleted(_ context: OPApplePayContextProtocol, didCompleteWith status: OPPaymentStatus, error: Error?) +} + +/// Protocol for mocking/testing purposes. See `OPApplePayContext` for documentation +@objc public protocol OPApplePayContextProtocol : NSObjectProtocol { + /// See `OPApplePayContext.basketId` for documentation + @objc var basketId: String? { get set } + + /// See `OPApplePayContext.presentApplePay(...)` for documentation + @objc func presentApplePay(completion: OPVoidBlock?) throws + + /// See `OPApplePayContext.presentApplePay(...)` for documentation + @objc func presentApplePay(merchantId: String, companyLabel: String, completion: OPVoidBlock?) throws +} + +/// A helper class that implements and simplifies ApplePay. +/// +/// Use of this class looks like this: +/// 1. Create a button for ApplePay and connect it to a click handler +/// 2. Enable/Disable or Hide/Show the ApplePay button by calling `OloPayAPI.deviceSupportsApplePay()` +/// 3. In the click handler, do the following +/// 1. Check the device supports ApplePay +/// 2. Create a `PKPaymentRequest` describing the request (amount, line items, etc)... An easy way to do this is to use the `OloPayAPI.createPaymentRequest(...)` helper function +/// 3. Initialize this class with the payment request from the previous step +/// 4. Call `OPApplePayContext.presentApplePay()` to present the Apple Pay sheet and begin the payment process +/// 4. Implement `OPApplePayContextDelegate.applePaymentMethodCreated(...)` to submit the basket to Olo's Ordering API +/// 5. Optionally implement `OPApplePayContextDelegate.applePaymentCompleted(...)` to handle success and error states when the ApplePay sheet is dimissed +/// +/// - Important: Create a new instance of this class for every payment request +/// - Warning: OPApplePayContext needs to be created as a class member variable rather than a variable with function scope or else it can become +/// `nil` while the ApplePay sheet is presented and callback methods won't get called +/// +/// __Example Implementation__ +/// ``` +/// class ViewController: UIViewController, OPApplePayContextDelegate { +/// // This needs to be a class member variable or it can go out of +/// // scope during the ApplePay flow and become nil, preventing callbacks +/// // from executing +/// var _applePayContext: OPApplePayContextProtocol? = nil +/// +/// // Called when user taps on ApplePay button to begin ApplePay flow +/// func submitApplePay() { +/// let api: OloPayAPIProtocol = OloPayAPI() //This can be mocked for testing purposes +/// guard api.deviceSupportsApplePay() else { +/// return +/// } +/// +/// do { +/// let pkPaymentRequest = try api.createPaymentRequest(forAmount: 2.99, inCountry: "US", withCurrency: "USD") +/// _applePayContext = OPApplePayContext(paymentRequest: pkPaymentRequest, delegate: self) //This can be mocked for testing purposes +/// _applePayContext?.presentApplePay() { +/// // Optional logic for when the ApplePay flow is displayed +/// } +/// } +/// catch { +/// // Handle error conditions. See docs for `OPApplePayContext.presentApplePay()` for more information +/// } +/// } +/// +/// func applePaymentMethodCreated(_ context: OPApplePayContextProtocol, didCreatePaymentMethod paymentMethod: OPPaymentMethod) -> NSError? { +/// // Use the payment method to submit the basket to Olo's Ordering API (the basket id can be retrieved with `context.basketId` +/// // If the API returns an error, return that error. If the API call is successful, return nil +/// } +/// +/// func applePaymentCompleted(_ context: OPApplePayContextProtocol, didCompleteWith status: OPPaymentStatus, error: Error?) { +/// // This is called after the payment sheet has been dismissed +/// // Use the status and error parameters to determine if payment was successful +/// } +/// } +/// ``` +@objc public class OPApplePayContext : NSObject, OloApplePayLauncherDelegate, OPApplePayContextProtocol { + var _applePayLauncher: OPApplePayLauncher? + var _delegate: OPApplePayContextDelegate? + var _applePayPresented: Bool + + static var merchantId: String? + static var companyLabel: String? + + /// Basket ID convenience property for being able to submit a basket in `OPApplePayContextDelegate.applePaymentMethodCreated(...)` + @objc public var basketId: String? + + /// Initializes this class. + /// - Parameters: + /// - paymentRequest: The payment request to use with Apple Pay. + /// - delegate: The delegate. + /// - basketId: The id of the basket associated with this context. Useful in `OPApplePayContextDelegate.applePaymentMethodCreated(...)` + /// - Returns: An `OPApplePayContext` instance or `nil` if the request is invalid (e.g. the user is restricted by parental controls or can't make + /// payments on any of the requests supported networks + @objc public required init?(paymentRequest: PKPaymentRequest, delegate: OPApplePayContextDelegate, basketId: String? = nil) { + _applePayPresented = false + _delegate = delegate + self.basketId = basketId + super.init() + + _applePayLauncher = OPApplePayLauncher(paymentRequest: paymentRequest, delegate: self) + if (_applePayLauncher == nil) { + return nil + } + } + + /// Presents the Apple Pay sheet from the key window (using the merchant id and company label set in `OloPayAPI.setup(...)`) and starts the payment process. + /// + /// - Important: This method can only be called once per `OPApplePayContext` instance. Subsequent calls to this method will result in a no-op + /// + /// - Parameters: + /// - completion: Called after the Apple Pay sheet is visible to the user + /// + /// - Throws: `OPApplePayContextError.missingMerchantId`, `OPApplePayContextError.emptyMerchantId`, + /// `OPApplePayContextError.missingCompanyLabel`, or `OPApplePayContextError.emptyCompanyLabel` + @objc public func presentApplePay(completion: OPVoidBlock? = nil) throws { + guard !_applePayPresented else { + return + } + + guard let merchantId = OPApplePayContext.merchantId else { + print("OPApplePayContext: merchantId must be set before calling createPaymentRequest()") + throw OPApplePayContextError.missingMerchantId + } + + guard let companyLabel = OPApplePayContext.companyLabel else { + print("OPApplePayContext: companyLabel must be set before calling createPaymentRequest()") + throw OPApplePayContextError.missingCompanyLabel + } + + try presentApplePay(merchantId: merchantId, companyLabel: companyLabel, completion: completion) + } + + /// Presents the Apple Pay sheet from the key window and starts the payment process. + /// + /// - Important: This method can only be called once per `OPApplePayContext` instance. Subsequent calls to this method will result in a no-op + /// + /// - Parameters: + /// - merchantId: The merchant id to be used for this Apple Pay transaction. This overrides the value set in `OloPayAPI.setup(...)` + /// - companyLabel: The company label to be used for this Apple Pay transaction. This overrides the value set in `OloPayAPI.setup(...)` + /// - completion: Called after the Apple Pay sheet is visible to the user + /// + /// - Throws: Throws `OPApplePayContextError.emptyMerchantId` or `OPApplePayContextError.emptyCompanyLabel` + @objc public func presentApplePay(merchantId: String, companyLabel: String, completion: OPVoidBlock? = nil) throws { + guard !_applePayPresented else { + return + } + + if merchantId.isEmpty { + print("OPApplePayContext: merchantId cannot be empty") + throw OPApplePayContextError.emptyMerchantId + } + + if companyLabel.isEmpty { + print("OPApplePayContext: companyLabel cannot be empty") + throw OPApplePayContextError.emptyCompanyLabel + } + + _applePayPresented = true + _applePayLauncher?.presentApplePay(merchantId: merchantId, companyLabel: companyLabel, completion: completion) + } + + func paymentMethodCreated(_ launcher: OPApplePayLauncher, _ paymentMethod: OPPaymentMethod, _ paymentInfo: PKPayment, completion: @escaping OPApplePayCompletionBlock) { + let result = self._delegate?.applePaymentMethodCreated(self, didCreatePaymentMethod: paymentMethod) + completion(result) + } + + func applePayCompleted(_ launcher: OPApplePayLauncher, _ status: OPPaymentStatus, error: Error?) { + guard let paymentCompleted = self._delegate?.applePaymentCompleted else { + return + } + + let wrappedError = OPError.wrapIfNeeded(from: error as NSError?) + paymentCompleted(self, status, wrappedError) + } +} diff --git a/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardCvvView.swift b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardCvvView.swift new file mode 100644 index 0000000..bbf4323 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardCvvView.swift @@ -0,0 +1,411 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentCardCvvView.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit + +/// Defines the interface that should be implemented to receive updates from instances of +/// `OPPaymentCardCvvView`. Each callback method is optional so you only need to implement the ones you need. +/// - Important: There are two versions of each callback method. One version contains a reference to the view and can be used if you are implementing these callbacks in your UI layer. If implementing callbacks in a data layer it is recommended to implement the versions that do not contain a reference to a view. +@objc public protocol OPPaymentCardCvvViewDelegate: NSObjectProtocol { + /// Called when the field changes due to user input. Useful if the delegate is being used in + /// the UI layer. + /// - Parameters: + /// - cvvView: The view that changed + @objc optional func fieldChanged(_ cvvView: OPPaymentCardCvvView) + + /// Called when the field changes due to user input. Useful if the delegate is not being + /// used in the UI layer. + /// - Parameters: + /// - state: The current state of the view that changed + @objc optional func fieldChanged(with state: OPCardFieldStateProtocol) + + /// Called when editing begins in the CVV view. Useful if the delegate is being used in + /// the UI layer. + /// - Parameters: + /// - cvvView: The view that is being edited + @objc optional func didBeginEditing(_ cvvView: OPPaymentCardCvvView) + + /// Called when editing begins in the CVV view. Useful if the delegate is not being + /// used in the UI layer. + /// - Parameters: + /// - state: The current state of the view that is beign edited + @objc optional func didBeginEditing(with state: OPCardFieldStateProtocol) + + /// Called when editing ends in the CVV view. Useful if the delegate is being used in + /// the UI layer. + /// - Parameters: + /// - cvvView: The view that is no longer being edited + @objc optional func didEndEditing(_ cvvView: OPPaymentCardCvvView) + + /// Called when editing ends in the CVV view. Useful if the delegate is not being + /// used in the UI layer. + /// - Parameters: + /// - state: The current state of the view no longer being edited + @objc optional func didEndEditing(with state: OPCardFieldStateProtocol) + + /// Called whenever the the CVV view's `isValid` property changes. Useful if the delegate is being + /// used in the UI layer + /// - Parameters: + /// - cvvView: The view that changed + @objc optional func validStateChanged(_ cvvView: OPPaymentCardCvvView) + + /// Called whenever the the CVV view's `isValid` property changes. Useful if the delegate is not being + /// used in the UI layer. + /// - Parameters: + /// - state: The current state of the view that changed + @objc optional func validStateChanged(with state: OPCardFieldStateProtocol) +} + +/// Convenience view for gathering CVV details from a user +/// - Important: CVV details are intentionally restricted for PCI compliance +@objc public class OPPaymentCardCvvView : UIView, UIKeyInput, OPPaymentCardCvvTextFieldDelegate, OPValidStateChangedDelegate { + + private let _cvvDetails = OPPaymentCardCvvTextField() + private let _errorMessage = UILabel() + private let _cvvState = OPCvvState() + private let _viewSpacing: CGFloat = 5.0 + + private var _defaultErrorTextColor: UIColor = { + if #available(iOS 13.0, *) { + return .systemRed + } + return .red + }() + + /// :nodoc: + @objc public convenience init() { + self.init(frame: CGRect.zero) + } + + /// :nodoc: + @objc public override init(frame: CGRect){ + super.init(frame: frame) + setupViews(frame) + } + + /// :nodoc: + @objc public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupViews(_ frame: CGRect? = nil) { + _cvvState.delegate = self + _cvvDetails.cvvDelegate = self + + _errorMessage.textAlignment = .center + _errorMessage.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 14)) + _errorMessage.textColor = _defaultErrorTextColor + _errorMessage.accessibilityIdentifier = "Error Message" + _errorMessage.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + + errorTextColor = _defaultErrorTextColor + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .fill + stackView.spacing = _viewSpacing + + stackView.addArrangedSubview(_cvvDetails) + stackView.addArrangedSubview(_errorMessage) + + addSubview(stackView) + + stackView.translatesAutoresizingMaskIntoConstraints = false + let constraints = [ + stackView.topAnchor.constraint(equalTo: self.topAnchor), + stackView.leftAnchor.constraint(equalTo: self.leftAnchor), + stackView.rightAnchor.constraint(equalTo: self.rightAnchor), + stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + _cvvDetails.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0), + _cvvDetails.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0), + + _errorMessage.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0), + _errorMessage.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0), + ] + NSLayoutConstraint.activate(constraints) + } + + /// An optional handler for providing custom error messages that are displayed when `displayGeneratedErrorMessages` is `true`. Regardless of whether error messages are displayed or not, error messages can be retrieved by calling + /// `OPPaymentCardCvvView.getErrorMessage(...)` + @objc static public var errorMessageHandler: OPCvvErrorMessageBlock? = nil { + didSet { + OPCvvState.errorMessageHandler = errorMessageHandler + } + } + + /// Delegate for callbacks related to text editing in this view + @objc public var cvvDetailsDelegate: OPPaymentCardCvvViewDelegate? + + /// Provides a snapshot of the current state of this view + @objc public var fieldState: OPCardFieldStateProtocol { + get { _cvvState._fieldState } + } + + /// Whether or not error messages should be displayed based on user input. Defaults to `true` + @objc public var displayGeneratedErrorMessages: Bool = true { + didSet { + if !displayGeneratedErrorMessages { + _errorMessage.text = "" + } else { + updateErrorMessage() + } + } + } + + /// Use this to clear or set the currently displayed error message. If `displayGeneratedErrorMessages` is `true` then + /// this will be set and cleared automatically based on user input. If `false` this can be used to set and clear your own error + /// messages + @objc public var errorMessage: String { + get { _errorMessage.text ?? "" } + set { _errorMessage.text = newValue } + } + + /// The keyboard appearance for the field. Default is `UIKeyboardAppearance.default` + @objc public var keyboardAppearance: UIKeyboardAppearance { + get { _cvvDetails.keyboardAppearance } + set { _cvvDetails.keyboardAppearance = newValue } + } + + /// The font used in the CVV field. Default is `UIFont.systemFont(ofSize: 18)` + @objc public var cvvFont: UIFont { + get { _cvvDetails.cvvFont } + set { _cvvDetails.cvvFont = newValue } + } + + /// The text color used when entering valid text. Default is `.label` + @objc public var cvvTextColor: UIColor = .label { + didSet { + updateErrorMessage() + } + } + + + /// The font used for error text. Default is `UIFont.systemFont(ofSize: 14)` + @objc public var errorFont: UIFont { + get { _errorMessage.font } + set { _errorMessage.font = newValue } + } + + /// The alignment of the built in error message, default is `.center` + @objc public var errorTextAlignment: NSTextAlignment { + get { _errorMessage.textAlignment } + set { _errorMessage.textAlignment = newValue } + } + + /// The text color used when the user has entered invalid information, such as an incomplete CVV. Default is `.systemRed` + @objc public var errorTextColor: UIColor = .red { + didSet { + _errorMessage.textColor = errorTextColor + } + } + + /// The text color used for placeholder text. Default is `.systemGray2` + @objc public var placeholderColor: UIColor { + get { _cvvDetails.placeholderColor } + set { _cvvDetails.placeholderColor = newValue } + } + + /// The text used as a placeholder when the user has not entered any text. Default is `CVV` + @objc public var placeholderText: String { + get { _cvvDetails.cvvPlaceholder } + set { _cvvDetails.cvvPlaceholder = newValue } + } + + /// The cursor color for the field. + /// This is a proxy for the view's tintColor property, exposed for clarity only + /// (in other words, setting `cursorColor` is identical to setting `tintColor`) + @objc public var cursorColor: UIColor { + get { _cvvDetails.cursorColor } + set { _cvvDetails.cursorColor = newValue } + } + + /// The border color for the field. Can be `nil` (in which case no border will be drawn). Default is `.systemGray2` + @objc public var borderColor: UIColor? { + get { _cvvDetails.borderColor } + set { _cvvDetails.borderColor = newValue } + } + + /// The width of the field’s border. Default is `1.0` + @objc public var borderWidth: CGFloat { + get { _cvvDetails.borderWidth } + set { _cvvDetails.borderWidth = newValue } + } + + /// The corner radius for the field’s border. Default is `5.0` + @objc public var cornerRadius: CGFloat { + get { _cvvDetails.cornerRadius } + set { _cvvDetails.cornerRadius = newValue } + } + + /// The padding between the border of the CVV input field and the text. Default is `10` on all sides + @objc public var contentPadding: UIEdgeInsets { + get { _cvvDetails.contentPadding } + set { _cvvDetails.contentPadding = newValue } + } + + /// The alignment of the text within the view + @objc public var textAlignment: NSTextAlignment { + get { _cvvDetails.textAlignment } + set { _cvvDetails.textAlignment = newValue} + } + + /// Whether or not the input field is empty + @objc public var hasText: Bool { _cvvDetails.hasText } + + /// Whether or not the input field contains a valid CVV format + @objc public var isValid: Bool { _cvvState.isValid } + + /// The background color for the CVV input field + @objc public override var backgroundColor: UIColor? { + get { _cvvDetails.backgroundColor } + set { _cvvDetails.backgroundColor = newValue } + } + + /// The custom accessory view to display when this view becomes the first responder + @objc public override var inputAccessoryView: UIView? { + get { _cvvDetails.inputAccessoryView } + set { _cvvDetails.inputAccessoryView = newValue } + } + + /// Enable/disable selecting or editing the field + @objc public var isEnabled: Bool { + get { _cvvDetails.isEnabled } + set { _cvvDetails.isEnabled = newValue } + } + + /// :nodoc: + @objc public override var isFirstResponder: Bool { _cvvDetails.isFirstResponder } + + /// :nodoc: + @objc public override var canBecomeFirstResponder: Bool { _cvvDetails.canBecomeFirstResponder } + + /// :nodoc: + @objc public override var canResignFirstResponder: Bool { _cvvDetails.canResignFirstResponder } + + /// :nodoc: + @objc public override var intrinsicContentSize: CGSize { + let newHeight = + _cvvDetails.intrinsicContentSize.height + + _viewSpacing + + _errorMessage.intrinsicContentSize.height + + return CGSize( + width: _cvvDetails.intrinsicContentSize.width, + height: newHeight + ) + } + + /// :nodoc: + public func insertText(_ text: String) { _cvvDetails.insertText(text) } + + /// :nodoc: + public func deleteBackward() { _cvvDetails.deleteBackward() } + + /// :nodoc: + @objc public override func layoutSubviews() { _cvvDetails.layoutSubviews() } + + /// Causes the text field to begin editing and presents the keyboard + @objc override public func becomeFirstResponder() -> Bool { + _cvvDetails.becomeFirstResponder() + } + + /// Causes the text field to stop editing and dismisses the keyboard + @objc override public func resignFirstResponder() -> Bool { + _cvvDetails.resignFirstResponder() + } + + /// Clears the contents of the CVV field + @objc public func clear() { + let responderState = _cvvState.isFirstResponder + _cvvState.reset() + _cvvState.onFirstResponderStateChanged(responderState) + _cvvDetails.text = "" + + fieldChanged(_cvvDetails) + } + + /// Returns an `OPCvvTokenParamsProtocol` instance representing the CVV entered by the user, or `nil` if the + /// CVV field is not in a valid state (`isValid` is `false`) + /// - Important: If the CVV is not in a valid state then the error message will get updated + @objc public func getCvvTokenParams() -> OPCvvTokenParamsProtocol? { + _cvvState.editingCompleted() + updateErrorMessage(ignoreUneditedFieldErrors: false) + + guard _cvvState.isValid else { + return nil + } + + return OPCvvTokenParams(_cvvDetails.cvvValue) + } + + /// Get the error message (if any) for this control. Error messages can be customized by providing your own `errorMessageHandler` + /// - Note: This method functions independently of `displayGeneratedErrorMessages` + /// - Important: Not being in a valid state does not guarantee an error message will be returned (see the `ignoreUneditedFieldErrors` parameter) + /// - Parameters: + /// - ignoreUneditedFieldErrors: If `true` (the default) an error message will only be returned if the field has been "edited". In this context, "edited" means the field has become the first responder, had text entered, and stopped being the first responder. If `false` an error message will be returned without regard to whether the field has been "edited" or not. + /// - Returns: An error message that can be displayed to the user (e.g. in a custom dialog) or an empty string + @objc public func getErrorMessage(ignoreUneditedFieldErrors: Bool = true) -> String { + return _cvvState.getErrorMessage(ignoreUneditedFieldErrors) + } + + /// Whether or not there is an error message that could be displayed (e.g. by the control or in a custom dialog) + /// - Parameters: + /// - ignoreUneditedFieldErrors: If `true` (the default) an error message will only be returned if the field has been "edited". In this context, "edited" means the field has become the first responder, had text entered, and stopped being the first responder. If `false` an error message will be returned without regard to whether the field has been "edited" or not. + /// - Returns: `true` if there is an error message that can be displayed to the user, `false` otherwise + @objc public func hasErrorMessage(ignoreUneditedFieldErrors: Bool = true) -> Bool { + return _cvvState.hasErrorMessage(ignoreUneditedFieldErrors) + } + + @objc func updateErrorMessage(ignoreUneditedFieldErrors: Bool = true) { + let errorText = _cvvState.getErrorMessage(ignoreUneditedFieldErrors) + _cvvDetails.textColor = errorText.isEmpty ? cvvTextColor : errorTextColor + + guard displayGeneratedErrorMessages else { + return + } + + errorMessage = errorText + + invalidateIntrinsicContentSize() + } + + /// :nodoc: + func fieldChanged(_ cvvTextField: OPPaymentCardCvvTextField) { + _cvvState.onInputChanged(_cvvDetails.cvvValue) + updateErrorMessage() + cvvDetailsDelegate?.fieldChanged?(with: fieldState) + cvvDetailsDelegate?.fieldChanged?(self) + } + + /// :nodoc: + func didBeginEditing(_ cvvTextField: OPPaymentCardCvvTextField) { + _cvvState.onFirstResponderStateChanged(true) + updateErrorMessage() + cvvDetailsDelegate?.didBeginEditing?(with: fieldState) + cvvDetailsDelegate?.didBeginEditing?(self) + + } + + /// :nodoc: + func didEndEditing(_ cvvTextField: OPPaymentCardCvvTextField) { + _cvvState.onFirstResponderStateChanged(false) + updateErrorMessage() + cvvDetailsDelegate?.didEndEditing?(with: fieldState) + cvvDetailsDelegate?.didEndEditing?(self) + } + + /// :nodoc: + func validStateChanged(isValid: Bool) { + cvvDetailsDelegate?.validStateChanged?(with: fieldState) + cvvDetailsDelegate?.validStateChanged?(self) + } +} diff --git a/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsForm.swift b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsForm.swift new file mode 100644 index 0000000..c8b8816 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsForm.swift @@ -0,0 +1,186 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentCardDetailsForm.swift +// OloPaySDK +// +// Created by Justin Anderson on 7/22/21. +// + +import Foundation +import UIKit +import Stripe + +/// Defines the interface that should be implemented to receive updates from instances of +/// `OPPaymentCardDetailsForm`. Each callback method is optional so you only need to implement the ones you need. +/// - Important: There are two versions of each callback method. One version contains a reference to the view and can be used if you are implementing these callbacks in your UI layer. If implementing callbacks in a data layer it is recommended to implement the versions that do not contain a reference to a view. +@objc public protocol OPPaymentCardDetailsFormDelegate: NSObjectProtocol { + /// Called when all of the form view's required inputs are valid or transition away from all being valid. + /// - Parameters: + /// - form: The form that changed state + /// - isValid: Whether or not the form is in a valid state + @objc optional func isValidChanged(_ form: OPPaymentCardDetailsForm, _ isValid: Bool) + + /// Called when all of the form view's required inputs are valid or transition away from all being valid. + /// - Parameters: + /// - isValid: Whether or not the form is in a valid state + @objc optional func isValidChanged(_ isValid: Bool) +} + +/// Convenience multi-field form for collecting card details from a user +/// - Important: Card details are intentionally restricted for PCI compliance +@objc public class OPPaymentCardDetailsForm : UIView, STPCardFormViewDelegate { + private var _form: STPCardFormView + private var _isValid: Bool = false + private var _numberField: UITextField? + private var _expirationField: UITextField? + private var _cvvField: UITextField? + private var _postalCodeField: UITextField? + + /// Public initializer for `OPPaymentCardDetailsForm` + /// - Parameters: + /// - style: The visual style to use for this instance + @objc public init(style: OPCardFormStyle = .standard) { + _form = STPCardFormView(style: OPCardFormStyle.convert(from: style)) + super.init(frame: CGRect.zero) + setupViews() + } + + /// :nodoc: + @objc public override init(frame: CGRect = CGRect.zero) { + _form = STPCardFormView(style: .standard) + super.init(frame: frame) + setupViews() + } + + /// :nodoc: + @objc public required init?(coder: NSCoder) { + _form = STPCardFormView(style: .standard) + super.init(coder: coder) + setupViews() + } + + /// The delegate to notify when the card form transitions to or from being valid. + @objc public var cardDetailsDelegate: OPPaymentCardDetailsFormDelegate? + + /// The background color for the form + @objc public override var backgroundColor: UIColor? { + get { _form.backgroundColor } + set { _form.backgroundColor = newValue } + } + + /// The background color that is automatically applied to the input fields when `isUserInteractionEnabled` is set to `false` + @objc public var disabledBackgroundColor: UIColor? { + get { _form.disabledBackgroundColor } + set { _form.disabledBackgroundColor = newValue } + } + + func setupViews() { + let stackView = UIStackView() + stackView.axis = NSLayoutConstraint.Axis.vertical + stackView.distribution = UIStackView.Distribution.fillProportionally + stackView.alignment = UIStackView.Alignment.fill + + stackView.addArrangedSubview(_form) + addSubview(stackView) + + stackView.translatesAutoresizingMaskIntoConstraints = false + let constraints = [ + stackView.topAnchor.constraint(equalTo: self.topAnchor), + stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ] + + NSLayoutConstraint.activate(constraints) + _form.delegate = self + setupTextFields() + } + + /// Whether or not the form is in a valid state. Use `OPPaymentCardDetailsFormDelegate` to know when this state changes + @objc public var isValid: Bool { _isValid } + + /// :nodoc: + @objc public override var canResignFirstResponder: Bool { _form.canResignFirstResponder } + + /// :nodoc: + @objc public override func resignFirstResponder() -> Bool { _form.resignFirstResponder() } + + /// :nodoc: + @objc public override var isFirstResponder: Bool { _form.isFirstResponder } + + /// :nodoc: + @objc public override var canBecomeFirstResponder: Bool { _form.canBecomeFirstResponder } + + /// Causes the number field to begin editing and presents the keyboard + /// - Important: This is functionally the same as calling `becomeFirstResponder(at: .number)` + @objc public override func becomeFirstResponder() -> Bool { self.becomeFirstResponder(at: .number) } + + /// Causes the specific text field to begin editing and presents the keyboard + /// - Parameters: + /// - field: Determins which card field to be set as first responder + @objc @discardableResult public func becomeFirstResponder(at field: OPCardField) -> Bool { + switch field { + case .number: + return _numberField!.becomeFirstResponder() + case .expiration: + return _expirationField!.becomeFirstResponder() + case .cvv: + return _cvvField!.becomeFirstResponder() + case .postalCode: + return _postalCodeField!.becomeFirstResponder() + case .unknown: + return false + } + } + + /// :nodoc: + @objc public override var intrinsicContentSize: CGSize { _form.intrinsicContentSize } + + /// :nodoc: + @objc public override func layoutSubviews() { _form.layoutSubviews() } + + /// :nodoc: + @objc public override var frame: CGRect { + get { _form.frame } + set { _form.frame = newValue } + } + + /// :nodoc: + @objc override public func updateConstraints() { + _form.updateConstraints() + super.updateConstraints() + } + + /// Returns the `OPPaymentMethodParamsProtocol` instance representing the details in the form, if it exists, otherwise `nil`. + @objc public func getPaymentMethodParams() -> OPPaymentMethodParamsProtocol? { + guard let cardParams = _form.cardParams else { + return nil + } + return OPPaymentMethodParams(cardParams, fromSource: OPPaymentMethodSource.formInput) + } + + /// :nodoc: + public func cardFormView(_ form: STPCardFormView, didChangeToStateComplete complete: Bool) { + _isValid = complete + cardDetailsDelegate?.isValidChanged?(self, _isValid) + cardDetailsDelegate?.isValidChanged?(_isValid) + } + + /// :nodoc: + private func setupTextFields() { + let allTextFields = OPPaymentCardDetailsInternalView.getAllTextFields(from: _form) + + allTextFields.forEach { field in + if (String(describing: type(of: field)) == OPStrings.stripeNumberField) { + _numberField = field + } else if (String(describing: type(of: field)) == OPStrings.stripeExiryField){ + _expirationField = field + } else if (String(describing: type(of: field)) == OPStrings.stripeCvvField){ + _cvvField = field + } else if (String(describing: type(of: field)) == OPStrings.stripePostalCodeField){ + _postalCodeField = field + } + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsView.swift b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsView.swift new file mode 100644 index 0000000..81a82fb --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Controls/OPPaymentCardDetailsView.swift @@ -0,0 +1,697 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentCardDetailsView.swift +// OloPaySDK +// +// Created by Justin Anderson on 5/11/21. +// + +import Stripe +import Foundation +import UIKit + +/// Defines the interface that should be implemented to receive updates from instances of +/// `OPPaymentCardDetailsView`. Each callback method is optional so you only need to implement the ones you need. +/// - Important: There are two versions of each callback method. One version contains a reference to the view and can be used if you are implementing these callbacks in your UI layer. If implementing callbacks in a data layer it is recommended to implement the versions that do not contain a reference to a view. +@objc public protocol OPPaymentCardDetailsViewDelegate: NSObjectProtocol { + /// Called when any field changes. + /// - Parameters: + /// - cardDetails: The card details view that changed + @objc optional func paymentCardDetailsViewDidChange(_ cardDetails: OPPaymentCardDetailsView) + + /// Called when any field changes. + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true`) + @objc optional func paymentCardDetailsViewDidChange(with fieldStates: NSDictionary, isValid: Bool) + + /// Called when editing begins in the view as a whole. This will always be followed by a `paymentCardDetailsViewFieldDidBeginEditing(...)` callback. + /// - Parameters: + /// - cardDetails: The card details view that changed + @objc optional func paymentCardDetailsViewDidBeginEditing(_ cardDetails: OPPaymentCardDetailsView) + + /// Called when editing begins on the view as a whole. This will always be followed by a `paymentCardDetailsViewFieldDidBeginEditing(...)` callback. + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true`) + @objc optional func paymentCardDetailsViewDidBeginEditing(with fieldStates: NSDictionary, isValid: Bool) + + /// Called when editing ends on the view as a whole. This will always be preceded by a `paymentCardDetailsViewFieldDidEndEditing(...)` callback. + /// - Parameters: + /// - cardDetails: The card details view that changed + @objc optional func paymentCardDetailsViewDidEndEditing(_ cardDetails: OPPaymentCardDetailsView) + + /// Called when editing ends on the view as a whole. This will always be preceded by a `paymentCardDetailsViewFieldDidEndEditing(...)` callback. + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true` + @objc optional func paymentCardDetailsViewDidEndEditing(with fieldStates: NSDictionary, isValid: Bool) + + /// Called when editing begins on a specific field + /// - Parameters: + /// - cardDetails: The card details view that changed + /// - field: The field that is being edited + @objc optional func paymentCardDetailsViewFieldDidBeginEditing(_ cardDetails: OPPaymentCardDetailsView, field: OPCardField) + + /// Called when editing begins on a specific field + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - field: The field that is being edited + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true` + @objc optional func paymentCardDetailsViewFieldDidBeginEditing(with fieldStates: NSDictionary, field: OPCardField, isValid: Bool) + + /// Called when editing ends for a specific field + /// - Parameters: + /// - cardDetails: The card details view that changed + /// - field: The field that is no longer being edited + @objc optional func paymentCardDetailsViewFieldDidEndEditing(_ cardDetails: OPPaymentCardDetailsView, field: OPCardField) + + /// Called when editing ends for a specific field + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - field: The field that is being edited + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true` + @objc optional func paymentCardDetailsViewFieldDidEndEditing(with fieldStates: NSDictionary, field: OPCardField, isValid: Bool) + + /// Called whenever the view's `isValid` property changes + /// - Parameters: + /// - cardDetails: The card details view that changed + @objc optional func paymentCardDetailsViewIsValidChanged(_ cardDetails: OPPaymentCardDetailsView) + + /// Called whenever the view's `isValid` property changes + /// - Important: If using Swift, `fieldStates` can be converted to a Swift Dictionary as follows: ```let state = fieldStates as! Dictionary``` + /// - Parameters: + /// - fieldStates: A dictionary representing the current state of the view. Keys are of type `OPCardField` and values are of type `OPCardFieldStateProtocol` + /// - isValid: A convenience parameter to quickly determine if the view is in a valid state (e.g. all fields in the dictionary have an `isValid` property with a value of `true` + @objc optional func paymentCardDetailsViewIsValidChanged(with fieldStates: NSDictionary, isValid: Bool) +} + +/// Convenience view for gathering card details from a user. +/// - Important: Card details are intentionally restricted for PCI compliance +@objc public class OPPaymentCardDetailsView : UIView, UIKeyInput, OPPaymentCardDetailsViewInternalDelegate, OPValidStateChangedDelegate { + let _cardDetails: OPPaymentCardDetailsInternalView = OPPaymentCardDetailsInternalView() + let _errorMessage: UILabel = UILabel() + let _viewSpacing: CGFloat = 5.0 + var _displayErrorMessages = true + var _cardState = OPCardState() + var _clearFieldsInProgress = false + var _numberField: UITextField? + var _expirationField: UITextField? + var _cvvField: UITextField? + var _postalCodeField: UITextField? + + /// :nodoc: + @objc public convenience init() { + self.init(frame: CGRect.zero) + } + + /// :nodoc: + @objc public override init(frame: CGRect) { + super.init(frame: frame) + setupViews(frame: frame) + } + + /// :nodoc: + @objc public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupViews(frame: CGRect? = nil) { + _errorMessage.textAlignment = .center + _errorMessage.textColor = _cardDetails.textErrorColor + _errorMessage.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 14)) + _errorMessage.accessibilityIdentifier = "Error Message" + + let stackView = UIStackView() + stackView.axis = NSLayoutConstraint.Axis.vertical + stackView.distribution = UIStackView.Distribution.fill + stackView.alignment = UIStackView.Alignment.fill + stackView.spacing = _viewSpacing + + stackView.addArrangedSubview(_cardDetails) + stackView.addArrangedSubview(_errorMessage) + + addSubview(stackView) + + stackView.translatesAutoresizingMaskIntoConstraints = false + let constraints = [ + stackView.topAnchor.constraint(equalTo: self.topAnchor), + stackView.leftAnchor.constraint(equalTo: self.leftAnchor), + stackView.rightAnchor.constraint(equalTo: self.rightAnchor), + stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ] + NSLayoutConstraint.activate(constraints) + + _cardDetails.cardDetailsDelegate = self + _cardState.delegate = self + + numberPlaceholder = OPStrings.numberPlaceholder + expirationPlaceholder = OPStrings.expirationPlaceholder + cvvPlaceholder = OPStrings.cvvPlaceholder + postalCodePlaceholder = OPStrings.postalCodePlaceholder + + // Must be called after setting all placeholders to known values + setupTextFields() + } + + /// The state of this control as an `NSDictionary`. Keys are of type `OPCardField`. + /// Values are of type `OPCardFieldStateProtocol` + /// - Important: The control is valid if all fields have an `isValid` value of `true` + /// - Important: This property is intended mainly for compatibily with obj-c. Swift users should use `fieldStates` + @objc public var fieldStatesObjc: NSDictionary { + get { fieldStates as NSDictionary } + } + + /// The state of this control. + /// - Important: The control is valid if all fields have an `isValid` value of `true` + public var fieldStates: [OPCardField : OPCardFieldStateProtocol] { + get { _cardState.fieldStates } + } + + /// The font used in each child field. Default is `UIFont.systemFont(ofSize: 18)` + @objc public var font: UIFont { + get { _cardDetails.font } + set { + _cardDetails.font = newValue + _errorMessage.font = newValue + } + } + + /// The font used for error text. Default is `UIFont.systemFont(ofSize: 14)` + @objc public var errorFont: UIFont { + get { _errorMessage.font } + set { _errorMessage.font = newValue } + } + + /// The alignment of the built in error message, default is `.center` + @objc public var errorTextAlignment: NSTextAlignment { + get { _errorMessage.textAlignment } + set { _errorMessage.textAlignment = newValue } + } + + /// The text color used when entering valid text. Default is `.label` + @objc public var textColor: UIColor { + get { _cardDetails.textColor } + set { _cardDetails.textColor = newValue } + } + + /// The text color used when the user has entered invalid information, such as an invalid card number. Default is `.systemRed` + @objc public var textErrorColor: UIColor { + get { _cardDetails.textErrorColor } + set { + _cardDetails.textErrorColor = newValue + _errorMessage.textColor = newValue + } + } + + /// The text placeholder color used in each child field. This will also set the color of the card placeholder icon. Default is `.systemGray2` + @objc public var placeholderColor: UIColor { + get { _cardDetails.placeholderColor } + set { _cardDetails.placeholderColor = newValue } + } + + /// The placeholder for the card number field. Default is “4242424242424242”. If this is set to something that resembles a card number, + /// it will automatically format it as such (in other words, you don’t need to add spaces to this string) + @objc @IBInspectable public var numberPlaceholder: String? { + get { _cardDetails.numberPlaceholder } + set { _cardDetails.numberPlaceholder = newValue } + } + + /// The placeholder for the expiration field. Defaults to “MM/YY” + @objc @IBInspectable public var expirationPlaceholder: String? { + get { _cardDetails.expirationPlaceholder } + set { _cardDetails.expirationPlaceholder = newValue } + } + + /// The placeholder for the cvv field. Defaults to “CVV” + @objc @IBInspectable public var cvvPlaceholder: String? { + get { _cardDetails.cvcPlaceholder } + set { _cardDetails.cvcPlaceholder = newValue } + } + + /// Deprecated: Use `cvvPlaceholder` instead + @available(*, deprecated, renamed: "cvvPlaceholder") + @objc @IBInspectable public var cvcPlaceholder: String? { + get { cvvPlaceholder } + set { cvvPlaceholder = newValue } + } + + /// The placeholder for the postal code field. Defaults to "Postal Code" + @objc @IBInspectable public var postalCodePlaceholder: String? { + get { _cardDetails.postalCodePlaceholder } + set { _cardDetails.postalCodePlaceholder = newValue } + } + + /// The cursor color for the field. + /// This is a proxy for the view's `tintColor` property, exposed for clarity only + /// (in other words, setting `cursorColor` is identical to setting `tintColor`) + @objc public var cursorColor: UIColor { + get { _cardDetails.cursorColor } + set { _cardDetails.cursorColor = newValue } + } + + /// The border color for the field. Can be `nil` (in which case no border will be drawn). Default is `.systemGray2` + @objc public var borderColor: UIColor? { + get { _cardDetails.borderColor } + set { _cardDetails.borderColor = newValue } + } + + /// The width of the field’s border. Default is `1.0` + @objc public var borderWidth: CGFloat { + get { _cardDetails.borderWidth } + set { _cardDetails.borderWidth = newValue } + } + + /// The corner radius for the field’s border. Default is `5.0` + @objc public var cornerRadius: CGFloat { + get { _cardDetails.cornerRadius } + set { _cardDetails.cornerRadius = newValue } + } + + /// The keyboard appearance for the field. Default is `UIKeyboardAppearance.default` + @objc public var keyboardAppearance: UIKeyboardAppearance { + get { _cardDetails.keyboardAppearance } + set { _cardDetails.keyboardAppearance = newValue } + } + + /// This behaves identically to setting the inputView for each child text field + @objc public override var inputView: UIView? { + get { _cardDetails.inputView } + set { _cardDetails.inputView = newValue } + } + + /// The custom accessory view to display when this view becomes the first responder + @objc public override var inputAccessoryView: UIView? { + get { _cardDetails.inputAccessoryView } + set { _cardDetails.inputAccessoryView = newValue } + } + + /// The curent brand image displayed in the receiver + @objc public var brandImage: UIImage? { _cardDetails.brandImage } + + /// Whether or not all fields are currently in a valid state + @objc dynamic public var isValid: Bool { + get { _cardState.isValid } + } + + /// Enable/disable selecting or editing the field + @objc public var isEnabled: Bool { + get { _cardDetails.isEnabled } + set { _cardDetails.isEnabled = newValue } + } + + /// The detected brand of the card, based on the user's input + @objc public var cardType: OPCardBrand { + OPCardBrand.convert(from: STPCardValidator.brand(forNumber: _cardDetails.cardNumber ?? "")) + } + + /// Whether or not the card number field is valid + @objc public var cardNumberIsValid: Bool { + get { _cardState.cardNumber.isValid } + } + + /// Whether or not the expiration field is valid + @objc public var expirationIsValid: Bool { + get { _cardState.expiration.isValid } + } + + /// Whether or not the CVV is in a valid format + @objc public var cvvIsValid: Bool { + get { _cardState.cvv.isValid } + } + + /// Deprecated: Use `cvvIsValid` instead + @available(*, deprecated, renamed: "cvvIsValid") + @objc public var cvcIsValid: Bool { + return cvvIsValid + } + + /// Whether or not the card number field is empty + @objc public var cardNumberIsEmpty: Bool { + get { _cardState.cardNumber.isEmpty } + } + + /// Whether or not the postal code is in a valid format. This will return `true` if `postalCodeEntryEnabled` is `false` + @objc public var postalCodeIsValid: Bool { + get { _cardState.postalCode.isValid } + } + + /// Whether or not the expiration field is empty + @objc public var expirationIsEmpty: Bool { + get { _cardState.expiration.isEmpty } + } + + /// Whether or not the cvv is empty + @objc public var cvvIsEmpty: Bool { + get { _cardState.cvv.isEmpty } + } + + /// Deprecated: Use `cvvIsEmpty` instead + @available(*, deprecated, renamed: "cvvIsEmpty") + @objc public var cvcIsEmpty: Bool { + return cvvIsEmpty + } + + /// `true` if the postal code is empty, `false` otherwise + @objc public var postalCodeIsEmpty: Bool { + get { _cardState.postalCode.isEmpty } + } + + /// Controls if a postal code entry field will be displayed to the user. Default is `true`. If `true`, the type of code entry shown is controlled + /// by the set `countryCode` value. Some country codes may result in no postal code entry being shown if those countries do not + /// commonly use postal codes. If `false`, no postal code entry will ever be displayed. + /// - Important: A postal code is **_**required_** to process a credit card with Olo's Ordering API. If you choose not to use the postal code field associated with this control + /// you will need to provide your own mechanism for getting a postal code from the user. + @objc public var postalCodeEntryEnabled: Bool { + get { _cardState.postalCodeEnabled } + set { + _cardDetails.postalCodeEntryEnabled = newValue + _cardState.postalCodeEnabled = newValue + } + } + + /// The two-letter ISO country code that corresponds to the user’s billing address. If `postalCodeEntryEnabled` is `true`, this controls + /// which type of entry is allowed. If `postalCodeEntryEnabled` is `false`, this property has no effect. If set to `nil` and postal + /// code entry is enabled, the country from the user’s current locale will be filled in. Otherwise the specific country code set will be + /// used. By default this will fetch the user’s current country code from `NSLocale` + @objc public var countryCode: String? { + get { _cardDetails.countryCode } + set { _cardDetails.countryCode = newValue } + } + + /// Causes the number field to begin editing and presents the keyboard + /// - Important: This is functionally the same as calling `becomeFirstResponder(at: .number)` + @objc @discardableResult public override func becomeFirstResponder() -> Bool { self.becomeFirstResponder(at: .number) } + + /// Causes the specific text field to begin editing and presents the keyboard + /// - Parameters: + /// - field: Determins which card field to be set as first responder + @objc @discardableResult public func becomeFirstResponder(at field: OPCardField) -> Bool { + switch field { + case .number: + return _numberField!.becomeFirstResponder() + case .expiration: + return _expirationField!.becomeFirstResponder() + case .cvv: + return _cvvField!.becomeFirstResponder() + case .postalCode: + return _postalCodeField!.becomeFirstResponder() + case .unknown: + return false + } + } + + /// Causes the text field to stop editing and dismisses the keyboard + @objc @discardableResult public override func resignFirstResponder() -> Bool { _cardDetails.resignFirstResponder() } + + /// Resets all of the contents of all of the fields. If the field is currently being edited, the number field will become selected + @objc public func clear() { + _clearFieldsInProgress = true + + let focusedField = _cardState.focusedField + + _cardState.reset() + _cardDetails.clear() + _cardDetails.resignFirstResponder() + + _clearFieldsInProgress = false + + if focusedField != nil && focusedField != .number { + paymentCardDetailsViewFieldDidEndEditing(_cardDetails, field: focusedField!) + } + + _cardDetails.becomeFirstResponder() + paymentCardDetailsViewDidChange(_cardDetails); + } + + /// Use this to customize CVV images displayed in the view for each type of card + @objc static public var cvvImageHandler: OPCardBrandImageBlock { + get { OPPaymentCardDetailsInternalView.cvvImageClosure } + set { OPPaymentCardDetailsInternalView.cvvImageClosure = newValue } + } + + /// Deprecated: Use `cvvImageHandler` instead + @available(*, deprecated, renamed: "cvvImageHandler") + @objc static public var cvcImageHandler: OPCardBrandImageBlock { + get { cvvImageHandler } + set { cvvImageHandler = newValue } + } + + /// Use this to customize card images displayed in the view for each type of card + @objc static public var brandImageHandler: OPCardBrandImageBlock { + get { OPPaymentCardDetailsInternalView.brandImageClosure } + set { OPPaymentCardDetailsInternalView.brandImageClosure = newValue } + } + + /// Use this to customize error images displayed in the view for each type of card + @objc static public var errorImageHandler: OPCardBrandImageBlock { + get { OPPaymentCardDetailsInternalView.errorImageClosure } + set { OPPaymentCardDetailsInternalView.errorImageClosure = newValue } + } + + /// An optional handler for providing custom error messages that are displayed when `displayGeneratedErrorMessages` is `true`. Regardless of whether error messages are displayed or not, error messages can be retrieved by calling + /// `OPPaymentCardDetailsView.getErrorMessage(...)` + @objc static public var errorMessageHandler: OPCardErrorMessageBlock? = nil { + didSet { + OPCardState.errorMessageHandler = errorMessageHandler + } + } + + /// Whether or not the error messages should be displayed based on user input. Defaults to `true` + @objc public var displayGeneratedErrorMessages: Bool { + get { _displayErrorMessages } + set { + _displayErrorMessages = newValue + updateErrorMessage() + if !_displayErrorMessages { + _errorMessage.text = "" + } + } + } + + /// Use this to clear or set the currently displayed error message. If `displayGeneratedErrorMessages` is `true` this will be set and cleared + /// automatically based on user input. If `false` this can be used to set and clear your own messages + @objc public var errorMessage: String { + get { _errorMessage.text ?? "" } + set { _errorMessage.text = newValue } + } + + /// Check if there is an error message that could be displayed (e.g. by the control or in a custom dialog) + /// - Parameters: + /// - ignoreUneditedFieldErrors: If `true` (the default), only fields that have been edited by the user will be considered. In this context, "edited" means the user has entered text and resigned first responder status while not empty. If `false`, all fields will be looked at to determine an error message regardless of whether thay have been "edited" + /// - Returns: `true` if there is an error message that can be displayed to the user, `false` otherwise + @objc public func hasErrorMessage(_ ignoreUneditedFieldErrors: Bool = true) -> Bool { + return _cardState.hasErrorMessage(ignoreUneditedFieldErrors) + } + + /// Get the error message (if any) for this control. Error messages can be customized by providing your own `errorMessageHandler` + /// - Note: This method functions independently of `displayGeneratedErrorMessages` + /// - Important: Not being in a valid state does not guarantee an error message will be returned (see the `ignoreUneditedFieldErrors` parameter) + /// - Parameters: + /// - ignoreUneditedFieldErrors: If `true` (the default), only fields that have been edited by the user will be considered. In this context, "edited" means the user has entered text and resigned first responder status while not empty. If `false`, all fields will be looked at to determine an error message regardless of whether thay have been "edited" + /// - Returns: An error message that can be displayed to the user (e.g. in a custom dialog) or an empty string + @objc public func getErrorMessage(_ ignoreUneditedFieldErrors: Bool = true) -> String { + return _cardState.getErrorMessage(ignoreUneditedFieldErrors) + } + + /// :nodoc: + @objc(brandImageRectForBounds:) public func brandImageRect(forBounds bounds: CGRect) -> CGRect { _cardDetails.brandImageRect(forBounds: bounds) } + + /// :nodoc: + @objc(fieldsRectForBounds:) public func fieldsRect(forBounds bounds: CGRect) -> CGRect { _cardDetails.fieldsRect(forBounds: bounds) } + + /// The background color of the field + @objc public override var backgroundColor: UIColor? { + get { _cardDetails.backgroundColor } + set { _cardDetails.backgroundColor = newValue } + } + + /// The vertical alignment for the field + @objc public var contentVerticalAlignment: UIControl.ContentVerticalAlignment { + get { _cardDetails.contentVerticalAlignment } + set { _cardDetails.contentVerticalAlignment = newValue } + } + + /// Delegate to receive callbacks about card input events for this view. + @objc public var cardDetailsDelegate: OPPaymentCardDetailsViewDelegate? = nil + + /// :nodoc: + @objc public override var isFirstResponder: Bool { _cardDetails.isFirstResponder } + + /// :nodoc: + @objc public override var canBecomeFirstResponder: Bool { _cardDetails.canBecomeFirstResponder } + + /// :nodoc: + @objc public override var canResignFirstResponder: Bool { _cardDetails.canResignFirstResponder } + + /// :nodoc: + @objc public override var intrinsicContentSize: CGSize { + let newHeight = + _cardDetails.intrinsicContentSize.height + + _viewSpacing + + _errorMessage.intrinsicContentSize.height + + return CGSize( + width: _cardDetails.intrinsicContentSize.width, + height: newHeight + ) + } + + /// :nodoc: + @objc public override func layoutSubviews() { _cardDetails.layoutSubviews() } + + /// :nodoc: + @objc public var hasText: Bool { _cardDetails.hasText } + + /// :nodoc: + @objc public func insertText(_ text: String) { _cardDetails.insertText(text) } + + /// :nodoc: + @objc public func deleteBackward() { _cardDetails.deleteBackward() } + + /// :nodoc: + @objc override public func updateConstraints() { + _cardDetails.updateConstraints() + super.updateConstraints() + } + + /// Returns an `OPPaymentMethodParamsProtocol` instance representing the card details. + /// - Important: If the CVV is not in a valid state (`isValid` is `false`) then the error message will get updated + @objc public func getPaymentMethodParams() -> OPPaymentMethodParamsProtocol? { + guard isValid else { + updateErrorMessage(ignoreUneditedFieldErrors: false) + return nil + } + + return OPPaymentMethodParams(_cardDetails.getPaymentMethodParams(), fromSource: OPPaymentMethodSource.singleLineInput) + } + + /// :nodoc: + func paymentCardDetailsViewDidChange(_ cardDetails: OPPaymentCardDetailsInternalView) { + guard let focusedField = _cardState.focusedField else { + return + } + + if focusedField == .number { + _cardState.onCardNumberChanged(newText: getText(for: .number), brand: cardType) + } else if focusedField == .expiration { + _cardState.onExpirationChanged( + expirationMonth: _cardDetails.formattedExpirationMonth ?? "", + expirationYear: _cardDetails.formattedExpirationYear ?? "") + } else if focusedField == .cvv { + _cardState.onCvvChanged(newText: getText(for: .cvv)) + } else if focusedField == .postalCode { + _cardState.onPostalCodeChanged(newText: getText(for: .postalCode)) + } + + cardDetailsDelegate?.paymentCardDetailsViewDidChange?(self) + cardDetailsDelegate?.paymentCardDetailsViewDidChange?(with: fieldStatesObjc, isValid: isValid) + updateErrorMessage() + } + + private func getText(for field: OPCardField) -> String { + switch (field) { + case .number: + return _cardDetails.cardNumber ?? "" + case .expiration: + return "\(_cardDetails.formattedExpirationMonth ?? "")\(_cardDetails.formattedExpirationYear ?? "")" + case .cvv: + return _cardDetails.cvc ?? "" + case .postalCode: + return _cardDetails.postalCode ?? "" + case .unknown: + return "" + } + } + + /// :nodoc: + func paymentCardDetailsViewDidBeginEditing(_ cardDetails: OPPaymentCardDetailsInternalView) { + if _clearFieldsInProgress { + return + } + + cardDetailsDelegate?.paymentCardDetailsViewDidBeginEditing?(self) + cardDetailsDelegate?.paymentCardDetailsViewDidBeginEditing?(with: fieldStatesObjc, isValid: isValid) + } + + /// :nodoc: + func paymentCardDetailsViewDidEndEditing(_ cardDetails: OPPaymentCardDetailsInternalView) { + if _clearFieldsInProgress { + return + } + + _cardState.onResignFirstResponder() + + cardDetailsDelegate?.paymentCardDetailsViewDidEndEditing?(self) + cardDetailsDelegate?.paymentCardDetailsViewDidEndEditing?(with: fieldStatesObjc, isValid: isValid) + + updateErrorMessage() + } + + /// :nodoc: + func paymentCardDetailsViewFieldDidBeginEditing(_ cardDetails: OPPaymentCardDetailsInternalView, field: OPCardField) { + if _clearFieldsInProgress { + return + } + + _cardState.onBecomeFirstResponder(field: field) + + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing?(self, field: field) + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing?(with: fieldStatesObjc, field: field, isValid: isValid) + updateErrorMessage() + } + + /// :nodoc: + func paymentCardDetailsViewFieldDidEndEditing(_ cardDetails: OPPaymentCardDetailsInternalView, field: OPCardField) { + if _clearFieldsInProgress { + return + } + + // We need to call this manually because Stripe calls this end editing callback PRIOR to the callbacks indicating + // which fields are beginning and ending editing states. Without calling this manually, by the time Stripe's view + // change callback fires, we have a new focused field and we miss the change for the current field. Note that + // this only happens when the UI auto-advances the cursor to the next field + paymentCardDetailsViewDidChange(_cardDetails) + + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing?(self, field: field) + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing?(with: fieldStatesObjc, field: field, isValid: isValid) + updateErrorMessage() + } + + /// :nodoc: + func validStateChanged(isValid: Bool) { + cardDetailsDelegate?.paymentCardDetailsViewIsValidChanged?(self) + cardDetailsDelegate?.paymentCardDetailsViewIsValidChanged?(with: fieldStatesObjc, isValid: isValid) + } + + /// Tells the view to update it's error message, if necessary. This is normally called internally by the view itself + /// and should not generally need to be called. + @objc func updateErrorMessage(ignoreUneditedFieldErrors: Bool = true) { + guard displayGeneratedErrorMessages else { return } + errorMessage = getErrorMessage(ignoreUneditedFieldErrors) + invalidateIntrinsicContentSize() + } + + /// :nodoc: + private func setupTextFields() { + let allTextFields = OPPaymentCardDetailsInternalView.getAllTextFields(from: _cardDetails) + + allTextFields.forEach { field in + if (field.placeholder == OPStrings.numberPlaceholder) { + _numberField = field + } else if (field.placeholder == OPStrings.expirationPlaceholder){ + _expirationField = field + } else if (field.placeholder == OPStrings.cvvPlaceholder){ + _cvvField = field + } else if (field.placeholder == OPStrings.postalCodePlaceholder){ + _postalCodeField = field + } + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPApplePayContextError.swift b/src/OloPaySDK/OloPaySDK/Data/OPApplePayContextError.swift new file mode 100644 index 0000000..0a98be4 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPApplePayContextError.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPApplePayContextError.swift +// OloPaySDK +// +// Created by Justin Anderson on 7/1/21. +// + +import Foundation +import Stripe + +/// An enum representing ApplePay specific errors +@objc public enum OPApplePayContextError : Int, Error { + /// The merchant id is missing + case missingMerchantId + /// The company label is missing + case missingCompanyLabel + /// The merchant id is empty + case emptyMerchantId + /// The company label is empty + case emptyCompanyLabel +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCardBrand.swift b/src/OloPaySDK/OloPaySDK/Data/OPCardBrand.swift new file mode 100644 index 0000000..84cd0b1 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCardBrand.swift @@ -0,0 +1,89 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardBrand.swift +// OloPaySDK +// +// Created by Justin Anderson on 5/29/21. +// + +import Foundation +import Stripe + +/// An enum representing card brands supported by Olo Pay +/// - Important: See the `OPCardBrand.description` property for how to use this enum when submitting a basket to the Olo Ordering API +@objc public enum OPCardBrand : Int, CustomStringConvertible { + /// Visa + case visa + /// American Express + case amex + /// MasterCard + case mastercard + /// Discover + case discover + /// Unsupported card type + case unsupported + /// Unknown card type + case unknown + + /// A string representation of this enum. Use this as the `cardtype` when submitting a basket to the Olo Ordering API + /// - Important: If the value is `unknown` the basket submission will fail + public var description: String { + switch self { + case .visa: + return "Visa" + case .amex: + return "Amex" + case .mastercard: + return "Mastercard" + case .discover: + return "Discover" + case .unsupported: + return "Unsupported" + case .unknown: + return "Unknown" + } + } + + static func convert(from cardBrand: STPCardBrand?) -> OPCardBrand { + switch cardBrand { + case .visa: + return OPCardBrand.visa + case .amex: + return OPCardBrand.amex + case .mastercard: + return OPCardBrand.mastercard + case .discover: + return OPCardBrand.discover + case .JCB, + .dinersClub, + .unionPay, + .cartesBancaires: + return OPCardBrand.unsupported + case .unknown, + .none: + fallthrough + @unknown default: + return OPCardBrand.unknown + } + } + + static func convert(from cardBrand: OPCardBrand?) -> STPCardBrand { + switch cardBrand { + case .visa: + return STPCardBrand.visa + case .amex: + return STPCardBrand.amex + case .mastercard: + return STPCardBrand.mastercard + case .discover: + return STPCardBrand.discover + case .unknown, + .unsupported, + .none: + fallthrough + @unknown default: + return STPCardBrand.unknown + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCardErrorType.swift b/src/OloPaySDK/OloPaySDK/Data/OPCardErrorType.swift new file mode 100644 index 0000000..25c3c7d --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCardErrorType.swift @@ -0,0 +1,85 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardErrorType.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/30/21. +// + +import Foundation +import Stripe + +/// Possible card error codes when there was an error tokenizing a card. These values can be +/// accessed from the `OPError.cardErrorType` property. +@objc public enum OPCardErrorType : Int, CustomStringConvertible { + /// The card number is invalid (empty, incorrect format, incomplete, etc) + case invalidNumber + /// The expiration month is invalid. + case invalidExpMonth + /// The expiration year is invalid + case invalidExpYear + /// The card is expired. + case expiredCard + /// The card was declined. + case cardDeclined + /// An error occured while processing this card. + case processingError + /// The postal code is invalid (empty, incorrect format, incomplete, etc). + case invalidZip + /// The CVV is not valid (empty, incorrect format, incomplete, etc) + case invalidCvv + /// An unknown or unaccounted-for error occured + case unknownCardError + + /// A string representation of this enum + public var description: String { + switch self { + case .invalidNumber: + return "invalidNumber" + case .invalidExpMonth: + return "invalidExpMonth" + case .invalidExpYear: + return "invalidExpYear" + case .expiredCard: + return "expiredCard" + case .cardDeclined: + return "cardDeclined" + case .processingError: + return "processingError" + case .invalidZip: + return "invalidZip" + case .invalidCvv: + return "invalidCvv" + case .unknownCardError: + return "unknownCardError" + } + } + + internal static func convert(from key: STPCardErrorCode) -> OPCardErrorType { + switch key { + case .invalidNumber: + return .invalidNumber + case .invalidExpMonth: + return .invalidExpMonth + case .invalidExpYear: + return .invalidExpYear + case .invalidCVC: + return .invalidCvv + case .incorrectNumber: + return .invalidNumber + case .expiredCard: + return .expiredCard + case .cardDeclined: + return .cardDeclined + case .incorrectCVC: + return .invalidCvv + case .processingError: + return .processingError + case .incorrectZip: + return .invalidZip + @unknown default: + return .unknownCardError + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCardField.swift b/src/OloPaySDK/OloPaySDK/Data/OPCardField.swift new file mode 100644 index 0000000..df6bb1c --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCardField.swift @@ -0,0 +1,40 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardField.swift +// OloPaySDK +// +// Created by Justin Anderson on 7/1/21. +// + +import Foundation + +/// Represents the different credit card fields +@objc public enum OPCardField: Int, CustomStringConvertible { + /// The card's number field + case number + /// The card's expiration field + case expiration + /// The card's security code (CVV) field + case cvv + /// The card's postal code field + case postalCode + /// An unknown card field + case unknown + + /// A string representation of the card field + public var description: String { + switch self { + case .number: + return "number" + case .expiration: + return "expiration" + case .cvv: + return "cvv" + case .postalCode: + return "postalCode" + case .unknown: + return "unknown" + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCardFieldStateProtocol.swift b/src/OloPaySDK/OloPaySDK/Data/OPCardFieldStateProtocol.swift new file mode 100644 index 0000000..fb4925e --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCardFieldStateProtocol.swift @@ -0,0 +1,29 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardFieldStateProtocol.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/21/23. +// + +import Foundation + +/// Protocol representing the state of a credit card field (card number, expiration, cvv, postal code). +@objc public protocol OPCardFieldStateProtocol : NSObjectProtocol { + /// Whether or not the field is valid + var isValid: Bool { get } + + /// Whether or not the field is empty + var isEmpty: Bool { get } + + /// Whether or not the field has ever not been empty. Once `true`, it will not change back to `false` + var wasEdited: Bool { get } + + /// Whether or not the field is currenlty the first responder + var isFirstResponder: Bool { get } + + /// Whether or not the field has ever been the first responder. Once `true`, it will not change back to `false` + /// - Note: This only gets set to `true` if the field lost first responder status while `wasEdited` was `true`. + var wasFirstResponder: Bool { get } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCardFormStyle.swift b/src/OloPaySDK/OloPaySDK/Data/OPCardFormStyle.swift new file mode 100644 index 0000000..f1b1185 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCardFormStyle.swift @@ -0,0 +1,49 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardFormStyle.swift +// OloPaySDK +// +// Created by Justin Anderson on 7/22/21. +// + +import Foundation +import Stripe + +/// Options for configuring the display of `OPPaymentCardDetailsForm` instances +@objc public enum OPCardFormStyle : Int, CustomStringConvertible { + /// Displays the form in a rounded rect with full separators between each input field. + case standard + /// Displays the form without an outer border and underlines under each input field. + case borderless + + /// A string representation of this enum + public var description: String { + switch self { + case .standard: + return "standard" + case .borderless: + return "borderless" + } + } + + internal static func convert(from type: STPCardFormViewStyle) -> OPCardFormStyle { + switch type { + case .standard: + return .standard + case .borderless: + return .borderless + @unknown default: + return .standard + } + } + + internal static func convert(from type: OPCardFormStyle) -> STPCardFormViewStyle { + switch type { + case .standard: + return .standard + case .borderless: + return .borderless + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCvvTokenParamsProtocol.swift b/src/OloPaySDK/OloPaySDK/Data/OPCvvTokenParamsProtocol.swift new file mode 100644 index 0000000..6a76463 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCvvTokenParamsProtocol.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCvvTokenParamsProtocol.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/4/23. +// + +import Foundation + +/// Parameters class used to generate a CVV token via `OloPayAPI.createCvvToken(...)` +@objc public protocol OPCvvTokenParamsProtocol: NSObjectProtocol {} + diff --git a/src/OloPaySDK/OloPaySDK/Data/OPCvvUpdateTokenProtocol.swift b/src/OloPaySDK/OloPaySDK/Data/OPCvvUpdateTokenProtocol.swift new file mode 100644 index 0000000..7b462dc --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPCvvUpdateTokenProtocol.swift @@ -0,0 +1,20 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCvvUpdateTokenProtocol.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/4/23. +// + +import Foundation + +/// Represents a single-use cvv update token used to submit a basket via Olo's Ordering API +/// when a saved card requires CVV revalidation +@objc public protocol OPCvvUpdateTokenProtocol : NSObjectProtocol { + /// The id for the token + @objc var id: String { get } + + /// The environment the token was created in + @objc var environment: OPEnvironment { get } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPEnvironment.swift b/src/OloPaySDK/OloPaySDK/Data/OPEnvironment.swift new file mode 100644 index 0000000..b957b8a --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPEnvironment.swift @@ -0,0 +1,45 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPEnvironment.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/9/22. +// + +import Foundation +import ImageIO + +/// Enum indicating the environment that should be used for the Olo Pay SDK +@objc public enum OPEnvironment : Int, CustomStringConvertible { + /// Production environment + case production + /// Test environment + case test + + public var description: String { + switch self { + case .production: + return "production" + case .test: + return "test" + } + } + + internal static func convert(from key: String) -> OPEnvironment { + if key == OPEnvironment.test.description { + return OPEnvironment.test + } + + return OPEnvironment.production + } + + internal var publishableKeyUrl: URL? { + switch self { + case .production: + return URL(string: "https://static.olocdn.net/web-client/olo-pay/keys/prod.json") + case .test: + return URL(string: "https://static.olocdn.net/web-client/olo-pay/keys/dev.json") + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPError.swift b/src/OloPaySDK/OloPaySDK/Data/OPError.swift new file mode 100644 index 0000000..73ee011 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPError.swift @@ -0,0 +1,105 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPErrors.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/17/21. +// + +import Foundation +import Stripe + +/// Error class for all OloPay-pecific errors +/// +/// Errors will come back as either `Error` or `NSError` instances. To get an instance of this class +/// just check that it is the correct type and cast to `OPError`. +/// ``` +/// func someMethod(error: Error?) { +/// if let opError = error as? OPError { +/// //Do something with the error +/// } +/// } +/// ``` +@objc public class OPError : NSError { + /// Domain for all OPError instances + @objc public static let oloPayDomain = "com.olo.olopay" + + /// Error type key for the `userInfo` property. This can be used to access the error type, but the `errorType` property exposes this information directly + @objc public static let errorTypeKey = "\(oloPayDomain):ErrorType" + + /// Card error type key for the `userInfo` property. This can be used to access the card error type, but the `cardErrorType` property exposes this information directly + /// - Important: This key only exists in the `userInfo` dictionary if the error represents a card error + @objc public static let cardErrorTypeKey = "\(oloPayDomain):CardErrorType" + + /// Create an `OPError` instance for a card error. Useful for testing purposes + /// - Parameters + /// - cardErrorType: The type of card error + /// - description: The description for the error + @objc public init(cardErrorType: OPCardErrorType, description: String) { + var userInfo: [String : Any] = [:] + + userInfo[OPError.errorTypeKey] = OPErrorType.cardError + userInfo[OPError.cardErrorTypeKey] = cardErrorType + userInfo[NSLocalizedDescriptionKey] = description + + super.init(domain: OPError.oloPayDomain, code: OPErrorType.cardError.rawValue, userInfo: userInfo) + } + + /// Create an `OPError` instance (other than card errors). Useful for testing purposes + /// - Parameters: + /// - errorType: The type of error + /// - description: The description for the error + @objc public init(errorType: OPErrorType, description: String) { + var userInfo: [String : Any] = [:] + + userInfo[OPError.errorTypeKey] = errorType + userInfo[OPError.cardErrorTypeKey] = nil + userInfo[NSLocalizedDescriptionKey] = description + + super.init(domain: OPError.oloPayDomain, code: errorType.rawValue, userInfo: userInfo) + } + + private init(error: NSError) { + var userInfo: [String: Any] = [:] + + if let errorCode = STPErrorCode.init(rawValue: error.code) { + let opErrorCode = OPErrorType.convert(from: errorCode) + userInfo[OPError.errorTypeKey] = opErrorCode + + if opErrorCode == .cardError { + userInfo[NSLocalizedDescriptionKey] = error.userInfo[NSLocalizedDescriptionKey] ?? "" + } + } + + if let errorCodeRaw = error.userInfo[STPError.cardErrorCodeKey] as? String, let errorCode = STPCardErrorCode.init(rawValue: errorCodeRaw) { + userInfo[OPError.cardErrorTypeKey] = OPCardErrorType.convert(from: errorCode) + } + + super.init(domain: OPError.oloPayDomain, code: error.code, userInfo: userInfo) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + internal static func wrapIfNeeded(from error: NSError?) -> NSError? { + guard let unwrappedError = error, unwrappedError.domain == STPError.stripeDomain else { + return error + } + + return OPError(error: unwrappedError) + } + + /// The type of error + public var errorType: OPErrorType { userInfo[OPError.errorTypeKey] as? OPErrorType ?? OPErrorType.generalError } + + /// If `errorType` is `OPCardErrorType.cardError`, this holds the type of error. For any other error type this is `nil` + public var cardErrorType: OPCardErrorType? { userInfo[OPError.cardErrorTypeKey] as? OPCardErrorType ?? nil } + + /// If `errorType` is `OPCardErrorType.cardError`, this holds a user-friendly message that can be displayed to the user. For any other error type this is `nil` + /// - Note: This is a convenience property that is functionally equivalent to using `localizedDescription` + public var cardErrorMessage: String? { errorType == .cardError ? localizedDescription : nil } +} + + diff --git a/src/OloPaySDK/OloPaySDK/Data/OPErrorType.swift b/src/OloPaySDK/OloPaySDK/Data/OPErrorType.swift new file mode 100644 index 0000000..b9656d2 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPErrorType.swift @@ -0,0 +1,100 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPErrorType.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/30/21. +// + +import Foundation +import Stripe + +internal let applePayContextErrorRawValue: Int = 1010 +internal let generalErrorRawValue: Int = 1020 + +/// Possible error code values for OPErrors (NSErrors with the `OPError.oloPayDomain` domain +@objc public enum OPErrorType : Int, CustomStringConvertible { + /// Trouble connecting to servers + case connectionError + /// Request has invalid parameters + case invalidRequestError + /// General-purpose API error + case apiError + /// Something was wrong with the card details + case cardError + /// Operation was cancelled + case cancellationError + /// Something was wrong with the Apple Pay Context + case applePayContextError + /// An authentication error + case authenticationError + /// Other general errors + case generalError + + internal static func convert(from key: STPErrorCode) -> OPErrorType { + switch key { + case .connectionError: + return .connectionError + case .invalidRequestError: + return .invalidRequestError + case .apiError: + return .apiError + case .cardError: + return .cardError + case .cancellationError: + return .cancellationError + case .ephemeralKeyDecodingError: + return .generalError + case .authenticationError: + return .authenticationError + @unknown default: + return .generalError + } + } + + /// :nodoc: + public var rawValue: Int { + switch self { + case .connectionError: + return STPErrorCode.connectionError.rawValue + case .invalidRequestError: + return STPErrorCode.invalidRequestError.rawValue + case .apiError: + return STPErrorCode.apiError.rawValue + case .cardError: + return STPErrorCode.cardError.rawValue + case .cancellationError: + return STPErrorCode.cancellationError.rawValue + case .applePayContextError: + return applePayContextErrorRawValue + case .authenticationError: + return STPErrorCode.authenticationError.rawValue + case .generalError: + return generalErrorRawValue + } + } + + /// A string representation of this enum + public var description: String { + switch self { + case .connectionError: + return "connectionError" + case .invalidRequestError: + return "invalidRequestError" + case .apiError: + return "apiError" + case .cardError: + return "cardError" + case .cancellationError: + return "cancellationError" + case .applePayContextError: + return "applePayContextError" + case .authenticationError: + return "authenticationError" + case .generalError: + return "generalError" + } + } +} + diff --git a/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodParamsProtocol.swift b/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodParamsProtocol.swift new file mode 100644 index 0000000..c9c8112 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodParamsProtocol.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentMethodParamsProtocol.swift +// OloPaySDK +// +// Created by Kyle Szklenski on 9/1/21. +// + +import Foundation + +/// Payment method parameters to send payment data to `OloPayAPI.createPaymentMethod(...)` +@objc public protocol OPPaymentMethodParamsProtocol : NSObjectProtocol {} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodProtocol.swift b/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodProtocol.swift new file mode 100644 index 0000000..4cbaf21 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPPaymentMethodProtocol.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentMethodProtocol.swift +// OloPaySDK +// +// Created by Justin Anderson on 5/28/21. +// + +import Foundation +import Stripe + +/// Represents a payment method containing all information needed to submit a basket +/// via Olo's Ordering API +@objc public protocol OPPaymentMethodProtocol : NSObjectProtocol { + /// The payment method id. This should be set to the token field when submitting a basket + @objc var id: String { get } + + /// The last four digits of the card + @objc var last4: String? { get } + + /// The issuer of the card (e.g. Visa, Mastercard, etc) + @objc var cardType: OPCardBrand { get } + + /// Two-digit number representing the card's expiration month + @objc var expirationMonth: NSNumber? { get } + + /// Four-digit number representing the card’s expiration year + @objc var expirationYear: NSNumber? { get } + + /// ZIP or postal code + @objc var postalCode: String? { get } + + /// Whether or not this payment method was created via ApplePay + @objc var isApplePay: Bool { get } + + /// Country from the card in the payment method + @objc var country: String? { get } + + /// The environment used to create the payment method + @objc var environment: OPEnvironment { get } +} + diff --git a/src/OloPaySDK/OloPaySDK/Data/OPPaymentStatus.swift b/src/OloPaySDK/OloPaySDK/Data/OPPaymentStatus.swift new file mode 100644 index 0000000..3eaac45 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPPaymentStatus.swift @@ -0,0 +1,57 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentStatus.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// + +import Foundation +import Stripe + +/// An enum representing the status of an Apple Pay payment requested from the user. +@objc public enum OPPaymentStatus : Int, CustomStringConvertible { + /// 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 + + /// A string representation of this enum + public var description: String { + switch self { + case .success: + return "Success" + case .error: + return "Error" + case .userCancellation: + return "UserCancellation" + } + } + + static func convert(from paymentStatus: STPPaymentStatus) -> OPPaymentStatus { + switch paymentStatus { + case .error: + return OPPaymentStatus.error + case .success: + return OPPaymentStatus.success + case .userCancellation: + return OPPaymentStatus.userCancellation + @unknown default: + return OPPaymentStatus.error + } + } + + static func convert(from paymentStatus: OPPaymentStatus) -> STPPaymentStatus { + switch paymentStatus { + case .error: + return STPPaymentStatus.error + case .success: + return STPPaymentStatus.success + case .userCancellation: + return STPPaymentStatus.userCancellation + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPSdkBuildType.swift b/src/OloPaySDK/OloPaySDK/Data/OPSdkBuildType.swift new file mode 100644 index 0000000..222af00 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPSdkBuildType.swift @@ -0,0 +1,38 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSdkBuildType.swift +// OloPaySDK +// +// Created by Richard Dowdy on 6/1/23. +// + +import Foundation + +/// :nodoc: +public enum OPSdkBuildType: Int, CustomStringConvertible { + /// :nodoc: + case internalBuild + /// :nodoc: + case publicBuild + + /// :nodoc: + public var description: String { + switch self { + case .internalBuild: + return "internal" + case .publicBuild: + return "public" + } + } + + internal static func convert(from key: String) -> OPSdkBuildType? { + if key.lowercased() == OPSdkBuildType.internalBuild.description { + return OPSdkBuildType.internalBuild + } else if key.lowercased() == OPSdkBuildType.publicBuild.description { + return OPSdkBuildType.publicBuild + } + + return nil + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperInfo.swift b/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperInfo.swift new file mode 100644 index 0000000..53d94e7 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperInfo.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSdkWrapperInfo.swift +// OloPaySDK +// +// Created by Richard Dowdy on 6/1/23. +// + +import Foundation + +/// :nodoc: +public class OPSdkWrapperInfo: NSObject { + private let _majorVersion: Int + private let _minorVersion: Int + private let _buildVersion: Int + private let _sdkBuildType: OPSdkBuildType + private let _sdkPlatform: OPSdkWrapperPlatform + + /// :nodoc: + public init (withMajorVersion majorVersion: Int, withMinorVersion minorVersion: Int, withBuildVersion buildVersion: Int, withSdkBuildType sdkBuildType: OPSdkBuildType, withSdkPlatform sdkPlatform: OPSdkWrapperPlatform) { + _majorVersion = majorVersion + _minorVersion = minorVersion + _buildVersion = buildVersion + _sdkBuildType = sdkBuildType + _sdkPlatform = sdkPlatform + } + + /// :nodoc: + public var version: String { + get {"\(_majorVersion).\(_minorVersion).\(_buildVersion)"} + } + + /// :nodoc: + public var buildType: String { + get {_sdkBuildType.description} + } + + /// :nodoc: + public var platform: String { + get {_sdkPlatform.description} + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperPlatform.swift b/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperPlatform.swift new file mode 100644 index 0000000..7f4cc85 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPSdkWrapperPlatform.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSdkWrapperPlatform.swift +// OloPaySDK +// +// Created by Richard Dowdy on 6/1/23. +// + +import Foundation + +/// :nodoc: +public enum OPSdkWrapperPlatform: Int, CustomStringConvertible { + /// :nodoc: + case reactNative + /// :nodoc: + case capacitor + /// :nodoc: + case flutter + + /// :nodoc: + public var description: String { + switch self { + case .reactNative: + return "reactNative" + case .capacitor: + return "capacitor" + case .flutter: + return "flutter" + } + } + + internal static func convert(from key: String) -> OPSdkWrapperPlatform { + switch key { + case OPSdkWrapperPlatform.reactNative.description: + return OPSdkWrapperPlatform.reactNative + case OPSdkWrapperPlatform.capacitor.description: + return OPSdkWrapperPlatform.capacitor + default: + return OPSdkWrapperPlatform.flutter + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPSetupParameters.swift b/src/OloPaySDK/OloPaySDK/Data/OPSetupParameters.swift new file mode 100644 index 0000000..9c5f619 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPSetupParameters.swift @@ -0,0 +1,53 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSetupParameters.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/17/21. +// + +import Foundation + +/// Optional parameters for setting up the Olo Pay API +@objc public class OPSetupParameters : NSObject { + /// This property is deprecated and will be removed in a future release + @available(*, deprecated, message: "The freshSetup parameter is deprecated and will be removed in a future release") + public let freshSetup: Bool + + /// If using ApplePay, this is the merchant Id registered with Apple + /// - Important: This is required when using ApplePay + public let applePayMerchantId: String? + + /// If using ApplePay, this is the company label that will be displayed on the ApplePay payment sheet + /// - Important: This is required when using ApplePay + public let applePayCompanyLabel: String? + + /// The environment the SDK is going to be used in + public let environment: OPEnvironment + + /// This constructor is deprecated. Alternative constructors that don't take a `freshSetup` parameter should be used instead. + @available(*, deprecated, message: "Use alternative constructors without the freshSetup parameter") + public init( + withEnvironment environment: OPEnvironment? = OPEnvironment.production, + withFreshSetup freshSetup: Bool? = false, + withApplePayMerchantId merchantId : String? = "", + withApplePayCompanyLabel companyLabel : String? = "" + ) { + self.environment = environment! + self.freshSetup = freshSetup ?? false + applePayMerchantId = merchantId + applePayCompanyLabel = companyLabel + } + + public init( + withEnvironment environment: OPEnvironment? = OPEnvironment.production, + withApplePayMerchantId merchantId : String? = "", + withApplePayCompanyLabel companyLabel : String? = "" + ) { + self.environment = environment! + self.freshSetup = false + applePayMerchantId = merchantId + applePayCompanyLabel = companyLabel + } +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPStrings.swift b/src/OloPaySDK/OloPaySDK/Data/OPStrings.swift new file mode 100644 index 0000000..2d49e60 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPStrings.swift @@ -0,0 +1,68 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPStrings.swift +// OloPaySDK +// +// Created by Justin Anderson on 7/20/21. +// + +import Foundation + +//TODO: Add localization support +/// Default error messages used by `OPPaymentCardDetailsView` +@objc public class OPStrings : NSObject { + /// Default error message for an invalid card number + @objc public static let invalidCardNumberError = "Your card's number is invalid" + + /// Default error message for an empty card number + @objc public static let emptyCardNumberError = "Your card's number is missing" + + /// Default error message for an invalid expiration date + @objc public static let invalidExpirationError = "Your card's expiration date is invalid" + + /// Default error message for an empty expiration date + @objc public static let emptyExpirationError = "Your card's expiration date is missing" + + /// Default error message for an invalid cvv + @objc public static let invalidCvvError = "Your card's security code is invalid" + + /// Default error message for an empty cvv + @objc public static let emptyCvvError = "Your card's security code is missing" + + /// Default error message for an invalid postal code + @objc public static let invalidPostalCodeError = "Your ZIP/postal code is invalid" + + /// Default error message for an empty postal code + @objc public static let emptyPostalCodeError = "Your ZIP/postal code is missing" + + /// Default error mesage if the card number is valid but is not a supported type + @objc public static let unsupportedCardError = "Your card type is not supported" + + /// Default error message for unknown/general card errors + @objc public static let generalCardError = "Your card details are invalid" + + /// Default error message for an incomplete CVV field (used with `OPPaymentCardCvvView`) + @objc public static let incompleteCvvError = "Your card's security code is incomplete" + + /// Deprecated: Use `invalidCvvError` instead + @available(*, deprecated, renamed: "invalidCvvError") + @objc public static let invalidCvcError = invalidCvvError + + /// Deprecated: Use `emptyCvvError` instead + @available(*, deprecated, renamed: "emptyCvvError") + @objc public static let emptyCvcError = emptyCvvError + + // INTERNAL STRINGS + internal static let incorrectCvvTokenParamsType = "Params must be of type OPCvvTokenParams" + internal static let numberPlaceholder = "4242424242424242" + internal static let expirationPlaceholder = "MM/YY" + internal static let cvvPlaceholder = "CVV" + internal static let postalCodePlaceholder = "Postal Code" + + // Stripe Input Field Types + internal static let stripeNumberField = "STPCardNumberInputTextField" + internal static let stripeExiryField = "STPCardExpiryInputTextField" + internal static let stripeCvvField = "STPCardCVCInputTextField" + internal static let stripePostalCodeField = "STPPostalCodeInputTextField" +} diff --git a/src/OloPaySDK/OloPaySDK/Data/OPTypeAliases.swift b/src/OloPaySDK/OloPaySDK/Data/OPTypeAliases.swift new file mode 100644 index 0000000..119213c --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Data/OPTypeAliases.swift @@ -0,0 +1,54 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPTypeAliases.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// + +import Foundation +import UIKit + +/// An empty block, called with no arguments, returning nothing. +public typealias OPVoidBlock = () -> Void + +/// A callback used to return an error during the ApplePay completion flow +/// - Parameters: +/// - error: The error that occurred when submitting an ApplePay payment method to Olo's Ordering API, or `nil` if no error occurred +public typealias OPApplePayCompletionBlock = (_ error: Error?) -> Void + +/// A completion handler used when generating payment methods +/// +/// - Parameters: +/// - paymentMethod: The payment method from the response. Will be `nil` if an error occurs. +/// - error: The error returned from the response, or nil if no error occurred. +public typealias OPPaymentMethodCompletionBlock = (_ paymentMethod: OPPaymentMethodProtocol?, _ error: Error?) -> Void + +/// A completion handler used when generating CVV update tokens +/// +/// - Parameters: +/// - token: The CVV Update token from the response. Will be `nil` if an error occurs +/// - error: The error returned from the response, or nil if no error occured +public typealias OPCvvTokenUpdateCompletionBlock = (_ token: OPCvvUpdateTokenProtocol?, _ error: Error?) -> Void + +/// A block used for returning an image associated with the card brand parameter +/// - Parameters: +/// - cardBrand: The brand to get an image for +/// - Returns: An image associated with the given brand, or nil +public typealias OPCardBrandImageBlock = (_ cardBrand: OPCardBrand) -> UIImage? + +/// A block used for returning an error message based on the current state of the Card control +/// - Parameters: +/// - cardState: A representation of the current state of the card input control +/// - cardBrand: The detected brand of the card number entered by the user +/// - ignoreUneditedFieldErrors: If true, only fields that have been edited should be considered when generating an error message. If false, all fields should be considered. +/// - Returns: An error message, or empty string if no message should be displayed +public typealias OPCardErrorMessageBlock = (_ cardState: NSDictionary, _ cardBrand: OPCardBrand, _ ignoreUneditedFieldErrors: Bool) -> String + +/// A block used for returning an error message based on the current state of the CVV control +/// - Parameters: +/// - cvvFieldState: A representation of the current state of the CVV control +/// - ignoreUneditedFieldErrors: If true, only fields that have been edited should be considered when generating an error message. If false, all fields should be considered. +/// - Returns: An error message, or empty string if no message should be displayed +public typealias OPCvvErrorMessageBlock = (_ cvvFieldState: OPCardFieldStateProtocol, _ ignoreUneditedFieldErrors: Bool) -> String diff --git a/src/OloPaySDK/OloPaySDK/Info.plist b/src/OloPaySDK/OloPaySDK/Info.plist new file mode 100644 index 0000000..c0701c6 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/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 + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/src/OloPaySDK/OloPaySDK/Internal/ApplePay/OPApplePayLauncher.swift b/src/OloPaySDK/OloPaySDK/Internal/ApplePay/OPApplePayLauncher.swift new file mode 100644 index 0000000..3ce58cf --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/ApplePay/OPApplePayLauncher.swift @@ -0,0 +1,211 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPApplePayLauncher.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/21/23. +// + +import Foundation +import PassKit +import Stripe +import ObjectiveC + +@objc protocol OloApplePayLauncherDelegate: NSObjectProtocol { + func paymentMethodCreated( + _ launcher: OPApplePayLauncher, + _ paymentMethod: OPPaymentMethod, + _ paymentInfo: PKPayment, + completion: @escaping OPApplePayCompletionBlock + ) + + @objc(applePayCompleted:status:error:) + func applePayCompleted( + _ launcher: OPApplePayLauncher, + _ status: OPPaymentStatus, + error: Error? + ) +} + +@available(iOSApplicationExtension, unavailable) +@available(macCatalystApplicationExtension, unavailable) +@objc internal class OPApplePayLauncher: NSObject, PKPaymentAuthorizationControllerDelegate { + private weak var _delegate: OloApplePayLauncherDelegate? + private var _presentationWindow: UIWindow? + private var _applePayLaunched = false + private var _applePayLauncherAssociatedObjectKey = 0 + private var _paymentState = ApplePayState.notStarted + private var _didCancelOrTimeoutWhilePending = false + private var _error: Error? + private var _merchantId: String? + private var _companyLabel: String? + @objc var _authController: PKPaymentAuthorizationController? + + + @objc(initWithPaymentRequest:delegate:) + public required init?(paymentRequest: PKPaymentRequest, delegate: OloApplePayLauncherDelegate?) { + guard (StripeAPI.canSubmitPaymentRequest(paymentRequest)) else { + return nil + } + + super.init() + + _delegate = delegate + _authController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) + _authController?.delegate = self + } + + public func presentApplePay(merchantId: String, companyLabel: String, completion: OPVoidBlock? = nil) { + dispatchToMainThreadIfNecessary { + let window = UIApplication.shared.windows.first { $0.isKeyWindow } + self.presentApplePay(from: window, merchantId: merchantId, companyLabel: companyLabel, completion: completion) + } + } + + @objc(presentApplePayFromWindow:withMerchantId:withCompanyLabel:withCompletion:) + public func presentApplePay(from window: UIWindow?, merchantId: String, companyLabel: String, completion: OPVoidBlock? = nil) { + _presentationWindow = window + _merchantId = merchantId + _companyLabel = companyLabel + + guard !_applePayLaunched, let applePayController = self._authController else { + return + } + _applePayLaunched = true + + // This instance must live so that the apple pay sheet is dismissed; until then, the app is effectively frozen. + objc_setAssociatedObject( + applePayController, UnsafeRawPointer(&_applePayLauncherAssociatedObjectKey), self, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + applePayController.present { (presented) in + dispatchToMainThreadIfNecessary { + completion?() + } + } + } + + public func paymentAuthorizationController( + _ controller: PKPaymentAuthorizationController, + didAuthorizePayment payment: PKPayment, + handler completion: @escaping (PKPaymentAuthorizationResult) -> Void + ) { + completePayment(with: payment) { status, error in + let errors = [STPAPIClient.pkPaymentError(forStripeError: error)].compactMap({ $0 }) + let result = PKPaymentAuthorizationResult(status: status, errors: errors) + completion(result) + } + } + + 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: + dismissController(controller) { + self._delegate?.applePayCompleted(self, .userCancellation, error: nil) + self.endApplePay() + } + 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: + dismissController(controller) { + self._delegate?.applePayCompleted(self, .error, error: self._error) + self.endApplePay() + } + case .success: + dismissController(controller) { + self._delegate?.applePayCompleted(self, .success, error: nil) + self.endApplePay() + } + } + } + + public func presentationWindow(for controller: PKPaymentAuthorizationController) -> UIWindow? { + return _presentationWindow + } + + private func completePayment(with payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus, Error?) -> Void) { + // Helper to handle annoying logic around "Do I call completion block or dismiss + call delegate?" + let handleFinalState: ((ApplePayState, Error?) -> Void) = { state, error in + switch state { + case .error: + self._paymentState = .error + self._error = error + if self._didCancelOrTimeoutWhilePending { + self.dismissController(self._authController){ + self._delegate?.applePayCompleted(self, .error, error: self._error) + self.endApplePay() + } + } else { + completion(PKPaymentAuthorizationStatus.failure, self._error) + } + case .success: + self._paymentState = .success + if self._didCancelOrTimeoutWhilePending { + self.dismissController(self._authController){ + self._delegate?.applePayCompleted(self, .success, error: nil) + self.endApplePay() + } + } else { + completion(PKPaymentAuthorizationStatus.success, nil) + } + case .pending, .notStarted: + assert(false, "Invalid final state") + return + } + } + + let client = STPAPIClient.shared + let metadata = OPMetadataGenerator(applePayMerchantId: _merchantId, applePayCompanyLabel: _companyLabel).generate() + client.createPaymentMethod(with: payment, metadata: metadata) { paymentMethod, paymentMethodError in + if let paymentMethod = paymentMethod, paymentMethodError == nil, self._authController != nil { + guard let launcherDelegate = self._delegate else { + handleFinalState(.error, nil) + return + } + + let opPaymentMethod = OPPaymentMethod(paymentMethod: paymentMethod) + launcherDelegate.paymentMethodCreated(self, opPaymentMethod, payment) { intentCreationError in + if intentCreationError == nil && self._authController != nil { + handleFinalState(.success, nil) + } else { + handleFinalState(.error, intentCreationError) + } + } + } else { + handleFinalState(.error, paymentMethodError) + } + } + } + + private func endApplePay() { + if let authorizationController = _authController { + objc_setAssociatedObject( + authorizationController, UnsafeRawPointer(&_applePayLauncherAssociatedObjectKey), + nil, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + _authController = nil + _delegate = nil + } + + private func dismissController(_ controller: PKPaymentAuthorizationController?, completion: @escaping OPVoidBlock) { + controller?.dismiss { + dispatchToMainThreadIfNecessary { + completion() + } + } + } +} + +internal enum ApplePayState: Int { + case notStarted + case pending + case error + case success +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardCvvTextField.swift b/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardCvvTextField.swift new file mode 100644 index 0000000..0d7679b --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardCvvTextField.swift @@ -0,0 +1,174 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardCvvTextField.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit + +protocol OPPaymentCardCvvTextFieldDelegate: NSObjectProtocol { + func fieldChanged(_ cvvTextField: OPPaymentCardCvvTextField) + func didBeginEditing(_ cvvTextField: OPPaymentCardCvvTextField) + func didEndEditing(_ cvvTextField: OPPaymentCardCvvTextField) +} + +class OPPaymentCardCvvTextField: UITextField, UITextFieldDelegate { + private static let defaultPadding: CGFloat = 10 + + private let _defaultGray: UIColor = { + if #available(iOS 13.0, *) { + return .systemGray2 + } + return .lightGray + }() + + private let _defaultBackground: UIColor = { + if #available(iOS 13.0, *) { + return .systemBackground + } + return .white + }() + + @objc public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + // NOTE: Though it seems weird, this backing variable is necessary + // to prevent a crash in the React Native SDK + private var _inputAccessoryView: UIView? = nil + @objc open override var inputAccessoryView: UIView? { + get { _inputAccessoryView } + set { + _inputAccessoryView = newValue + super.inputAccessoryView = newValue + } + } + + var cvvDelegate: OPPaymentCardCvvTextFieldDelegate? + + var cvvFont: UIFont { + get { font! } + set { font = newValue } + } + + var placeholderColor: UIColor = .lightGray { + didSet { + // Force the placeholder text to update with the new color + cvvPlaceholder = cvvPlaceholder + } + } + + var cvvPlaceholder: String { + get { placeholder ?? "" } + set { + attributedPlaceholder = NSAttributedString(string: newValue, attributes: [NSAttributedString.Key.foregroundColor: placeholderColor]) + } + } + + var cursorColor: UIColor { + get { tintColor } + set { tintColor = newValue } + } + + var borderColor: UIColor? = nil { + didSet { + if let borderColor = borderColor { + layer.borderColor = (borderColor.copy() as! UIColor).cgColor + } else { + layer.borderColor = UIColor.clear.cgColor + } + } + } + + private var _borderWidth: CGFloat = 1.0 + var borderWidth: CGFloat { + get { _borderWidth } + set { + _borderWidth = newValue + layer.borderWidth = borderWidth + } + } + + private var _cornerRadius: CGFloat = 5.0 + var cornerRadius: CGFloat { + get { _cornerRadius } + set { + _cornerRadius = newValue + layer.cornerRadius = newValue + } + } + + var contentPadding: UIEdgeInsets = UIEdgeInsets( + top: defaultPadding, + left: defaultPadding, + bottom: defaultPadding, + right: defaultPadding + ) + + var cvvValue: String { + get { self.text ?? "" } + } + + override func textRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.textRect(forBounds: bounds) + return rect.inset(by: contentPadding) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.editingRect(forBounds: bounds) + return rect.inset(by: contentPadding) + } + + func setup() { + self.keyboardType = .numberPad + font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 18)) + textColor = .black + cvvPlaceholder = OPStrings.cvvPlaceholder + placeholderColor = _defaultGray + + borderColor = _defaultGray + borderWidth = _borderWidth + cornerRadius = _cornerRadius + backgroundColor = _defaultBackground + + addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged) + delegate = self + } + + @objc private func textFieldChanged(_ textField: UITextField) { + cvvDelegate?.fieldChanged(self) + } + + @objc public func textFieldDidBeginEditing(_ textField: UITextField) { + cvvDelegate?.didBeginEditing(self) + } + + @objc public func textFieldDidEndEditing(_ textField: UITextField) { + cvvDelegate?.didEndEditing(self) + } + + @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + + //Check max length + let maxCvvLength = 4 + let currentString = (textField.text ?? "") as NSString + let newString = currentString.replacingCharacters(in: range, with: string) + guard newString.count <= maxCvvLength else { + return false + } + + // Limit the text field to decimal digits only + // NOTE: The string.isEmpty check allows the backspace character to delete characters + return string.isEmpty || string.rangeOfCharacter(from: NSCharacterSet.decimalDigits) != nil + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardDetailsInternalView.swift b/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardDetailsInternalView.swift new file mode 100644 index 0000000..344919b --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Controls/OPPaymentCardDetailsInternalView.swift @@ -0,0 +1,142 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentCardDetailsTextFieldInternal.swift +// OloPaySDK +// +// Created by Justin Anderson on 5/18/21. +// + +import Foundation +import Stripe +import UIKit + +protocol OPPaymentCardDetailsViewInternalDelegate: NSObjectProtocol { + func paymentCardDetailsViewDidChange(_ cardDetails: OPPaymentCardDetailsInternalView) + + func paymentCardDetailsViewDidBeginEditing(_ cardDetails: OPPaymentCardDetailsInternalView) + + func paymentCardDetailsViewDidEndEditing(_ cardDetails: OPPaymentCardDetailsInternalView) + + func paymentCardDetailsViewFieldDidBeginEditing(_ cardDetails: OPPaymentCardDetailsInternalView, field: OPCardField) + + func paymentCardDetailsViewFieldDidEndEditing(_ cardDetails: OPPaymentCardDetailsInternalView, field: OPCardField) +} + +//Subclass of STPPaymentCardTextField to allow customization of CVV, Card, and Error images +class OPPaymentCardDetailsInternalView: STPPaymentCardTextField, STPPaymentCardTextFieldDelegate { + @objc public override init(frame: CGRect) { + super.init(frame: frame) + self.delegate = self + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.delegate = self + } + + internal var cardDetailsDelegate: OPPaymentCardDetailsViewInternalDelegate? + + internal func getPaymentMethodParams() -> STPPaymentMethodParams { + let result = STPPaymentMethodParams() + result.card = self.cardParams + result.type = .card + + let address = STPPaymentMethodAddress() + address.postalCode = postalCode + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = address + + result.billingDetails = billingDetails + + return result + } + + // Closure property to change CVV image handler + static var cvvImageClosure: OPCardBrandImageBlock { + get { _cvvImageClosure } + set { _cvvImageClosure = newValue } + } + + // Closure property to change brand image handler + static var brandImageClosure: OPCardBrandImageBlock { + get { _brandImageClosure } + set { _brandImageClosure = newValue } + } + + // Closure property to change error image handler + static var errorImageClosure: OPCardBrandImageBlock { + get { _errorImageClosure } + set { _errorImageClosure = newValue } + } + + // Default CVV image handler that uses Stripe's native images + private static var _cvvImageClosure: OPCardBrandImageBlock = + { brand in STPPaymentCardTextField.cvcImage(for: OPCardBrand.convert(from: brand)) } + + // Default brand image handler that uses Stripe's native images + private static var _brandImageClosure: OPCardBrandImageBlock = + { brand in STPPaymentCardTextField.brandImage(for: OPCardBrand.convert(from: brand)) } + + //Default error image handler that uses Stripe's native images + private static var _errorImageClosure: OPCardBrandImageBlock = + { brand in STPPaymentCardTextField.errorImage(for: OPCardBrand.convert(from: brand)) } + + // Override to allow for cvv image customization + @objc(cvcImageForCardBrand:) override class func cvcImage(for cardBrand: STPCardBrand) -> UIImage? + { _cvvImageClosure(OPCardBrand.convert(from: cardBrand)) } + + // Override to allow for brand image customization + @objc(brandImageForCardBrand:) override class func brandImage(for cardBrand: STPCardBrand) -> UIImage? + { _brandImageClosure(OPCardBrand.convert(from: cardBrand)) } + + // Override to allow for error image customization + @objc(errorImageForCardBrand:) override class func errorImage(for cardBrand: STPCardBrand) -> UIImage? + { _errorImageClosure(OPCardBrand.convert(from: cardBrand)) } + + // STPPaymentCardTedtFieldDelegate + @objc func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewDidChange(self) + } + + @objc func paymentCardTextFieldDidBeginEditing(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewDidBeginEditing(self) + } + + @objc func paymentCardTextFieldDidEndEditing(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewDidEndEditing(self) + } + + @objc func paymentCardTextFieldDidBeginEditingNumber(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing(self, field: OPCardField.number) + } + + @objc func paymentCardTextFieldDidEndEditingNumber(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing(self, field: OPCardField.number) + } + + @objc func paymentCardTextFieldDidBeginEditingCVC(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing(self, field: OPCardField.cvv) + } + + @objc func paymentCardTextFieldDidEndEditingCVC(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing(self, field: OPCardField.cvv) + } + + @objc func paymentCardTextFieldDidBeginEditingExpiration(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing(self, field: OPCardField.expiration) + } + + @objc func paymentCardTextFieldDidEndEditingExpiration(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing(self, field: OPCardField.expiration) + } + + @objc func paymentCardTextFieldDidBeginEditingPostalCode(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidBeginEditing(self, field: OPCardField.postalCode) + } + + @objc func paymentCardTextFieldDidEndEditingPostalCode(_ textField: STPPaymentCardTextField) { + cardDetailsDelegate?.paymentCardDetailsViewFieldDidEndEditing(self, field: OPCardField.postalCode) + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardFieldState.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardFieldState.swift new file mode 100644 index 0000000..a1c5edc --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardFieldState.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardFieldState.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/21/23. +// + +import Foundation + +class OPCardFieldState: NSObject, OPCardFieldStateProtocol { + var isValid = false + var isEmpty = true + var wasEdited = false + var isFirstResponder = false + var wasFirstResponder = false + + @objc required override init() { + super.init() + } + + internal func reset() { + isValid = false + isEmpty = true + wasEdited = false + isFirstResponder = false + wasFirstResponder = false + } + + @objc public override var description: String { + let properties = [ + String(format: "%@: %p", NSStringFromClass(OPCardFieldState.self), self), + "isValid = \(String(describing: isValid))", + "isEmpty = \(String(describing: isEmpty))", + "wasEdited = \(String(describing: wasEdited))", + "isFirstResponder = \(String(describing: isFirstResponder))", + "wasFirstResponder = \(String(describing: wasFirstResponder))" + ] + + return "<\(properties.joined(separator: "; "))>" + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardState.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardState.swift new file mode 100644 index 0000000..d33f655 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCardState.swift @@ -0,0 +1,242 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCardState.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/29/23. +// + +import Foundation +import Stripe + +internal class OPCardState { + var _cardBrand = OPCardBrand.unknown + var _postalCodeTextValid = false + + let fieldStates = [ + OPCardField.number: OPCardFieldState(), + OPCardField.expiration: OPCardFieldState(), + OPCardField.cvv: OPCardFieldState(), + OPCardField.postalCode: OPCardFieldState() + ] + + var cardNumber: OPCardFieldState { + get { fieldStates[.number]! } + } + + var expiration: OPCardFieldState { + get { fieldStates[.expiration]! } + } + + var cvv: OPCardFieldState { + get { fieldStates[.cvv]! } + } + + var postalCode: OPCardFieldState { + get { fieldStates[.postalCode]! } + } + + var postalCodeFieldValid: Bool { + get { postalCodeEnabled ? _postalCodeTextValid : true } + } + + var focusedField: OPCardField? { + get { + let fields = fieldStates.filter({ $0.value.isFirstResponder }) + return fields.first(where: { $0.value.isFirstResponder })?.key ?? nil + } + } + + var isValidCardBrand: Bool { + get { return _cardBrand != .unknown && _cardBrand != .unsupported } + } + + var isValid: Bool { + get { fieldStates.filter({ !$0.value.isValid }).isEmpty } + } + + static var errorMessageHandler: OPCardErrorMessageBlock? = nil + var delegate: OPValidStateChangedDelegate? = nil + + var postalCodeEnabled = true { + didSet { postalCode.isValid = postalCodeFieldValid } + } + + func hasErrorMessage(_ ignoreUneditedFields: Bool) -> Bool { + let errorFields = getErrorFields(ignoreUneditedFields: ignoreUneditedFields) + return !errorFields.isEmpty + } + + func getErrorMessage(_ ignoreUneditedFields: Bool = true) -> String { + guard hasErrorMessage(ignoreUneditedFields) else { + return "" + } + + guard let errorHandler = OPCardState.errorMessageHandler else { + return getDefaultError(ignoreUneditedFields) + } + + // Return custom error messages + return errorHandler(fieldStates as NSDictionary, _cardBrand, ignoreUneditedFields) + } + + func onCardNumberChanged(newText: String, brand: OPCardBrand) { + _cardBrand = brand + + onFieldChanged( + .number, + isEmpty: newText.isEmpty, + isValid: isValidCardBrand && isValidCardNumber(newText), + previousValidState: isValid) + } + + func onExpirationChanged(expirationMonth: String, expirationYear: String) { + onFieldChanged( + .expiration, + isEmpty: expirationMonth.isEmpty && expirationYear.isEmpty, + isValid: isValidExpiration(expirationMonth, expirationYear), + previousValidState: isValid) + } + + func onCvvChanged(newText: String) { + onFieldChanged( + .cvv, + isEmpty: newText.isEmpty, + isValid: isValidCvv(newText), + previousValidState: isValid) + } + + func onPostalCodeChanged(newText: String) { + onFieldChanged( + .postalCode, + isEmpty: newText.isEmpty, + isValid: isValidPostalCode(newText), + previousValidState: isValid) + } + + private func onFieldChanged(_ field: OPCardField, isEmpty: Bool, isValid: Bool, previousValidState: Bool) { + let fieldState = fieldStates[field]! + + fieldState.isEmpty = isEmpty + if !isEmpty { + fieldState.wasEdited = true + } + + fieldState.isValid = isValid + notifyValidStateChanged(previousValidState) + } + + func onBecomeFirstResponder(field: OPCardField) { + onResignFirstResponder() + fieldStates[field]!.isFirstResponder = true + } + + func onResignFirstResponder() { + guard let focusedField = focusedField else { + return + } + + let previousFocusedField = fieldStates[focusedField]! + + // Prevent fields from entering error states if focus changes + // prior to any text being entered in the field + if previousFocusedField.wasEdited { + previousFocusedField.wasFirstResponder = true + } + + previousFocusedField.isFirstResponder = false + } + + func reset() { + let previousValidState = isValid + + fieldStates.forEach { state in + state.value.reset() + } + + _cardBrand = .unknown + _postalCodeTextValid = false + + notifyValidStateChanged(previousValidState) + } + + func isValidCardNumber(_ cardNumber: String) -> Bool { + return STPCardValidator.validationState(forNumber: cardNumber, validatingCardBrand: true) == .valid + } + + func isValidCvv(_ cvv: String) -> Bool { + return STPCardValidator.validationState( + forCVC: cvv, + cardBrand: OPCardBrand.convert(from: _cardBrand)) == .valid + } + + func isValidExpiration(_ expirationMonth: String, _ expirationYear: String) -> Bool { + if STPCardValidator.validationState(forExpirationMonth: expirationMonth) != .valid { + return false + } + + return STPCardValidator.validationState( + forExpirationYear: expirationYear, + inMonth: expirationMonth) == .valid + } + + func isValidPostalCode(_ postalCode: String) -> Bool { + return isValidUsPostalCode(postalCode) || isValidCaPostalCode(postalCode) + } + + func isValidUsPostalCode(_ postalCode: String) -> Bool { + let regEx = #"^\s*[0-9]{5}(-[0-9]{4})?\s*$"# + return postalCode.range(of: regEx, options: .regularExpression) != nil + } + + func isValidCaPostalCode(_ postalCode: String) -> Bool { + let regEx = #"^[ABCEGHJKLMNPRSTVXY][0-9][ABCEGHJKLMNPRSTVWXYZ]\s?[0-9][ABCEGHJKLMNPRSTVWXYZ][0-9]$"# + let upperPostal = postalCode.uppercased() + return upperPostal.range(of: regEx, options: .regularExpression) != nil + } + + func notifyValidStateChanged(_ previousValidState: Bool) { + if previousValidState != isValid { + delegate?.validStateChanged(isValid: isValid) + } + } + + func getDefaultError(_ ignoreUneditedFields: Bool) -> String { + var errorMessage = "" + + if isInvalidField(.number, ignoreUneditedFields) { + if (cardNumber.isEmpty) { + errorMessage = OPStrings.emptyCardNumberError + } else if _cardBrand == .unsupported { + errorMessage = OPStrings.unsupportedCardError + } else { + errorMessage = OPStrings.invalidCardNumberError + } + } else if isInvalidField(.expiration, ignoreUneditedFields) { + errorMessage = expiration.isEmpty ? OPStrings.emptyExpirationError : OPStrings.invalidExpirationError + } else if isInvalidField(.cvv, ignoreUneditedFields) { + errorMessage = cvv.isEmpty ? OPStrings.emptyCvvError : OPStrings.invalidCvvError + } else if isInvalidField(.postalCode, ignoreUneditedFields) { + errorMessage = postalCode.isEmpty ? OPStrings.emptyPostalCodeError : OPStrings.invalidPostalCodeError + } + + return errorMessage + } + + internal func getErrorFields(ignoreUneditedFields: Bool) -> [OPCardField : OPCardFieldState] { + return fieldStates.filter({ isInvalidField($0.key, ignoreUneditedFields) }) + } + + func isInvalidField(_ field: OPCardField, _ ignoreUneditedFields: Bool) -> Bool { + let fieldState = fieldStates[field]! + + // Return error field regardless of edited/focused state + if !ignoreUneditedFields { + return !fieldState.isValid + } + + // Only invalid if the field has also been edited and focused + return !fieldState.isValid && fieldState.wasEdited && fieldState.wasFirstResponder + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvState.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvState.swift new file mode 100644 index 0000000..58a6a6e --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvState.swift @@ -0,0 +1,110 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCvvState.swift +// OloPaySDK +// +// Created by Justin Anderson on 8/21/23. +// + +import Foundation + +// Class to manage state for OPPaymentCardCvvView +class OPCvvState { + let _fieldState = OPCardFieldState() + + static var errorMessageHandler: OPCvvErrorMessageBlock? = nil + + var delegate: OPValidStateChangedDelegate? = nil + + var isValid: Bool { + get { return _fieldState.isValid } + } + + var isFirstResponder: Bool { + get { return _fieldState.isFirstResponder } + } + + func editingCompleted() { + _fieldState.wasEdited = true + _fieldState.wasFirstResponder = true + } + + func onInputChanged(_ newText: String) { + _fieldState.isEmpty = newText.isEmpty + + if !newText.isEmpty { + _fieldState.wasEdited = true + } + + let previousValidState = isValid + _fieldState.isValid = isValidCvv(cvv: newText) + + notifyValidStateChanged(previousValidState: previousValidState) + } + + func onFirstResponderStateChanged(_ isFirstResponder: Bool) { + let wasFirstResponder = _fieldState.isFirstResponder + + // Unless the field was edited, treat it as though the + // field never entered the first responder state (this helps + // prevent displaying an error prematurely) + if wasFirstResponder && !isFirstResponder && _fieldState.wasEdited { + _fieldState.wasFirstResponder = true + } + + _fieldState.isFirstResponder = isFirstResponder + } + + func reset() { + let previousValidState = isValid + _fieldState.reset() + + notifyValidStateChanged(previousValidState: previousValidState) + } + + func hasErrorMessage(_ ignoreUneditedFieldErrors: Bool = true) -> Bool { + if (!ignoreUneditedFieldErrors) { + return !_fieldState.isValid + } + + return !_fieldState.isValid && _fieldState.wasEdited && _fieldState.wasFirstResponder + } + + func getErrorMessage(_ ignoreUneditedFieldErrors: Bool = true) -> String { + guard hasErrorMessage(ignoreUneditedFieldErrors) else { + return "" + } + + guard let errorHandler = OPCvvState.errorMessageHandler else { + // Return default error message + return getCvvError() + } + + // Return custom error messages + return errorHandler(_fieldState, ignoreUneditedFieldErrors) + } + + private func isValidCvv(cvv: String) -> Bool { + let regEx = #"^[0-9]{3,4}$"# + return cvv.range(of: regEx, options: .regularExpression) != nil + } + + private func getCvvError() -> String { + if (_fieldState.isValid) { + return "" + } + + if (_fieldState.isEmpty) { + return OPStrings.emptyCvvError + } + + return OPStrings.incompleteCvvError + } + + private func notifyValidStateChanged(previousValidState: Bool) { + if previousValidState != isValid { + delegate?.validStateChanged(isValid: isValid) + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvTokenParams.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvTokenParams.swift new file mode 100644 index 0000000..df2a678 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvTokenParams.swift @@ -0,0 +1,22 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCvvTokenParams.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/11/23. +// + +import Foundation + +class OPCvvTokenParams : NSObject, OPCvvTokenParamsProtocol { + private let _cvv: String + + internal var cvv: String { + get { return _cvv } + } + + internal init(_ cvv: String) { + _cvv = cvv + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvUpdateToken.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvUpdateToken.swift new file mode 100644 index 0000000..f6179c9 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPCvvUpdateToken.swift @@ -0,0 +1,36 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPCvvUpdateToken.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/11/23. +// + +import Foundation +import Stripe + +class OPCvvUpdateToken: NSObject, OPCvvUpdateTokenProtocol { + let _token: STPToken + + internal required init(_ token: STPToken) { + _token = token + super.init() + } + + @objc public var id: String { _token.tokenId } + + @objc public var environment: OPEnvironment { + return _token.livemode ? OPEnvironment.production : OPEnvironment.test + } + + @objc public override var description: String { + let properties = [ + String(format: "%@: %p", NSStringFromClass(OPCvvUpdateToken.self), self), + "id = \(id)", + "environment = \(String(describing: environment))" + ] + + return "<\(properties.joined(separator: "; "))>" + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPHelpers.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPHelpers.swift new file mode 100644 index 0000000..0cd98a0 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPHelpers.swift @@ -0,0 +1,18 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPHelpers.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// + +import Foundation + +func dispatchToMainThreadIfNecessary(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async(execute: block) + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataGenerator.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataGenerator.swift new file mode 100644 index 0000000..6618654 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataGenerator.swift @@ -0,0 +1,54 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// MetadataGenerator.swift +// OloPaySDK +// +// Created by Richard Dowdy on 5/30/23. +// + +import Foundation +import UIKit + +class OPMetadataGenerator: NSObject { + + private var _source: OPPaymentMethodSource + private var _applePayMerchantId: String? = nil + private var _applePayCompanyLabel: String? = nil + + init (_ source: OPPaymentMethodSource) { + _source = source + } + + init (applePayMerchantId: String?, applePayCompanyLabel: String?) { + _source = OPPaymentMethodSource.applePay + _applePayMerchantId = applePayMerchantId + _applePayCompanyLabel = applePayCompanyLabel + } + + internal func generate() -> [String: String] { + let buildType = OPSdkBuildType.convert(from: OPSdkBuild.buildType) + + var metadata: [String: String] = [ + OPMetadataStrings.creationSourceKey: _source.description, + OPMetadataStrings.sdkBuildTypeKey: buildType?.description ?? OPMetadataStrings.unknownValue, + OPMetadataStrings.sdkVersionKey: OPSdkVersion.version, + OPMetadataStrings.sdkPlatformKey: OPMetadataStrings.sdkPlatformValue, + OPMetadataStrings.iosApiVersionKey: UIDevice.current.systemVersion, + OPMetadataStrings.sdkEnvironmentKey: OloPayAPI.environment.description, + ] + + if let hybridInfo = OloPayAPI.sdkWrapperInfo { + metadata[OPMetadataStrings.hybridSdkPlatformKey] = hybridInfo.platform + metadata[OPMetadataStrings.hybridSdkVersionKey] = hybridInfo.version + metadata[OPMetadataStrings.hybridSdkBuildTypeKey] = hybridInfo.buildType + } + + if(_source == OPPaymentMethodSource.applePay){ + metadata[OPMetadataStrings.digitalWalletCompanyLabelKey] = _applePayCompanyLabel ?? "" + metadata[OPMetadataStrings.applePayMerchantIdKey] = _applePayMerchantId ?? "" + } + + return metadata + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataStrings.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataStrings.swift new file mode 100644 index 0000000..026eccb --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPMetadataStrings.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPMetadataStrings.swift +// OloPaySDK +// +// Created by Richard Dowdy on 5/30/23. +// + +import Foundation + +class OPMetadataStrings: NSObject { + // THESE KEYS EXIST FOR ALL GENERATED METADATA + public static let creationSourceKey = "CreationSource" + public static let sdkBuildTypeKey = "BuildType" + public static let sdkVersionKey = "Version" + public static let sdkPlatformKey = "Platform" + public static let iosApiVersionKey = "OSVersion" + public static let sdkEnvironmentKey = "Environment" + + // THESE KEYS ONLY EXIST IF HYBRID SDK DATA IS SET + public static let hybridSdkBuildTypeKey = "HybridBuildType" + public static let hybridSdkPlatformKey = "HybridPlatform" + public static let hybridSdkVersionKey = "HybridVersion" + + // THESE KEYS ONLY EXIST IF THE SOURCE IS APPLE PAY + public static let digitalWalletCompanyLabelKey = "DigitalWalletCompanyLabel" + public static let applePayMerchantIdKey = "ApplePayMerchantId" + + // NOT A FIELD - A STRING FOR THE PLATFORM + public static let sdkPlatformValue = "ios" + + public static let unknownValue = "unknown" +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethod.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethod.swift new file mode 100644 index 0000000..7315bc0 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethod.swift @@ -0,0 +1,55 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentMethod.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/11/23. +// + +import Foundation +import Stripe + +class OPPaymentMethod: NSObject, OPPaymentMethodProtocol { + var _paymentMethod: STPPaymentMethod + + @objc required init(paymentMethod: STPPaymentMethod) { + _paymentMethod = paymentMethod + super.init() + } + + @objc public var id: String { _paymentMethod.stripeId } + + @objc public var last4: String? { _paymentMethod.card?.last4 } + + @objc public var cardType: OPCardBrand { OPCardBrand.convert(from: _paymentMethod.card?.brand) } + + @objc public var expirationMonth: NSNumber? { _paymentMethod.card?.expMonth as NSNumber? } + + @objc public var expirationYear: NSNumber? { _paymentMethod.card?.expYear as NSNumber? } + + @objc public var postalCode: String? { _paymentMethod.billingDetails?.address?.postalCode } + + @objc public var isApplePay: Bool { _paymentMethod.card?.wallet?.type == STPPaymentMethodCardWalletType.applePay } + + @objc public var country: String? { _paymentMethod.card?.country?.replacingOccurrences(of: "\"", with: "") } + + @objc public var environment: OPEnvironment { _paymentMethod.liveMode ? .production : .test } + + @objc public override var description: String { + let properties = [ + String(format: "%@: %p", NSStringFromClass(OPPaymentMethod.self), self), + "id = \(id)", + "last4 = \(String(describing: last4))", + "cardType = \(String(describing: cardType))", + "expirationMonth = \(String(describing: expirationMonth))", + "expirationYear = \(String(describing: expirationYear))", + "postalCode = \(String(describing: postalCode))", + "isApplePay = \(String(describing: isApplePay))", + "country = \(String(describing: country))", + "environment = \(environment.description)" + ] + + return "<\(properties.joined(separator: "; "))>" + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodParams.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodParams.swift new file mode 100644 index 0000000..6b48575 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodParams.swift @@ -0,0 +1,25 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentMethodParams.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/11/23. +// + +import Foundation +import Stripe + +class OPPaymentMethodParams : NSObject, OPPaymentMethodParamsProtocol { + private var _paymentMethodParams : STPPaymentMethodParams + + internal var paymentMethodParams: STPPaymentMethodParams { + get { return _paymentMethodParams } + } + + internal init(_ paymentMethodParams : STPPaymentMethodParams, fromSource source: OPPaymentMethodSource) { + _paymentMethodParams = paymentMethodParams + let _metadataGenerator = OPMetadataGenerator(source) + _paymentMethodParams.metadata = _metadataGenerator.generate() + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodSource.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodSource.swift new file mode 100644 index 0000000..c4852af --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPaymentMethodSource.swift @@ -0,0 +1,28 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPaymentMethodSource.swift +// OloPaySDK +// +// Created by Richard Dowdy on 5/30/23. +// + +import Foundation + +enum OPPaymentMethodSource: Int, CustomStringConvertible { + + case singleLineInput + case formInput + case applePay + + public var description: String { + switch self { + case .singleLineInput: + return "singleLineInput" + case .formInput: + return "formInput" + case .applePay: + return "applePay" + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPPublishableKey.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPublishableKey.swift new file mode 100644 index 0000000..aef8e86 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPPublishableKey.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPPublishableKey.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// + +import Foundation + +struct OPPublishableKey : Decodable { + let key: String +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkBuild.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkBuild.swift new file mode 100644 index 0000000..5afdcf2 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkBuild.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSdkBuild.swift +// OloPaySDK +// +// Created by Justin Anderson on 10/29/24. +// + +import Foundation + +internal class OPSdkBuild { + internal static let buildType = "Public" +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkVersion.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkVersion.swift new file mode 100644 index 0000000..ece55f0 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPSdkVersion.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPSdkVersion.swift +// OloPaySDK +// +// Created by Justin Anderson on 10/29/24. +// + +import Foundation + +internal class OPSdkVersion { + internal static let version = "4.1.0" +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorage.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorage.swift new file mode 100644 index 0000000..2563117 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorage.swift @@ -0,0 +1,41 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPStorage.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/16/21. +// + +import Foundation + +class OPStorage { + @OPStorageWrapper(key: "olopayapi_publishable_key_test", defaultValue: "") + static var testPublishableKey: String + + @OPStorageWrapper(key: "olopayapi_publishable_key_prod", defaultValue: "") + static var productionPublishableKey: String + + @OPStorageWrapper(key: "olopayapi_environment_key", defaultValue: OPEnvironment.production.description) + static var environment: String + + static func getPublishableKey(environment: OPEnvironment) -> String { + return environment == OPEnvironment.test ? + testPublishableKey : + productionPublishableKey + } + + static func setPublishableKey(environment: OPEnvironment, value: String) { + if(environment == OPEnvironment.test) { + testPublishableKey = value + } else { + productionPublishableKey = value + } + } + + internal static func reset() { + setPublishableKey(environment: .test, value: "") + setPublishableKey(environment: .production, value: "") + environment = OPEnvironment.production.description + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorageWrapper.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorageWrapper.swift new file mode 100644 index 0000000..1abcfb5 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPStorageWrapper.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPStorageWrapper.swift +// OloPaySDK +// +// Created by Justin Anderson on 6/11/21. +// + +import Foundation + +@propertyWrapper +class OPStorageWrapper { + private let key: String + private let defaultValue: String + + init(key: String, defaultValue: String) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: String { + get { UserDefaults.standard.string(forKey: key) ?? defaultValue } + set { UserDefaults.standard.set(newValue, forKey: key) } + } +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Data/OPValidStateChangedDelegate.swift b/src/OloPaySDK/OloPaySDK/Internal/Data/OPValidStateChangedDelegate.swift new file mode 100644 index 0000000..cc6502b --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Data/OPValidStateChangedDelegate.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPValidStateChangedDelegate.swift +// OloPaySDK +// +// Created by Justin Anderson on 9/29/23. +// + +import Foundation + +protocol OPValidStateChangedDelegate: NSObjectProtocol { + func validStateChanged(isValid: Bool) +} diff --git a/src/OloPaySDK/OloPaySDK/Internal/Extensions/UIViewExtensions.swift b/src/OloPaySDK/OloPaySDK/Internal/Extensions/UIViewExtensions.swift new file mode 100644 index 0000000..933a518 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/Internal/Extensions/UIViewExtensions.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// UIViewExtensions.swift +// OloPaySDK +// +// Created by Richard Dowdy on 7/24/24. +// + +import Foundation +import UIKit + +internal extension UIView { + class func getAllTextFields(from parentView: UIView) -> [UITextField] { + return parentView.subviews.flatMap { subView -> [UITextField] in + var result = getAllTextFields(from: subView) as [UITextField] + + if let textField = subView as? UITextField { + result.append(textField) + return result + } + + return result + } + } +} diff --git a/src/OloPaySDK/OloPaySDK/OloPaySDK.h b/src/OloPaySDK/OloPaySDK/OloPaySDK.h new file mode 100644 index 0000000..4aaec82 --- /dev/null +++ b/src/OloPaySDK/OloPaySDK/OloPaySDK.h @@ -0,0 +1,20 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPaySDK.h +// OloPaySDK +// +// Created by Kyle Szklenski on 4/29/21. +// + +#import + +//! Project version number for OloPaySDK. +FOUNDATION_EXPORT double OloPaySDKVersionNumber; + +//! Project version string for OloPaySDK. +FOUNDATION_EXPORT const unsigned char OloPaySDKVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/src/OloPaySDK/OloPaySDKTests/CardBrandTests.swift b/src/OloPaySDK/OloPaySDKTests/CardBrandTests.swift new file mode 100644 index 0000000..8f22b16 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CardBrandTests.swift @@ -0,0 +1,48 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardBrandTypeTests.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/14/21. +// + +import XCTest +@testable import OloPaySDK +import Stripe + +class CardBrandTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testConvertFrom_StripeBrand_To_OloPayBrand() throws { + let cases = [(STPCardBrand.visa, OPCardBrand.visa, true), (STPCardBrand.mastercard, OPCardBrand.mastercard, true), (STPCardBrand.amex, OPCardBrand.amex, true), + (STPCardBrand.discover, OPCardBrand.discover, true), (STPCardBrand.unknown, OPCardBrand.unknown, true), (STPCardBrand.JCB, OPCardBrand.unsupported, true), + (STPCardBrand.dinersClub, OPCardBrand.unsupported, true), (STPCardBrand.unionPay, OPCardBrand.unsupported, true), (STPCardBrand.visa, OPCardBrand.unknown, false), + (STPCardBrand.mastercard, OPCardBrand.visa, false), (STPCardBrand.unknown, OPCardBrand.discover, false)] + cases.forEach { + XCTAssertEqual(OPCardBrand.convert(from: $0) == $1, $2) + } + } + + func testConvertFrom_OloPayBrand_To_StripeBrand() throws { + let cases = [(STPCardBrand.visa, OPCardBrand.visa, true), (STPCardBrand.mastercard, OPCardBrand.mastercard, true), (STPCardBrand.amex, OPCardBrand.amex, true), + (STPCardBrand.discover, OPCardBrand.discover, true), (STPCardBrand.unknown, OPCardBrand.unknown, true)] + cases.forEach { + XCTAssertEqual(OPCardBrand.convert(from: $1) == $0, $2) + } + } + + func testConvertFrom_OloPayCardBrand_To_String() throws { + let cases = [(OPCardBrand.amex, "Amex"), (OPCardBrand.discover, "Discover"), (OPCardBrand.mastercard, "Mastercard"), (OPCardBrand.visa, "Visa"), (OPCardBrand.unknown, "Unknown")] + cases.forEach { + XCTAssertEqual($0.description, $1) + } + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/CardErrorTypeTests.swift b/src/OloPaySDK/OloPaySDKTests/CardErrorTypeTests.swift new file mode 100644 index 0000000..cf57def --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CardErrorTypeTests.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardErrorTypeTest.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/8/21. +// + +import XCTest +import Stripe +@testable import OloPaySDK + +class CardErrorTypeTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testFrom_STPCardErrorType_to_CardErrorType() throws { + let cases = [(STPCardErrorCode.cardDeclined, OPCardErrorType.cardDeclined), (STPCardErrorCode.expiredCard, OPCardErrorType.expiredCard), + (STPCardErrorCode.incorrectCVC, OPCardErrorType.invalidCvv), (STPCardErrorCode.incorrectZip, OPCardErrorType.invalidZip), + (STPCardErrorCode.incorrectNumber, OPCardErrorType.invalidNumber), (STPCardErrorCode.invalidCVC, OPCardErrorType.invalidCvv), + (STPCardErrorCode.invalidNumber, OPCardErrorType.invalidNumber), (STPCardErrorCode.invalidExpYear, OPCardErrorType.invalidExpYear), + (STPCardErrorCode.invalidExpMonth, OPCardErrorType.invalidExpMonth), (STPCardErrorCode.processingError, OPCardErrorType.processingError)] + cases.forEach { + XCTAssertEqual(OPCardErrorType.convert(from: $0), $1) + } + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/CardFieldTests.swift b/src/OloPaySDK/OloPaySDKTests/CardFieldTests.swift new file mode 100644 index 0000000..0c22b49 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CardFieldTests.swift @@ -0,0 +1,29 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardFieldTests.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/14/21. +// +import XCTest +import Stripe +@testable import OloPaySDK + +class CardFieldTests : XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testConvertFrom_OloPayCardField_To_String() throws { + let cases = [(OPCardField.number, "number"), (OPCardField.expiration, "expiration"), (OPCardField.cvv, "cvv"), (OPCardField.postalCode, "postalCode"), (OPCardField.unknown, "unknown")] + cases.forEach { + XCTAssertEqual($0.description, $1) + } + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/CardFormStyleTests.swift b/src/OloPaySDK/OloPaySDKTests/CardFormStyleTests.swift new file mode 100644 index 0000000..8efa75e --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CardFormStyleTests.swift @@ -0,0 +1,38 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardFormStyleTests.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/14/21. +// + +import XCTest +import Stripe +@testable import OloPaySDK + +class CardFormStyleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testConvertFrom_StripeCardFormViewStyle_To_OloPayCardFormStyle() throws { + let cases = [(STPCardFormViewStyle.borderless, OPCardFormStyle.borderless), (STPCardFormViewStyle.standard, OPCardFormStyle.standard)] + cases.forEach { + XCTAssertEqual(OPCardFormStyle.convert(from: $0), $1) + } + } + + func testConvertFrom_OloPayCardFormStyle_To_StripeCardFormViewStyle() throws { + let cases = [(STPCardFormViewStyle.borderless, OPCardFormStyle.borderless), (STPCardFormViewStyle.standard, OPCardFormStyle.standard)] + cases.forEach { + XCTAssertEqual(OPCardFormStyle.convert(from: $1), $0) + } + } + +} diff --git a/src/OloPaySDK/OloPaySDKTests/CardStateTests.swift b/src/OloPaySDK/OloPaySDKTests/CardStateTests.swift new file mode 100644 index 0000000..cae43ed --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CardStateTests.swift @@ -0,0 +1,643 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardStateTests.swift +// OloPaySDKTests +// +// Created by Justin Anderson on 9/29/23. +// + +import XCTest +@testable import OloPaySDK + +final class CardStateTests: XCTestCase { + lazy var _cardState: OPCardState? = nil + var cardState: OPCardState { + get { _cardState! } + } + + override func setUpWithError() throws { + _cardState = OPCardState() + } + + override func tearDownWithError() throws { + OPCardState.errorMessageHandler = nil + } + + func testConstructor_validInitialState() { + XCTAssertFalse(cardState.isValid) + XCTAssertTrue(cardState.postalCodeEnabled) + XCTAssertFalse(cardState._postalCodeTextValid) + XCTAssertEqual(OPCardBrand.unknown, cardState._cardBrand) + } + + func testFocusedField_withoutFocusedFields_returnsNil() { + XCTAssertNil(cardState.focusedField) + } + + func testFocusedField_withFocusedField_returnsFocusedField() { + cardState.expiration.isFirstResponder = true + XCTAssertEqual(OPCardField.expiration, cardState.focusedField) + } + + func testIsValid_inInitialState_returnsFalse() { + XCTAssertFalse(cardState.isValid) + } + + func testIsValid_withAllFieldsValid_returnsTrue() { + cardState.fieldStates.forEach({$0.value.isValid = true}) + XCTAssertTrue(cardState.isValid) + } + + func testIsValid_withInvalidField_returnsFalse() { + let fieldStates = cardState.fieldStates + fieldStates.forEach({$0.value.isValid = true}) + + fieldStates.forEach({ fieldState in + fieldState.value.isValid = false + XCTAssertFalse(cardState.isValid) + fieldState.value.isValid = true + }) + } + + func testPostalCodeFieldValid_postalCodeNotEnabled_fieldStateInvalid_returnsTrue() { + cardState.postalCode.isValid = false + cardState.postalCodeEnabled = false + XCTAssertTrue(cardState.postalCodeFieldValid) + } + + func testPostalCodeFieldValid_postalCodeEnabled_fieldStateValid_returnsTrue() { + cardState.postalCode.isValid = true + cardState._postalCodeTextValid = true + cardState.postalCodeEnabled = true + XCTAssertTrue(cardState.postalCodeFieldValid) + } + + func testPostalCodeFieldValid_postalCodeEnabled_fieldStateNotValid_returnsFalse() { + cardState.postalCode.isValid = false + cardState.postalCodeEnabled = true + XCTAssertFalse(cardState.postalCodeFieldValid) + } + + func testIsValidCardBrand_cardBrandInvalid_returnsFalse() { + cardState._cardBrand = .unknown + XCTAssertFalse(cardState.isValidCardBrand) + + cardState._cardBrand = .unsupported + XCTAssertFalse(cardState.isValidCardBrand) + } + + func testIsValidCardBrand_cardBrandValid_returnsTrue() { + cardState._cardBrand = .visa + XCTAssertTrue(cardState.isValidCardBrand) + + cardState._cardBrand = .amex + XCTAssertTrue(cardState.isValidCardBrand) + + cardState._cardBrand = .mastercard + XCTAssertTrue(cardState.isValidCardBrand) + + cardState._cardBrand = .discover + XCTAssertTrue(cardState.isValidCardBrand) + } + + func testHasErrorMessage_notIgnoreUneditedFields_withInvalidField_returnsTrue() { + let invalidField = OPCardField.cvv + + cardState.fieldStates.forEach { + $0.value.isValid = $0.key != invalidField + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + } + + XCTAssertTrue(cardState.hasErrorMessage(false)) + } + + func testHasErrorMessage_notIgnoreUneditedFields_withoutInvalidField_returnsFalse() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + XCTAssertFalse(cardState.hasErrorMessage(false)) + } + + func testHasErrorMessage_ignoreUneditedFields_withInvalidEditedField_returnsTrue() { + let invalidField = OPCardField.expiration + + cardState.fieldStates.forEach { + $0.value.isValid = $0.key != invalidField + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + } + + XCTAssertTrue(cardState.hasErrorMessage(true)) + } + + func testHasErrorMessage_ignoreUneditedFields_withInvalidUneditedField_returnsFalse() { + let invalidField = OPCardField.number + + cardState.fieldStates.forEach { + $0.value.isValid = $0.key != invalidField + } + + XCTAssertFalse(cardState.hasErrorMessage(true)) + } + + func testGetErrorMessage_withoutCustomErrors_invalidNumber_returnsDefaultErrors() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + cardState.cardNumber.isValid = false + cardState.cardNumber.isEmpty = true + XCTAssertEqual(OPStrings.emptyCardNumberError, cardState.getErrorMessage(false)) + + cardState.cardNumber.isEmpty = false + XCTAssertEqual(OPStrings.invalidCardNumberError, cardState.getErrorMessage(false)) + + cardState._cardBrand = .unknown + XCTAssertEqual(OPStrings.invalidCardNumberError, cardState.getErrorMessage(false)) + + cardState._cardBrand = .unsupported + XCTAssertEqual(OPStrings.unsupportedCardError, cardState.getErrorMessage(false)) + } + + func testGetErrorMessage_withoutCustomErrors_invalidExpiration_returnsDefaultErrors() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + cardState.expiration.isValid = false + cardState.expiration.isEmpty = true + XCTAssertEqual(OPStrings.emptyExpirationError, cardState.getErrorMessage(false)) + + cardState.expiration.isEmpty = false + XCTAssertEqual(OPStrings.invalidExpirationError, cardState.getErrorMessage(false)) + } + + func testGetErrorMessage_withoutCustomErrors_invalidCvv_returnsDefaultErrors() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + cardState.cvv.isValid = false + cardState.cvv.isEmpty = true + XCTAssertEqual(OPStrings.emptyCvvError, cardState.getErrorMessage(false)) + + cardState.cvv.isEmpty = false + XCTAssertEqual(OPStrings.invalidCvvError, cardState.getErrorMessage(false)) + } + + func testGetErrorMessage_withoutCustomErrors_invalidPostalCode_returnsDefaultErrors() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + cardState.postalCode.isValid = false + cardState.postalCode.isEmpty = true + XCTAssertEqual(OPStrings.emptyPostalCodeError, cardState.getErrorMessage(false)) + + cardState.postalCode.isEmpty = false + XCTAssertEqual(OPStrings.invalidPostalCodeError, cardState.getErrorMessage(false)) + } + + func testGetErrorMessage_withCustomErrors_returnsCustomError() { + OPCardState.errorMessageHandler = customErrorMessageHandler(_:_:_:) + cardState.cardNumber.isValid = false + + XCTAssertEqual("Custom Error", cardState.getErrorMessage(false)) + } + + func testOnCardNumberChanged_cardBrandSet() { + XCTAssertEqual(OPCardBrand.unknown, cardState._cardBrand) + + cardState.onCardNumberChanged(newText: "", brand: .visa) + XCTAssertEqual(OPCardBrand.visa, cardState._cardBrand) + } + + func testOnFieldChanged_textEmpty_fieldEmptyAndNotEdited() { + cardState.onCardNumberChanged(newText: "", brand: .unknown) + cardState.onExpirationChanged(expirationMonth: "", expirationYear: "") + cardState.onCvvChanged(newText: "") + cardState.onPostalCodeChanged(newText: "") + + cardState.fieldStates.forEach { + XCTAssertTrue($0.value.isEmpty) + XCTAssertFalse($0.value.wasEdited) + } + } + + func testOnFieldChanged_textNotEmpty_fieldEditedAndNotEmpty() { + cardState.onCardNumberChanged(newText: "Foo", brand: .unknown) + cardState.onExpirationChanged(expirationMonth: "Foo", expirationYear: "Bar") + cardState.onCvvChanged(newText: "Foo") + cardState.onPostalCodeChanged(newText: "Bar") + + cardState.fieldStates.forEach { + XCTAssertFalse($0.value.isEmpty) + XCTAssertTrue($0.value.wasEdited) + } + } + + func testOnFieldChanged_fieldStartsEdited_textEmpty_fieldStillEdited() { + cardState.fieldStates.forEach { + $0.value.wasEdited = true + } + + cardState.onCardNumberChanged(newText: "", brand: .unknown) + cardState.onExpirationChanged(expirationMonth: "", expirationYear: "") + cardState.onCvvChanged(newText: "") + cardState.onPostalCodeChanged(newText: "") + + cardState.fieldStates.forEach { + XCTAssertTrue($0.value.wasEdited) + } + } + + func testOnFieldChanged_isValidToggledTrue_delegateCalled() { + let invalidField = OPCardField.postalCode + + cardState.fieldStates.forEach { + $0.value.isValid = $0.key != invalidField + } + + let delegate = MockValidStateChangedDelegate() + cardState.delegate = delegate + + XCTAssertFalse(cardState.isValid) + cardState.onPostalCodeChanged(newText: "55056") + + XCTAssertTrue(cardState.isValid) + XCTAssertTrue(delegate.validStateChangedCalled) + XCTAssertNotNil(delegate.validStateChangedParameter) + XCTAssertTrue(delegate.validStateChangedParameter!) + } + + func testOnFieldChanged_isValidToggledFalse_delegateCalled() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + let delegate = MockValidStateChangedDelegate() + cardState.delegate = delegate + + XCTAssertTrue(cardState.isValid) + cardState.onCardNumberChanged(newText: "", brand: .visa) + + XCTAssertFalse(cardState.isValid) + XCTAssertTrue(delegate.validStateChangedCalled) + XCTAssertNotNil(delegate.validStateChangedParameter) + XCTAssertFalse(delegate.validStateChangedParameter!) + } + + func testOnFieldChanged_isValidNotToggled_delegateNotCalled() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + let delegate = MockValidStateChangedDelegate() + cardState.delegate = delegate + + XCTAssertTrue(cardState.isValid) + cardState.onPostalCodeChanged(newText: "12345") + + XCTAssertTrue(cardState.isValid) + XCTAssertFalse(delegate.validStateChangedCalled) + XCTAssertNil(delegate.validStateChangedParameter) + } + + func testOnBecomeFirstResponder_fieldBecomesFirstResponder() { + cardState.fieldStates.forEach { + cardState.onBecomeFirstResponder(field: $0.key) + XCTAssertTrue(cardState.fieldStates[$0.key]!.isFirstResponder) + } + } + + func testOnBecomeFirstResponder_previousFirstResponderField_notFirstResponder() { + cardState.cardNumber.isFirstResponder = true + cardState.onBecomeFirstResponder(field: .expiration) + + XCTAssertFalse(cardState.cardNumber.isFirstResponder) + } + + func testOnResignFirstResponder_focusedFieldWasEdited_wasFirstResponderSet() { + cardState.fieldStates.forEach { + $0.value.isFirstResponder = true + $0.value.wasEdited = true + cardState.onResignFirstResponder() + XCTAssertTrue(cardState.fieldStates[$0.key]!.wasFirstResponder) + } + } + + func testOnResignFirstResponder_focusedFieldWasNotEdited_wasFirstResponderNotSet() { + cardState.fieldStates.forEach { + $0.value.isFirstResponder = true + $0.value.wasEdited = false + cardState.onResignFirstResponder() + XCTAssertFalse(cardState.fieldStates[$0.key]!.wasFirstResponder) + } + } + + func testOnResignFirstResponder_focusedFieldWasFirstResponder_isFirstResponderCleared() { + cardState.fieldStates.forEach { + $0.value.isFirstResponder = true + cardState.onResignFirstResponder() + XCTAssertFalse(cardState.fieldStates[$0.key]!.isFirstResponder) + } + } + + func testReset_cardBrand_setToUnknown() { + cardState._cardBrand = .visa + cardState.reset() + XCTAssertEqual(OPCardBrand.unknown, cardState._cardBrand) + } + + func testReset_postalCodeTextValid_setToFalse() { + cardState._postalCodeTextValid = true + cardState.reset() + XCTAssertFalse(cardState._postalCodeTextValid) + } + + func testReset_isValidToggledFalse_delegateCalled() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + let delegate = MockValidStateChangedDelegate() + cardState.delegate = delegate + + XCTAssertTrue(cardState.isValid) + cardState.reset() + + XCTAssertFalse(cardState.isValid) + XCTAssertTrue(delegate.validStateChangedCalled) + XCTAssertNotNil(delegate.validStateChangedParameter) + XCTAssertFalse(delegate.validStateChangedParameter!) + } + + func testReset_isValidNotToggledFalse_delegateNotCalled() { + let delegate = MockValidStateChangedDelegate() + cardState.delegate = delegate + + XCTAssertFalse(cardState.isValid) + cardState.reset() + + XCTAssertFalse(cardState.isValid) + XCTAssertFalse(delegate.validStateChangedCalled) + XCTAssertNil(delegate.validStateChangedParameter) + } + + func testIsValidUSPostalCode_validPostalCode_returnsTrue() { + XCTAssertTrue(cardState.isValidUsPostalCode("55056")) + } + + func testIsValidUSPostalCode_postalCodeTooShort_returnsFalse() { + XCTAssertFalse(cardState.isValidUsPostalCode("550")) + } + + func testIsValidUSPostalCode_postalCodeTooLong_returnsFalse() { + XCTAssertFalse(cardState.isValidUsPostalCode("1234567890")) + } + + func testIsValidUSPostalCode_postalCodeNonDigits_returnsFalse() { + XCTAssertFalse(cardState.isValidUsPostalCode("55A56")) + XCTAssertFalse(cardState.isValidUsPostalCode("5.056")) + } + + func testIsValidCAPostalCode_validPostalCode_returnsTrue() { + XCTAssertTrue(cardState.isValidCaPostalCode("A1A 1A1")) + } + + func testIsValidCaPostalCode_postalCodeContainsOnlyDigits_returnsFalse() { + XCTAssertFalse(cardState.isValidCaPostalCode("111 121")) + } + + func testIsValidPostalCode_containsValidPostalCode_returnsTrue() { + XCTAssertTrue(cardState.isValidPostalCode("55056")) + XCTAssertTrue(cardState.isValidPostalCode("A1A 1A1")) + } + + func testIsValidPostalCode_containsInvalidPostalCode_returnsFalse() { + XCTAssertFalse(cardState.isValidPostalCode("55.56")) + XCTAssertFalse(cardState.isValidPostalCode("A1A41A1")) + } + + func testIsInvalidField_ignoreUneditedFields_fieldInvalid_wasEdited_wasFirstResponder_returnsTrue() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = false + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + XCTAssertTrue(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldInvalid_wasEdited_notWasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = false + $0.value.wasEdited = true + $0.value.wasFirstResponder = false + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldInvalid_notWasEdited_wasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = false + $0.value.wasEdited = false + $0.value.wasFirstResponder = true + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldInvalid_notWasEdited_notWasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = false + $0.value.wasEdited = false + $0.value.wasFirstResponder = false + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldValid_wasEdited_wasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = true + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldValid_wasEdited_notWasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = true + $0.value.wasEdited = true + $0.value.wasFirstResponder = false + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldValid_notWasEdited_wasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = true + $0.value.wasEdited = false + $0.value.wasFirstResponder = true + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_ignoreUneditedFields_fieldValid_notWasEdited_notWasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + + $0.value.isValid = true + $0.value.wasEdited = false + $0.value.wasFirstResponder = false + XCTAssertFalse(cardState.isInvalidField($0.key, true)) + } + } + + func testIsInvalidField_notIgnoreUneditedFields_fieldInvalid_returnsTrue() { + cardState.fieldStates.forEach { + $0.value.isValid = false + XCTAssertTrue(cardState.isInvalidField($0.key, false)) + } + } + + func testIsInvalidField_notIgnoreUneditedFields_fieldInvalid_wasEdited_wasFirstResponder_returnsTrue() { + cardState.fieldStates.forEach { + XCTAssertTrue(cardState.isInvalidField($0.key, false)) + $0.value.isValid = false + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + XCTAssertTrue(cardState.isInvalidField($0.key, false)) + } + } + + func testIsInvalidField_notIgnoreUneditedFields_fieldValid_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertTrue(cardState.isInvalidField($0.key, false)) + $0.value.isValid = true + XCTAssertFalse(cardState.isInvalidField($0.key, false)) + } + } + + func testIsInvalidField_notIgnoreUneditedFields_fieldValid_wasEdited_wasFirstResponder_returnsFalse() { + cardState.fieldStates.forEach { + XCTAssertTrue(cardState.isInvalidField($0.key, false)) + + $0.value.isValid = true + $0.value.wasEdited = true + $0.value.wasFirstResponder = true + XCTAssertFalse(cardState.isInvalidField($0.key, false)) + } + } + + func testGetErrorFields_ignoreUneditedFields_hasErrorFields_returnsErrorFields() { + cardState.cardNumber.isValid = false + cardState.cardNumber.wasEdited = true + cardState.cardNumber.wasFirstResponder = true + + cardState.expiration.isValid = false + cardState.expiration.wasEdited = true + cardState.expiration.wasFirstResponder = true + + let invalidFields = cardState.getErrorFields(ignoreUneditedFields: true) + XCTAssertEqual(2, invalidFields.count) + XCTAssertTrue(invalidFields.contains{ $0.key == .number }) + XCTAssertTrue(invalidFields.contains{ $0.key == .expiration }) + } + + func testGetErrorFields_ignoreUneditedFields_hasNoErrorFields_returnsNoFields() { + cardState.cardNumber.isValid = true + cardState.cardNumber.wasEdited = true + cardState.cardNumber.wasFirstResponder = true + + cardState.expiration.isValid = false + cardState.expiration.wasEdited = true + cardState.expiration.wasFirstResponder = false + + cardState.cvv.isValid = false + cardState.cvv.wasEdited = false + cardState.cvv.wasFirstResponder = true + + cardState.postalCode.isValid = false + cardState.postalCode.wasEdited = false + cardState.postalCode.wasFirstResponder = false + + let invalidFields = cardState.getErrorFields(ignoreUneditedFields: true) + + XCTAssertEqual(0, invalidFields.count) + } + + func testGetErrorFields_notIgnoreUneditedFields_hasErrorFields_returnsErrorFields() { + cardState.cardNumber.wasEdited = true + cardState.cardNumber.wasFirstResponder = true + + cardState.expiration.wasEdited = true + cardState.expiration.wasFirstResponder = false + + cardState.cvv.wasEdited = false + cardState.cvv.wasFirstResponder = true + + cardState.postalCode.wasEdited = false + cardState.postalCode.wasFirstResponder = false + + let invalidFields = cardState.getErrorFields(ignoreUneditedFields: false) + + XCTAssertEqual(4, invalidFields.count) + XCTAssertTrue(invalidFields.contains{ $0.key == .number }) + XCTAssertTrue(invalidFields.contains{ $0.key == .expiration }) + XCTAssertTrue(invalidFields.contains{ $0.key == .cvv }) + XCTAssertTrue(invalidFields.contains{ $0.key == .postalCode }) + } + + func testGetErrorFields_notIgnoreUneditedFields_hasNoErrorFields_returnsNoFields() { + cardState.fieldStates.forEach { + $0.value.isValid = true + } + + cardState.cardNumber.wasEdited = true + cardState.cardNumber.wasFirstResponder = true + + cardState.expiration.wasEdited = true + cardState.expiration.wasFirstResponder = false + + cardState.cvv.wasEdited = false + cardState.cvv.wasFirstResponder = true + + cardState.postalCode.wasEdited = false + cardState.postalCode.wasFirstResponder = false + + let invalidFields = cardState.getErrorFields(ignoreUneditedFields: false) + + XCTAssertEqual(0, invalidFields.count) + } + + private func customErrorMessageHandler(_ cardState: NSDictionary, _ cardBrand: OPCardBrand, _ ignoreUneditedFields: Bool) -> String { + return "Custom Error" + } + + fileprivate class MockValidStateChangedDelegate: NSObject, OPValidStateChangedDelegate { + var validStateChangedCalled: Bool = false + var validStateChangedParameter: Bool? = nil + + func validStateChanged(isValid: Bool) { + validStateChangedCalled = true + validStateChangedParameter = isValid + } + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/CvvStateTests.swift b/src/OloPaySDK/OloPaySDKTests/CvvStateTests.swift new file mode 100644 index 0000000..7df90cb --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/CvvStateTests.swift @@ -0,0 +1,278 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CvvStateTests.swift +// OloPaySDKTests +// +// Created by Justin Anderson on 8/21/23. +// + +import XCTest +@testable import OloPaySDK + +final class CvvStateTests: XCTestCase { + lazy var _cvvState: OPCvvState? = nil + var cvvState: OPCvvState { + get { _cvvState! } + } + + override func setUpWithError() throws { + _cvvState = OPCvvState() + } + + override func tearDownWithError() throws { + OPCvvState.errorMessageHandler = nil + } + + func testConstructor_validateInitialState() { + XCTAssertFalse(cvvState.isValid) + XCTAssertFalse(cvvState._fieldState.isValid) + XCTAssertTrue(cvvState._fieldState.isEmpty) + XCTAssertFalse(cvvState._fieldState.wasEdited) + XCTAssertFalse(cvvState._fieldState.isFirstResponder) + XCTAssertFalse(cvvState._fieldState.wasFirstResponder) + } + + func testOnInputChanged_withEmptyText_stateIsEmpty_stateNotEdited() { + cvvState.onInputChanged("") + + XCTAssertTrue(cvvState._fieldState.isEmpty) + XCTAssertFalse(cvvState._fieldState.wasEdited) + } + + func testOnInputChangd_withNonEmptyText_stateIsNotEmpty_stateWasEdited() { + cvvState.onInputChanged("2") + + XCTAssertFalse(cvvState._fieldState.isEmpty) + XCTAssertTrue(cvvState._fieldState.wasEdited) + } + + func testOnInputChanged_withTooFewDigits_stateNotValid() { + cvvState.onInputChanged("23") + XCTAssertFalse(cvvState.isValid) + } + + func testOnInputChanged_withTooManyDigits_stateNotValid() { + cvvState.onInputChanged("23456") + XCTAssertFalse(cvvState.isValid) + } + + func testOnInputChanged_withCharacters_stateNotValid() { + cvvState.onInputChanged("2a3") + XCTAssertFalse(cvvState.isValid) + } + + func testOnInputChanged_withThreeDigits_stateValid() { + cvvState.onInputChanged("123") + XCTAssertTrue(cvvState.isValid) + } + + func testOnInputChanged_withFourDigits_stateValid() { + cvvState.onInputChanged("1234") + XCTAssertTrue(cvvState.isValid) + } + + func testOnInputChanged_isValidToggledTrue_delegateCalled() { + let cvvDelegate = MockValidStateChangedDelegate() + cvvState.delegate = cvvDelegate + + XCTAssertFalse(cvvState.isValid) + cvvState.onInputChanged("123") + + XCTAssertTrue(cvvState.isValid) + XCTAssertTrue(cvvDelegate.validStateChangedCalled) + XCTAssertNotNil(cvvDelegate.validStateChangedParameter) + XCTAssertTrue(cvvDelegate.validStateChangedParameter!) + } + + func testOnInputChanged_isValidToggledFalse_delegateCalled() { + let cvvDelegate = MockValidStateChangedDelegate() + cvvState.onInputChanged("123") + + cvvState.delegate = cvvDelegate + + XCTAssertTrue(cvvState.isValid) + cvvState.onInputChanged("12") + + XCTAssertFalse(cvvState.isValid) + XCTAssertTrue(cvvDelegate.validStateChangedCalled) + XCTAssertNotNil(cvvDelegate.validStateChangedParameter) + XCTAssertFalse(cvvDelegate.validStateChangedParameter!) + } + + func testOnInputChanged_isValidNotToggled_delegateNotCalled() { + let cvvDelegate = MockValidStateChangedDelegate() + cvvState.onInputChanged("123") + + cvvState.delegate = cvvDelegate + + XCTAssertTrue(cvvState.isValid) + cvvState.onInputChanged("345") + + XCTAssertTrue(cvvState.isValid) + XCTAssertFalse(cvvDelegate.validStateChangedCalled) + XCTAssertNil(cvvDelegate.validStateChangedParameter) + } + + func testReset_isValidToggledFalse_delegateCalled() { + let cvvDelegate = MockValidStateChangedDelegate() + cvvState.onInputChanged("123") + + cvvState.delegate = cvvDelegate + + XCTAssertTrue(cvvState.isValid) + cvvState.reset() + + XCTAssertFalse(cvvState.isValid) + XCTAssertTrue(cvvDelegate.validStateChangedCalled) + XCTAssertNotNil(cvvDelegate.validStateChangedParameter) + XCTAssertFalse(cvvDelegate.validStateChangedParameter!) + } + + func testReset_isValidNotToggled_delegateNotCalled() { + let cvvDelegate = MockValidStateChangedDelegate() + cvvState.onInputChanged("12") + + cvvState.delegate = cvvDelegate + + XCTAssertFalse(cvvState.isValid) + cvvState.reset() + + XCTAssertFalse(cvvState.isValid) + XCTAssertFalse(cvvDelegate.validStateChangedCalled) + XCTAssertNil(cvvDelegate.validStateChangedParameter) + } + + func testOnFirstResponderStateChanged_gainsFirstResponderState_stateIsFirstResponder_notStateWasFirstResponder() { + cvvState.onFirstResponderStateChanged(true) + XCTAssertTrue(cvvState._fieldState.isFirstResponder) + XCTAssertFalse(cvvState._fieldState.wasFirstResponder) + } + + func testOnFirstResponderStateChanged_leavesFirstResponderState_stateWasEdited_notStateIsFirstResponder_stateWasFirstResponder() { + cvvState.onFirstResponderStateChanged(true) + cvvState._fieldState.wasEdited = true + cvvState.onFirstResponderStateChanged(false) + XCTAssertFalse(cvvState._fieldState.isFirstResponder) + XCTAssertTrue(cvvState._fieldState.wasFirstResponder) + } + + func testOnFirstResponderStateChanged_leavesFirstResponderState_stateNotWasEdited_notStateIsFirstResponder_stateNotWasFirstResponder() { + cvvState.onFirstResponderStateChanged(true) + cvvState._fieldState.wasEdited = false + cvvState.onFirstResponderStateChanged(false) + XCTAssertFalse(cvvState._fieldState.isFirstResponder) + XCTAssertFalse(cvvState._fieldState.wasFirstResponder) + } + + func testHasErrorMessage_withoutIgnoreUneditedErrors_stateNotValid_returnsFalse() { + cvvState._fieldState.isValid = true + XCTAssertFalse(cvvState.hasErrorMessage(false)) + } + + func testHasErrorMessage_withoutIgnoreUneditedErrors_stateNotValid_returnsTrue() { + cvvState._fieldState.isValid = false + XCTAssertTrue(cvvState.hasErrorMessage(false)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateValid_stateEdited_stateWasResponder_returnsFalse() { + cvvState._fieldState.isValid = true + cvvState._fieldState.wasEdited = true + cvvState._fieldState.wasFirstResponder = true + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateValid_stateEdited_stateNotWasResponder_returnsFalse() { + cvvState._fieldState.isValid = true + cvvState._fieldState.wasEdited = true + cvvState._fieldState.wasFirstResponder = false + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateValid_stateNotEdited_stateWasResponder_returnsFalse() { + cvvState._fieldState.isValid = true + cvvState._fieldState.wasEdited = false + cvvState._fieldState.wasFirstResponder = true + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateValid_stateNotEdited_stateNotWasResponder_returnsFalse() { + cvvState._fieldState.isValid = true + cvvState._fieldState.wasEdited = false + cvvState._fieldState.wasFirstResponder = false + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateNotValid_stateEdited_stateWasResponder_returnsTrue() { + cvvState._fieldState.isValid = false + cvvState._fieldState.wasEdited = true + cvvState._fieldState.wasFirstResponder = true + XCTAssertTrue(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateNotValid_stateEdited_stateNotWasResponder_returnsFalse() { + cvvState._fieldState.isValid = false + cvvState._fieldState.wasEdited = true + cvvState._fieldState.wasFirstResponder = false + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + + func testHasErrorMessage_withIgnoreUneditedErrors_stateNotValid_stateNotEdited_stateWasResponder_returnsFalse() { + cvvState._fieldState.isValid = false + cvvState._fieldState.wasEdited = false + cvvState._fieldState.wasFirstResponder = true + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testHasErrorMessage_withIgnoreUneditedErrors_stateNotValid_stateNotEdited_stateNotWasResponder_returnsFalse() { + cvvState._fieldState.isValid = false + cvvState._fieldState.wasEdited = false + cvvState._fieldState.wasFirstResponder = false + XCTAssertFalse(cvvState.hasErrorMessage(true)) + } + + func testGetErrorMessage_stateIsValid_returnsEmptyString() { + cvvState._fieldState.isValid = true + XCTAssertEqual("", cvvState.getErrorMessage()) + } + + func testGetErrorMessage_stateInvalid_stateEmpty_returnsEmptyCvvError() { + cvvState._fieldState.isValid = false + cvvState._fieldState.isEmpty = true + XCTAssertEqual(OPStrings.emptyCvvError, cvvState.getErrorMessage(false)) + } + + func testGetErrorMessage_stateInvalid_stateNotEmpty_returnsIncompleteCvvError() { + cvvState._fieldState.isValid = false + cvvState._fieldState.isEmpty = false + XCTAssertEqual(OPStrings.incompleteCvvError, cvvState.getErrorMessage(false)) + } + + func testGetErrorMessage_withCustomErrorHandler_stateInvalid_returnsCustomError() { + OPCvvState.errorMessageHandler = customErrorMessageHandler(_:_:) + cvvState._fieldState.isValid = false + + XCTAssertEqual("Custom Error", cvvState.getErrorMessage(false)) + } + + func testEditingCompleted() { + cvvState.editingCompleted() + XCTAssertTrue(cvvState._fieldState.wasEdited) + XCTAssertTrue(cvvState._fieldState.wasFirstResponder) + } + + private func customErrorMessageHandler(_ state: OPCardFieldStateProtocol, _ ignoreUneditedFieldErrors: Bool) -> String { + return "Custom Error" + } + + fileprivate class MockValidStateChangedDelegate: NSObject, OPValidStateChangedDelegate { + var validStateChangedCalled: Bool = false + var validStateChangedParameter: Bool? = nil + + func validStateChanged(isValid: Bool) { + validStateChangedCalled = true + validStateChangedParameter = isValid + } + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/ErrorTypeTests.swift b/src/OloPaySDK/OloPaySDKTests/ErrorTypeTests.swift new file mode 100644 index 0000000..e69e098 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/ErrorTypeTests.swift @@ -0,0 +1,31 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ErrorTypeTests.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/14/21. +// + +import XCTest +import Stripe +@testable import OloPaySDK + +class ErrorTypeTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testConvertFrom_StripeErrorType_To_OloPayErrorType() throws { + let cases = [(STPErrorCode.connectionError, OPErrorType.connectionError), (STPErrorCode.invalidRequestError, OPErrorType.invalidRequestError), (STPErrorCode.apiError, OPErrorType.apiError), (STPErrorCode.cardError, OPErrorType.cardError), (STPErrorCode.cancellationError, OPErrorType.cancellationError), (STPErrorCode.ephemeralKeyDecodingError, OPErrorType.generalError)] + cases.forEach { + XCTAssertEqual(OPErrorType.convert(from: $0), $1) + } + } + +} diff --git a/src/OloPaySDK/OloPaySDKTests/Info.plist b/src/OloPaySDK/OloPaySDKTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/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/src/OloPaySDK/OloPaySDKTests/MetadataGeneratorTests.swift b/src/OloPaySDK/OloPaySDKTests/MetadataGeneratorTests.swift new file mode 100644 index 0000000..7b09c3c --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/MetadataGeneratorTests.swift @@ -0,0 +1,183 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// MetadataGeneratorTests.swift +// OloPaySDKTests +// +// Created by Richard Dowdy on 5/30/23. +// + +import XCTest +@testable import OloPaySDK + +final class MetadataGeneratorTests: XCTestCase { + + let _applePayCompanyLabel = "Test Company" + let _applePayMerchantId = "com.merchant.test" + + override func setUpWithError() throws { + OloPayAPI.sdkWrapperInfo = nil + } + + func testMetadataGenerator_withNativeSingleLineInputSource_containsCreationSourceData() { + let metadata = getMetadata(.singleLineInput) + + XCTAssertEqual("singleLineInput", metadata["CreationSource"]) + assertNoHybridData(metadata) + assertNoApplePayData(metadata) + } + + func testMetadataGenerator_withNativeFormLineInputSource_containsCreationSourceData() { + let metadata = getMetadata(.formInput) + + XCTAssertEqual("formInput", metadata["CreationSource"]) + assertNoHybridData(metadata) + assertNoApplePayData(metadata) + } + + func testMetadataGenerator_withNativeApplePaySource_containsCreationSourceData() { + let metadata = getMetadata(.applePay) + + XCTAssertEqual("applePay", metadata["CreationSource"]) + assertApplePayData(metadata) + assertNoHybridData(metadata) + } + + func testMetadataGenerator_withNativeApplePaySource_containsApplePayData() { + let metadata = getMetadata(.applePay) + assertApplePayData(metadata) + } + + func testMetadataGenerator_withHybridSingleLineSource_containsHybridData() { + OloPayAPI.sdkWrapperInfo = OPSdkWrapperInfo( + withMajorVersion: 1, + withMinorVersion: 2, + withBuildVersion: 3, + withSdkBuildType: .internalBuild, + withSdkPlatform: .reactNative + ) + + let metadata = getMetadata(.singleLineInput) + assertHybridSdkKeysExist(metadata) + assertNoApplePayData(metadata) + + XCTAssertEqual("1.2.3", metadata["HybridVersion"]) + XCTAssertEqual("internal", metadata["HybridBuildType"]) + XCTAssertEqual("reactNative", metadata["HybridPlatform"]) + } + + func testMetadataGenerator_withHybridFormSource_containsHybridData() { + OloPayAPI.sdkWrapperInfo = OPSdkWrapperInfo( + withMajorVersion: 2, + withMinorVersion: 3, + withBuildVersion: 4, + withSdkBuildType: .publicBuild, + withSdkPlatform: .capacitor + ) + + let metadata = getMetadata(.formInput) + assertHybridSdkKeysExist(metadata) + assertNoApplePayData(metadata) + + XCTAssertEqual("2.3.4", metadata["HybridVersion"]) + XCTAssertEqual("public", metadata["HybridBuildType"]) + XCTAssertEqual("capacitor", metadata["HybridPlatform"]) + } + + func testMetadataGenerator_withHybridApplePaySource_containsHybridData() { + OloPayAPI.sdkWrapperInfo = OPSdkWrapperInfo( + withMajorVersion: 1, + withMinorVersion: 3, + withBuildVersion: 5, + withSdkBuildType: .internalBuild, + withSdkPlatform: .flutter + ) + + let metadata = getMetadata(.applePay) + assertHybridSdkKeysExist(metadata) + assertApplePayData(metadata) + + XCTAssertEqual("1.3.5", metadata["HybridVersion"]) + XCTAssertEqual("internal", metadata["HybridBuildType"]) + XCTAssertEqual("flutter", metadata["HybridPlatform"]) + } + + func testMetadataGenerator_withNativeApplePaySource_withoutApplePayConstructor_containsNoApplePayValues() { + let metadata = OPMetadataGenerator(.applePay).generate() + assertApplePayKeysExist(metadata) + + XCTAssertEqual("", metadata["ApplePayMerchantId"]) + XCTAssertEqual("", metadata["DigitalWalletCompanyLabel"]) + } + + func testMetadataGenerator_withTestEnvironment_containsValidEnvironmentData() { + OloPayAPI.environment = .test + XCTAssertEqual("test", getMetadata(.singleLineInput)["Environment"]) + } + + func testMetadataGenerator_withProductionEnvironment_containsValidEnvironmentData() { + OloPayAPI.environment = .production + XCTAssertEqual("production", getMetadata(.singleLineInput)["Environment"]) + } + + func getMetadata (_ source: OPPaymentMethodSource) -> [String : String] { + + let generator = source != .applePay ? + OPMetadataGenerator(source) : + OPMetadataGenerator(applePayMerchantId: _applePayMerchantId, applePayCompanyLabel: _applePayCompanyLabel) + + let metadata = generator.generate() + + assertStaticMetadata(metadata) + + return metadata + } + + func assertStaticMetadata(_ metadata: [String : String]) { + XCTAssertTrue(metadata.keys.contains("CreationSource")) + XCTAssertTrue(metadata.keys.contains("BuildType")) + XCTAssertTrue(metadata.keys.contains("Version")) + XCTAssertTrue(metadata.keys.contains("Platform")) + XCTAssertTrue(metadata.keys.contains("OSVersion")) + XCTAssertTrue(metadata.keys.contains("Environment")) + + + guard let buildType = OPSdkBuildType.convert(from: OPSdkBuild.buildType) else { + XCTFail("Expected 'buildType' to not be nil") + return + } + XCTAssertEqual(buildType.description, metadata["BuildType"]?.lowercased()) + XCTAssertEqual(OPSdkVersion.version, metadata["Version"]) + XCTAssertEqual("ios", metadata["Platform"]) + XCTAssertEqual(UIDevice.current.systemVersion, metadata["OSVersion"]) + } + + func assertHybridSdkKeysExist(_ metadata: [String : String]) { + XCTAssertTrue(metadata.keys.contains("HybridBuildType")) + XCTAssertTrue(metadata.keys.contains("HybridPlatform")) + XCTAssertTrue(metadata.keys.contains("HybridVersion")) + } + + func assertNoApplePayData(_ metadata: [String : String]) { + XCTAssertFalse(metadata.keys.contains("DigitalWalletCompanyLabel")) + XCTAssertFalse(metadata.keys.contains("ApplePayEnvironment")) + } + + func assertApplePayKeysExist(_ metadata: [String : String]) { + XCTAssertTrue(metadata.keys.contains("ApplePayMerchantId")) + XCTAssertTrue(metadata.keys.contains("DigitalWalletCompanyLabel")) + } + + func assertApplePayData(_ metadata: [String : String]) { + assertApplePayKeysExist(metadata) + + XCTAssertEqual("com.merchant.test", metadata["ApplePayMerchantId"]) + XCTAssertEqual("Test Company", metadata["DigitalWalletCompanyLabel"]) + } + + func assertNoHybridData(_ metadata: [String : String]) { + XCTAssertFalse(metadata.keys.contains("HybridBuildType")) + XCTAssertFalse(metadata.keys.contains("HybridPlatform")) + XCTAssertFalse(metadata.keys.contains("HybridVersion")) + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/OPStorageTests.swift b/src/OloPaySDK/OloPaySDKTests/OPStorageTests.swift new file mode 100644 index 0000000..d64b222 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/OPStorageTests.swift @@ -0,0 +1,65 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OPStorageTests.swift +// OloPaySDKTests +// +// Created by Richard Dowdy on 3/30/23. +// + +import XCTest +@testable import OloPaySDK + +class OPStorageTests: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + func testgetPublishablekey_withTestEnvironment_returnsTestKey() { + OPStorage.testPublishableKey = "Test" + + XCTAssertEqual("Test", OPStorage.getPublishableKey(environment: OPEnvironment.test)) + } + + + func testsetPublishableKey_withTestEnvironment_setsOnlyTestKey() { + OPStorage.testPublishableKey = "" + OPStorage.productionPublishableKey = "" + + OPStorage.setPublishableKey(environment: OPEnvironment.test, value: "Test") + + XCTAssertEqual("Test", OPStorage.testPublishableKey) + XCTAssertTrue(OPStorage.productionPublishableKey.isEmpty) + } + + func testgetPublishableKey_withProductionEnvironment_returnsProductionKey() { + OPStorage.productionPublishableKey = "Production" + + XCTAssertEqual("Production", OPStorage.getPublishableKey(environment: OPEnvironment.production)) + } + + func testsetPublishableKey_withProductionEnvironment_setsOnlyProductionKey() { + OPStorage.testPublishableKey = "" + OPStorage.productionPublishableKey = "" + + OPStorage.setPublishableKey(environment: OPEnvironment.production, value: "Production") + + XCTAssertEqual("Production", OPStorage.productionPublishableKey) + XCTAssertTrue(OPStorage.testPublishableKey.isEmpty) + } + + func testReset_valuesReturnedToDefaults() { + OPStorage.testPublishableKey = "Foo" + OPStorage.productionPublishableKey = "Bar" + OPStorage.environment = OPEnvironment.test.description + + OPStorage.reset() + + XCTAssertEqual("", OPStorage.getPublishableKey(environment: .test)) + XCTAssertEqual("", OPStorage.getPublishableKey(environment: .production)) + XCTAssertEqual("production", OPStorage.environment) + } +} diff --git a/src/OloPaySDK/OloPaySDKTests/OloPayAPITests.swift b/src/OloPaySDK/OloPaySDKTests/OloPayAPITests.swift new file mode 100644 index 0000000..ffe8682 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/OloPayAPITests.swift @@ -0,0 +1,599 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPayAPITests.swift +// OloPaySDKTests +// +// Created by Justin Anderson on 11/18/21. +// + +import XCTest +import Stripe +@testable import OloPaySDK + +class OloPayAPITests: XCTestCase { + lazy var initializer: OloPayApiInitializer? = nil; + let maxWaitSeconds: Double = 5 + + override func setUpWithError() throws { + initializer = OloPayApiInitializer(); + } + + override func tearDownWithError() throws { + } + + func testCreatePaymentMethod_apiInitialized_invalidPublishableKey_updatesKey() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI.publishableKey = "foobar" //Reset the publishable key to an invalid one + + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createValid()) { paymentMethod, error in + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertNotEqual("foobar", OloPayAPI.publishableKey) //Make sure the publishable key it NOT what we set it to after initializing the API + } + + func testCreatePaymentMethod_incorrectPaymentParamsType_throwsUnknownCardError() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + OloPayAPI().createPaymentMethod(with: InvalidPaymentMethodParams()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.unknownCardError, cardError.cardErrorType) + XCTAssertEqual(OPStrings.generalCardError, cardError.cardErrorMessage) + XCTAssertNil(paymentMethod) + + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsValid_returnsValidPaymentMethod() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createValid()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard error == nil else { + XCTFail("Error incorrectly thrown") + return + } + + guard let paymentMethod = paymentMethod as? OPPaymentMethod else { + XCTFail("Payment method not created") + return + } + + XCTAssertEqual("4242", paymentMethod.last4) + XCTAssertEqual(PaymentMethodParamsHelper.validExpYear, UInt(truncating: paymentMethod.expirationYear!)) + XCTAssertEqual(PaymentMethodParamsHelper.validExpMonth, UInt(truncating: paymentMethod.expirationMonth!)) + XCTAssertEqual(PaymentMethodParamsHelper.validPostalCode, paymentMethod.postalCode) + XCTAssertFalse(paymentMethod.isApplePay) + + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsInvalidNumber_throwsException() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createWithInvalidNumber()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidNumber, cardError.cardErrorType) + XCTAssertNil(paymentMethod) + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsInvalidYear_throwsException() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createWithInvalidYear()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidExpYear, cardError.cardErrorType) + XCTAssertNil(paymentMethod) + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsInvalidMonth_throwsException() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createWithInvalidMonth()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidExpMonth, cardError.cardErrorType) + XCTAssertNil(paymentMethod) + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsInvalidCvv_throwsException() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createWithInvalidCvv()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidCvv, cardError.cardErrorType) + XCTAssertNil(paymentMethod) + } + + func testCreatePaymentMethod_apiInitialized_paymentParamsUnsupportedCardBrand_throwsException() throws { + let expectation = XCTestExpectation(description: "createPaymentMethod() completed") + + var paymentMethod: OPPaymentMethodProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createPaymentMethod(with: PaymentMethodParamsHelper.createWithUnsupportedCardBrand()) { pm, e in + paymentMethod = pm + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidNumber, cardError.cardErrorType) + XCTAssertEqual(OPStrings.unsupportedCardError, cardError.cardErrorMessage) + XCTAssertNil(paymentMethod) + } + + func testCreateCvvUpdateToken_apiInitialized_invalidPublishableKey_updatesKey() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI.publishableKey = "foobar" //Reset the publishable key to an invalid one + + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createValid()) { token, error in + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + XCTAssertNotEqual("foobar", OloPayAPI.publishableKey) //Make sure the publishable key it NOT what we set it to after initializing the API + } + + func testCreateCvvUpdateToken_incorrectTokenParamsType_throwsInvalidRequestError() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createIncorrectParamsType()) { t, e in + token = t + error = e + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let error = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.invalidRequestError, error.errorType) + XCTAssertEqual("Params must be of type OPCvvTokenParams", error.localizedDescription) + XCTAssertNil(token) + } + + func testCreateCvvUpdateToken_apiInitialized_withValidCvv_returnsValidCvvUpdateToken() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createValid()) { t, e in + token = t + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let token = token else { + XCTFail("Token not created") + return + } + + XCTAssertNotEqual("", token.id) + XCTAssertEqual(OPEnvironment.test, token.environment) + XCTAssertNil(error) + } + + func testCreateCvvUpdateToken_apiInitialized_withEmptyCvv_throwsException() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createInvalidWithEmptyCvv()) { t, e in + token = t + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidCvv, cardError.cardErrorType) + XCTAssertEqual("Your card's security code is missing", cardError.localizedDescription) + XCTAssertNil(token) + } + + func testCreateCvvUpdateToken_apiInitialized_cvvWithTooFewDigits_throwsException() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createInvalidWithTooFewDigits()) { t, e in + token = t + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidCvv, cardError.cardErrorType) + XCTAssertNil(token) + } + + func testCreateCvvUpdateToken_apiInitialized_cvvWithTooManyDigits_throwsException() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createInvalidWithTooManyDigits()) { t, e in + token = t + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidCvv, cardError.cardErrorType) + XCTAssertNil(token) + } + + func testCreateCvvUpdateToken_apiInitialized_cvvWithCharacters_throwsException() throws { + let expectation = XCTestExpectation(description: "createCvvUpdateToken() completed") + + var token: OPCvvUpdateTokenProtocol? = nil + var error: Error? = nil + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI().createCvvUpdateToken(with: CvvTokenParamsHelper.createInvalidWithCharacters()) { t, e in + token = t + error = e + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + guard let cardError = error as? OPError else { + XCTFail("Expected error of type OPError") + return + } + + XCTAssertEqual(OPErrorType.cardError, cardError.errorType) + XCTAssertEqual(OPCardErrorType.invalidCvv, cardError.cardErrorType) + XCTAssertNil(token) + } + + func testCreatePaymentRequest_merchantIdNotSet_throwsMissingMerchantIdError() throws { + let expectation = XCTestExpectation(description: "setup() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + do { + let _ = try OloPayAPI().createPaymentRequest(forAmount: 2.50) + } catch OPApplePayContextError.missingMerchantId { + return //Exception thrown + } + + XCTFail("missingMerchantId not thrown") + } + + func testCreatePaymentRequest_companyNotSet_throwsMissingCompanyLabelError() throws { + let expectation = XCTestExpectation(description: "setup() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test, withApplePayMerchantId: "com.olopay.tests")) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + do { + let _ = try OloPayAPI().createPaymentRequest(forAmount: 2.50) + } catch OPApplePayContextError.missingCompanyLabel { + return //Exception thrown + } + + XCTFail("missingCompanyLabel not thrown") + } + + func testUpdatePublishableKey_storesKey() throws { + let expectation = XCTestExpectation(description: "updatePublishableKey() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI.publishableKey = "" + + OloPayAPI.updatePublishableKey { + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + XCTAssertFalse(OloPayAPI.publishableKey.isEmpty) + } + + func testUpdatePublishableKeyForUrl_urlDoesNotReturnPublishableKey_publishableKeyNotUpdated() { + let expectation = XCTestExpectation(description: "updatePublishableKey() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI.publishableKey = "" + + let url = URL(string: "https://static.olocdn.net") + + let task = OloPayAPI.updatePublishableKey(for: url!) { + expectation.fulfill() + } + + task.resume() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + + XCTAssertTrue(OloPayAPI.publishableKey.isEmpty) + } + + func testUpdatePublishableKeyForUrl_urlDoesNotReturnData_publishableKeyNotUpdated() { + let expectation = XCTestExpectation(description: "updatePublishableKey() completed") + + initializer!.setup(with: OPSetupParameters(withEnvironment: OPEnvironment.test)) { + OloPayAPI.publishableKey = "" + + let invalidUrl = URL(string: "https://foo.bar") + + let task = OloPayAPI.updatePublishableKey(for: invalidUrl!) { + expectation.fulfill() + } + + task.resume() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertTrue(OloPayAPI.publishableKey.isEmpty) + } + + private class CvvTokenParamsHelper { + static let validCvv = "234" + static let invalidCvvEmpty = "" + static let invalidCvvWithTooFewDigits = "23" + static let invalidCvvWithTooManyDigits = "1234556" + static let invalidCvvWithCharacters = "1a2" + + static func createValid() -> OPCvvTokenParamsProtocol { + return OPCvvTokenParams(validCvv) + } + + static func createInvalidWithEmptyCvv() -> OPCvvTokenParamsProtocol { + return OPCvvTokenParams(invalidCvvEmpty) + } + + static func createInvalidWithTooFewDigits() -> OPCvvTokenParamsProtocol { + return OPCvvTokenParams(invalidCvvWithTooFewDigits) + } + + static func createInvalidWithTooManyDigits() -> OPCvvTokenParamsProtocol { + return OPCvvTokenParams(invalidCvvWithTooManyDigits) + } + + static func createInvalidWithCharacters() -> OPCvvTokenParamsProtocol { + return OPCvvTokenParams(invalidCvvWithCharacters) + } + + static func createIncorrectParamsType() -> OPCvvTokenParamsProtocol { + return InvalidCvvTokenParams() + } + } + + private class PaymentMethodParamsHelper { + static let validCardNumber = "4242424242424242" + static let invalidCardNumber = "1234567890123456" + static let diningClubCardNumber = "3056930009020004" + static let validExpYear: UInt = 2025 + static let invalidExpYear: UInt = 2020 + static let validExpMonth: UInt = 12 + static let invalidExpMonth: UInt = 24 + static let validCvv = "234" + static let invalidCvv = "12" + static let validPostalCode = "10004" + + static func createValid() -> OPPaymentMethodParamsProtocol { + return OPPaymentMethodParams(createStripePaymentMethod(), fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createWithInvalidNumber() -> OPPaymentMethodParamsProtocol { + let paymentMethod = createStripePaymentMethod() + paymentMethod.card?.number = invalidCardNumber + return OPPaymentMethodParams(paymentMethod, fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createWithInvalidYear() -> OPPaymentMethodParamsProtocol { + let paymentMethod = createStripePaymentMethod() + paymentMethod.card?.expYear = NSNumber(value: invalidExpYear) + return OPPaymentMethodParams(paymentMethod, fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createWithInvalidMonth() -> OPPaymentMethodParamsProtocol { + let paymentMethod = createStripePaymentMethod() + paymentMethod.card?.expMonth = NSNumber(value: invalidExpMonth) + return OPPaymentMethodParams(paymentMethod, fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createWithInvalidCvv() -> OPPaymentMethodParamsProtocol { + let paymentMethod = createStripePaymentMethod() + paymentMethod.card?.cvc = invalidCvv + return OPPaymentMethodParams(paymentMethod, fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createWithUnsupportedCardBrand() -> OPPaymentMethodParamsProtocol { + let paymentMethod = createStripePaymentMethod() + paymentMethod.card?.number = diningClubCardNumber + return OPPaymentMethodParams(paymentMethod, fromSource: OPPaymentMethodSource.singleLineInput) + } + + static func createIncorrectParamsType() -> OPPaymentMethodParamsProtocol { + return InvalidPaymentMethodParams() + } + + static func createStripePaymentMethod() -> STPPaymentMethodParams { + let cardParams = STPCardParams() + cardParams.number = validCardNumber + cardParams.expYear = validExpYear + cardParams.expMonth = validExpMonth + cardParams.cvc = validCvv + + let paymentMethodParams = STPPaymentMethodParams() + paymentMethodParams.card = STPPaymentMethodCardParams(cardSourceParams: cardParams) + paymentMethodParams.type = .card + + let address = STPPaymentMethodAddress() + address.postalCode = validPostalCode + + let billingDetails = STPPaymentMethodBillingDetails() + billingDetails.address = address + + paymentMethodParams.billingDetails = billingDetails + return paymentMethodParams + } + } + + private class InvalidPaymentMethodParams : NSObject, OPPaymentMethodParamsProtocol {} + private class InvalidCvvTokenParams : NSObject, OPCvvTokenParamsProtocol {} +} diff --git a/src/OloPaySDK/OloPaySDKTests/OloPayApiInitializerTests.swift b/src/OloPaySDK/OloPaySDKTests/OloPayApiInitializerTests.swift new file mode 100644 index 0000000..0ef22db --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/OloPayApiInitializerTests.swift @@ -0,0 +1,90 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPayApiInitializerTests.swift +// OloPaySDKTests +// +// Created by Justin Anderson on 11/30/21. +// + +import XCTest +@testable import OloPaySDK + +class OloPayApiInitializerTests: XCTestCase { + let maxWaitSeconds: Double = 5 + + override func setUpWithError() throws { + OPStorage.reset() + } + + override func tearDownWithError() throws { + } + + func testSetup_environmentParameterNotSpecified_setupWithProductionEnvironment() { + OloPayAPI.publishableKey = "" + let params = OPSetupParameters() + let expectation = XCTestExpectation(description: "setup() completed") + + OloPayApiInitializer().setup(with: params) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertEqual(OPEnvironment.production, OloPayAPI.environment) + } + + func testSetup_productionEnvironmentSpecified_setupWithProductionEnvironment() { + OloPayAPI.publishableKey = "" + let params = OPSetupParameters(withEnvironment: .production) + let expectation = XCTestExpectation(description: "setup() completed") + + OloPayApiInitializer().setup(with: params) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertEqual(OPEnvironment.production, OloPayAPI.environment) + } + + func testSetup_testEnvironmentSpecified_setupWithTestEnvironment() { + OloPayAPI.publishableKey = "" + let params = OPSetupParameters(withEnvironment: .test) + let expectation = XCTestExpectation(description: "setup() completed") + + OloPayApiInitializer().setup(with: params) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertEqual(OPEnvironment.test, OloPayAPI.environment) + } + + func testSetup_publishableKeyEmpty_publishableKeyUpdated() { + OloPayAPI.publishableKey = "" + let params = OPSetupParameters() + let expectation = XCTestExpectation(description: "setup() completed") + + OloPayApiInitializer().setup(with: params) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertNotEqual("", OloPayAPI.publishableKey) + } + + func testSetup_withCachedPublishableKey_publishableKeyNotChanged() { + OloPayAPI.publishableKey = "foobar" + + let params = OPSetupParameters() + let expectation = XCTestExpectation(description: "setup() completed") + + OloPayApiInitializer().setup(with: params) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: maxWaitSeconds) + XCTAssertEqual("foobar", OloPayAPI.publishableKey) + } + + +} diff --git a/src/OloPaySDK/OloPaySDKTests/OloPaySDKTests.swift b/src/OloPaySDK/OloPaySDKTests/OloPaySDKTests.swift new file mode 100644 index 0000000..480e52e --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/OloPaySDKTests.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloPaySDKTests.swift +// OloPaySDKTests +// +// Created by Justin Anderson on 11/18/21. +// + +import XCTest + +class OloPaySDKTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/src/OloPaySDK/OloPaySDKTests/PaymentStatusTests.swift b/src/OloPaySDK/OloPaySDKTests/PaymentStatusTests.swift new file mode 100644 index 0000000..3b435a0 --- /dev/null +++ b/src/OloPaySDK/OloPaySDKTests/PaymentStatusTests.swift @@ -0,0 +1,39 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// PaymentStatusTypeTests.swift +// OloPaySDKTests +// +// Created by Kyle Szklenski on 12/14/21. +// + +import XCTest +@testable import OloPaySDK +import Stripe + +class PaymentStatusTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + + func testConvertFrom_StripePaymentStatus_To_OloPayPaymentStatus() throws { + let cases = [(STPPaymentStatus.error, OPPaymentStatus.error), (STPPaymentStatus.success, OPPaymentStatus.success), (STPPaymentStatus.userCancellation, OPPaymentStatus.userCancellation)] + cases.forEach { + XCTAssertEqual(OPPaymentStatus.convert(from: $0), $1) + } + } + + func testConvertFrom_OloPayPaymentStatus_To_StripePaymentStatus() throws { + let cases = [(STPPaymentStatus.error, OPPaymentStatus.error), (STPPaymentStatus.success, OPPaymentStatus.success), (STPPaymentStatus.userCancellation, OPPaymentStatus.userCancellation)] + cases.forEach { + XCTAssertEqual(OPPaymentStatus.convert(from: $1), $0) + } + } + +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.entitlements b/src/TestHarness/iOS/OloPaySDKTestHarness.entitlements new file mode 100644 index 0000000..7405134 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.in-app-payments + + merchant.com.olopaysdktestharness + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.pbxproj b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.pbxproj new file mode 100644 index 0000000..97e7a04 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.pbxproj @@ -0,0 +1,839 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0620CFF7265C296E000C1A92 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0620CFF6265C296E000C1A92 /* AppDelegate.swift */; }; + 0620CFF9265C296E000C1A92 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0620CFF8265C296E000C1A92 /* SceneDelegate.swift */; }; + 0620D000265C296E000C1A92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0620CFFF265C296E000C1A92 /* Assets.xcassets */; }; + 0620D003265C296E000C1A92 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0620D001265C296E000C1A92 /* LaunchScreen.storyboard */; }; + 0637640626711876006F4BB5 /* exportOptions.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0637640426711521006F4BB5 /* exportOptions.plist */; }; + D041D6602ADD681B0062338C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D65F2ADD681B0062338C /* User.swift */; }; + D041D6612ADD681B0062338C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D65F2ADD681B0062338C /* User.swift */; }; + D041D6632ADD695C0062338C /* LoggedInUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6622ADD695C0062338C /* LoggedInUser.swift */; }; + D041D6642ADD695C0062338C /* LoggedInUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6622ADD695C0062338C /* LoggedInUser.swift */; }; + D041D6662ADD71EF0062338C /* PaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6652ADD71EF0062338C /* PaymentType.swift */; }; + D041D6672ADD71EF0062338C /* PaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6652ADD71EF0062338C /* PaymentType.swift */; }; + D041D6692ADD935B0062338C /* BillingAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6682ADD935B0062338C /* BillingAccount.swift */; }; + D041D66A2ADD935B0062338C /* BillingAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D6682ADD935B0062338C /* BillingAccount.swift */; }; + D041D66C2ADDB38E0062338C /* BillingAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D66B2ADDB38E0062338C /* BillingAccounts.swift */; }; + D041D66D2ADDB38E0062338C /* BillingAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D041D66B2ADDB38E0062338C /* BillingAccounts.swift */; }; + D04B130128A2CF4200A79092 /* OloPaySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */; platformFilter = ios; }; + D04B130228A2CF4200A79092 /* OloPaySDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04B130328A2CF4F00A79092 /* OloPaySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */; platformFilter = ios; }; + D04B130428A2CF4F00A79092 /* OloPaySDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04FC04A28F895750065BF1A /* Stripe.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04728F895680065BF1A /* Stripe.xcframework */; }; + D04FC04B28F895750065BF1A /* Stripe.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04728F895680065BF1A /* Stripe.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04FC04C28F895770065BF1A /* Stripe3DS2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */; }; + D04FC04D28F895770065BF1A /* Stripe3DS2.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04FC04E28F895790065BF1A /* StripeApplePay.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */; }; + D04FC04F28F895790065BF1A /* StripeApplePay.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04FC05028F8957B0065BF1A /* StripeCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04128F895540065BF1A /* StripeCore.xcframework */; }; + D04FC05128F8957B0065BF1A /* StripeCore.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04128F895540065BF1A /* StripeCore.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D04FC05228F8957E0065BF1A /* StripeUICore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03E28F895460065BF1A /* StripeUICore.xcframework */; }; + D04FC05328F8957E0065BF1A /* StripeUICore.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03E28F895460065BF1A /* StripeUICore.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0697BE92A86E0F7004D59D6 /* CvvTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BE82A86E0F7004D59D6 /* CvvTokenViewController.swift */; }; + D0697BEA2A86E0F7004D59D6 /* CvvTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BE82A86E0F7004D59D6 /* CvvTokenViewController.swift */; }; + D0697BEE2A86E1B0004D59D6 /* CardInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BED2A86E1B0004D59D6 /* CardInputViewController.swift */; }; + D0697BEF2A86E1B0004D59D6 /* CardInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BED2A86E1B0004D59D6 /* CardInputViewController.swift */; }; + D0697BF12A86E21C004D59D6 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF02A86E21C004D59D6 /* MainViewController.swift */; }; + D0697BF22A86E21C004D59D6 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF02A86E21C004D59D6 /* MainViewController.swift */; }; + D0697BF52A86E26E004D59D6 /* ApplePayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF42A86E26E004D59D6 /* ApplePayViewController.swift */; }; + D0697BF62A86E26E004D59D6 /* ApplePayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF42A86E26E004D59D6 /* ApplePayViewController.swift */; }; + D0697BF92A8752EC004D59D6 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF82A8752EC004D59D6 /* LogViewController.swift */; }; + D0697BFA2A8752EC004D59D6 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BF82A8752EC004D59D6 /* LogViewController.swift */; }; + D0697BFF2A8B4E79004D59D6 /* CvvTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BFE2A8B4E79004D59D6 /* CvvTokenViewModel.swift */; }; + D0697C002A8B4E79004D59D6 /* CvvTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697BFE2A8B4E79004D59D6 /* CvvTokenViewModel.swift */; }; + D0697C032A8B5454004D59D6 /* LogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C022A8B5454004D59D6 /* LogViewModel.swift */; }; + D0697C042A8B5454004D59D6 /* LogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C022A8B5454004D59D6 /* LogViewModel.swift */; }; + D0697C062A8B589D004D59D6 /* CardInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C052A8B589D004D59D6 /* CardInputViewModel.swift */; }; + D0697C072A8B589D004D59D6 /* CardInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C052A8B589D004D59D6 /* CardInputViewModel.swift */; }; + D0697C092A8B5ACB004D59D6 /* ApplePayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C082A8B5ACB004D59D6 /* ApplePayViewModel.swift */; }; + D0697C0A2A8B5ACB004D59D6 /* ApplePayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0697C082A8B5ACB004D59D6 /* ApplePayViewModel.swift */; }; + D073212626995F41007353E5 /* HttpMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073212526995F41007353E5 /* HttpMethod.swift */; }; + D073212726995F41007353E5 /* HttpMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073212526995F41007353E5 /* HttpMethod.swift */; }; + D073212D26996999007353E5 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073212C26996999007353E5 /* Product.swift */; }; + D073212E26996999007353E5 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073212C26996999007353E5 /* Product.swift */; }; + D07321312699725A007353E5 /* OloApiClientExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07321302699725A007353E5 /* OloApiClientExtensions.swift */; }; + D07321322699725A007353E5 /* OloApiClientExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07321302699725A007353E5 /* OloApiClientExtensions.swift */; }; + D0732134269988F6007353E5 /* Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0732133269988F6007353E5 /* Order.swift */; }; + D0732135269988F6007353E5 /* Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0732133269988F6007353E5 /* Order.swift */; }; + D073213A269C840C007353E5 /* Dev.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D0732137269C840C007353E5 /* Dev.xcconfig */; }; + D073213B269C840C007353E5 /* Production.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D0732138269C840C007353E5 /* Production.xcconfig */; }; + D073213E269C88F2007353E5 /* ConfigUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073213D269C88F2007353E5 /* ConfigUtils.swift */; }; + D073213F269C88F2007353E5 /* ConfigUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073213D269C88F2007353E5 /* ConfigUtils.swift */; }; + D07370D926CE1DAC005B9556 /* UITestingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07370D826CE1DAC005B9556 /* UITestingIdentifiers.swift */; }; + D07370DA26CE1DAC005B9556 /* UITestingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07370D826CE1DAC005B9556 /* UITestingIdentifiers.swift */; }; + D0C256542697622700792F44 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C256532697622700792F44 /* SettingsViewController.swift */; }; + D0C256552697622700792F44 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C256532697622700792F44 /* SettingsViewController.swift */; }; + D0C2565C26978C9400792F44 /* TestHarnessSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2565B26978C9400792F44 /* TestHarnessSettings.swift */; }; + D0C2565D26978C9400792F44 /* TestHarnessSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2565B26978C9400792F44 /* TestHarnessSettings.swift */; }; + D0C2566726980BA200792F44 /* OloApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566626980BA200792F44 /* OloApiClient.swift */; }; + D0C2566826980BA200792F44 /* OloApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566626980BA200792F44 /* OloApiClient.swift */; }; + D0C2566C26981F3700792F44 /* Basket.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566B26981F3700792F44 /* Basket.swift */; }; + D0C2566D26981F3700792F44 /* Basket.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566B26981F3700792F44 /* Basket.swift */; }; + D0C2566F2698232300792F44 /* ThreadHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566E2698232300792F44 /* ThreadHelpers.swift */; }; + D0C256702698232300792F44 /* ThreadHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2566E2698232300792F44 /* ThreadHelpers.swift */; }; + D0D77ACF2903294E006FAFB7 /* Stripe.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04728F895680065BF1A /* Stripe.xcframework */; }; + D0D77AD02903294E006FAFB7 /* Stripe.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04728F895680065BF1A /* Stripe.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D77AD329032950006FAFB7 /* Stripe3DS2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */; }; + D0D77AD429032950006FAFB7 /* Stripe3DS2.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D77AD529032952006FAFB7 /* StripeApplePay.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */; }; + D0D77AD629032952006FAFB7 /* StripeApplePay.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D77AD729032954006FAFB7 /* StripeCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04128F895540065BF1A /* StripeCore.xcframework */; }; + D0D77AD829032954006FAFB7 /* StripeCore.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC04128F895540065BF1A /* StripeCore.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D77AD929032956006FAFB7 /* StripeUICore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03E28F895460065BF1A /* StripeUICore.xcframework */; }; + D0D77ADA29032956006FAFB7 /* StripeUICore.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D04FC03E28F895460065BF1A /* StripeUICore.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0E52A94293E52D8007FCABB /* StripePayments.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */; }; + D0E52A95293E52D8007FCABB /* StripePayments.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0E52A98293E52DA007FCABB /* StripePaymentsUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */; }; + D0E52A99293E52DA007FCABB /* StripePaymentsUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0E52A9A293E52E2007FCABB /* StripePayments.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */; }; + D0E52A9B293E52E2007FCABB /* StripePayments.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0E52A9C293E52E4007FCABB /* StripePaymentsUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */; }; + D0E52A9D293E52E4007FCABB /* StripePaymentsUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0F539F8267806E000339DC9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0620CFF6265C296E000C1A92 /* AppDelegate.swift */; }; + D0F539F9267806E000339DC9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0620CFF8265C296E000C1A92 /* SceneDelegate.swift */; }; + D0F539FD267806E000339DC9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0620D001265C296E000C1A92 /* LaunchScreen.storyboard */; }; + D0F539FE267806E000339DC9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0620CFFF265C296E000C1A92 /* Assets.xcassets */; }; + D0F53A00267806E000339DC9 /* exportOptions.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0637640426711521006F4BB5 /* exportOptions.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D0C9B99D2881B6B3000F0CB0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0C9B9972881B6B3000F0CB0 /* OloPaySDK.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 065C04A1263B3DAF002D9AF0; + remoteInfo = OloPaySDK; + }; + D0C9B9A12881B6B3000F0CB0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0C9B9972881B6B3000F0CB0 /* OloPaySDK.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D00EF3822746D9EE00BDA729; + remoteInfo = OloPaySDKTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0620D03C265C2D90000C1A92 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D04FC04F28F895790065BF1A /* StripeApplePay.xcframework in Embed Frameworks */, + D0E52A99293E52DA007FCABB /* StripePaymentsUI.xcframework in Embed Frameworks */, + D0E52A95293E52D8007FCABB /* StripePayments.xcframework in Embed Frameworks */, + D04FC05128F8957B0065BF1A /* StripeCore.xcframework in Embed Frameworks */, + D04FC04B28F895750065BF1A /* Stripe.xcframework in Embed Frameworks */, + D04FC04D28F895770065BF1A /* Stripe3DS2.xcframework in Embed Frameworks */, + D04B130228A2CF4200A79092 /* OloPaySDK.framework in Embed Frameworks */, + D04FC05328F8957E0065BF1A /* StripeUICore.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + D0F53A122678095500339DC9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D0D77AD629032952006FAFB7 /* StripeApplePay.xcframework in Embed Frameworks */, + D0E52A9D293E52E4007FCABB /* StripePaymentsUI.xcframework in Embed Frameworks */, + D0E52A9B293E52E2007FCABB /* StripePayments.xcframework in Embed Frameworks */, + D0D77AD829032954006FAFB7 /* StripeCore.xcframework in Embed Frameworks */, + D0D77AD02903294E006FAFB7 /* Stripe.xcframework in Embed Frameworks */, + D0D77AD429032950006FAFB7 /* Stripe3DS2.xcframework in Embed Frameworks */, + D04B130428A2CF4F00A79092 /* OloPaySDK.framework in Embed Frameworks */, + D0D77ADA29032956006FAFB7 /* StripeUICore.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0620CFF3265C296E000C1A92 /* OloPaySDKTestHarness.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OloPaySDKTestHarness.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0620CFF6265C296E000C1A92 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0620CFF8265C296E000C1A92 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 0620CFFF265C296E000C1A92 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0620D002265C296E000C1A92 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 0620D004265C296E000C1A92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0637640426711521006F4BB5 /* exportOptions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = exportOptions.plist; sourceTree = ""; }; + 06689A6F268133F600B7C877 /* OloPaySDKTestHarness.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OloPaySDKTestHarness.entitlements; sourceTree = ""; }; + D041D65F2ADD681B0062338C /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + D041D6622ADD695C0062338C /* LoggedInUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInUser.swift; sourceTree = ""; }; + D041D6652ADD71EF0062338C /* PaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentType.swift; sourceTree = ""; }; + D041D6682ADD935B0062338C /* BillingAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingAccount.swift; sourceTree = ""; }; + D041D66B2ADDB38E0062338C /* BillingAccounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingAccounts.swift; sourceTree = ""; }; + D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeApplePay.xcframework; path = ../../../Carthage/Build/StripeApplePay.xcframework; sourceTree = ""; }; + D04FC03E28F895460065BF1A /* StripeUICore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeUICore.xcframework; path = ../../../Carthage/Build/StripeUICore.xcframework; sourceTree = ""; }; + D04FC04128F895540065BF1A /* StripeCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripeCore.xcframework; path = ../../../Carthage/Build/StripeCore.xcframework; sourceTree = ""; }; + D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Stripe3DS2.xcframework; path = ../../../Carthage/Build/Stripe3DS2.xcframework; sourceTree = ""; }; + D04FC04728F895680065BF1A /* Stripe.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Stripe.xcframework; path = ../../../Carthage/Build/Stripe.xcframework; sourceTree = ""; }; + D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripePayments.xcframework; path = ../../../Carthage/Build/StripePayments.xcframework; sourceTree = ""; }; + D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = StripePaymentsUI.xcframework; path = ../../../Carthage/Build/StripePaymentsUI.xcframework; sourceTree = ""; }; + D0697BE82A86E0F7004D59D6 /* CvvTokenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CvvTokenViewController.swift; sourceTree = ""; }; + D0697BED2A86E1B0004D59D6 /* CardInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardInputViewController.swift; sourceTree = ""; }; + D0697BF02A86E21C004D59D6 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + D0697BF42A86E26E004D59D6 /* ApplePayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayViewController.swift; sourceTree = ""; }; + D0697BF82A8752EC004D59D6 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; }; + D0697BFE2A8B4E79004D59D6 /* CvvTokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CvvTokenViewModel.swift; sourceTree = ""; }; + D0697C022A8B5454004D59D6 /* LogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewModel.swift; sourceTree = ""; }; + D0697C052A8B589D004D59D6 /* CardInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardInputViewModel.swift; sourceTree = ""; }; + D0697C082A8B5ACB004D59D6 /* ApplePayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayViewModel.swift; sourceTree = ""; }; + D073212526995F41007353E5 /* HttpMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpMethod.swift; sourceTree = ""; }; + D073212C26996999007353E5 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; + D07321302699725A007353E5 /* OloApiClientExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloApiClientExtensions.swift; sourceTree = ""; }; + D0732133269988F6007353E5 /* Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Order.swift; sourceTree = ""; }; + D0732137269C840C007353E5 /* Dev.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Dev.xcconfig; sourceTree = ""; }; + D0732138269C840C007353E5 /* Production.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; + D073213D269C88F2007353E5 /* ConfigUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUtils.swift; sourceTree = ""; }; + D07370D826CE1DAC005B9556 /* UITestingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestingIdentifiers.swift; sourceTree = ""; }; + D0B52A37267A8BA700EECEB3 /* OloPaySDKTestHarness.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OloPaySDKTestHarness.entitlements; sourceTree = ""; }; + D0C256532697622700792F44 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + D0C2565B26978C9400792F44 /* TestHarnessSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHarnessSettings.swift; sourceTree = ""; }; + D0C2566626980BA200792F44 /* OloApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OloApiClient.swift; sourceTree = ""; }; + D0C2566B26981F3700792F44 /* Basket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basket.swift; sourceTree = ""; }; + D0C2566E2698232300792F44 /* ThreadHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadHelpers.swift; sourceTree = ""; }; + D0C9B9972881B6B3000F0CB0 /* OloPaySDK.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OloPaySDK.xcodeproj; path = ../../OloPaySDK/OloPaySDK.xcodeproj; sourceTree = ""; }; + D0F53A06267806E000339DC9 /* OloPaySDKTestHarness.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OloPaySDKTestHarness.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0620CFF0265C296E000C1A92 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D04B130128A2CF4200A79092 /* OloPaySDK.framework in Frameworks */, + D0E52A98293E52DA007FCABB /* StripePaymentsUI.xcframework in Frameworks */, + D0E52A94293E52D8007FCABB /* StripePayments.xcframework in Frameworks */, + D04FC05028F8957B0065BF1A /* StripeCore.xcframework in Frameworks */, + D04FC04E28F895790065BF1A /* StripeApplePay.xcframework in Frameworks */, + D04FC04A28F895750065BF1A /* Stripe.xcframework in Frameworks */, + D04FC04C28F895770065BF1A /* Stripe3DS2.xcframework in Frameworks */, + D04FC05228F8957E0065BF1A /* StripeUICore.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0F539FA267806E000339DC9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D04B130328A2CF4F00A79092 /* OloPaySDK.framework in Frameworks */, + D0E52A9C293E52E4007FCABB /* StripePaymentsUI.xcframework in Frameworks */, + D0E52A9A293E52E2007FCABB /* StripePayments.xcframework in Frameworks */, + D0D77AD729032954006FAFB7 /* StripeCore.xcframework in Frameworks */, + D0D77AD529032952006FAFB7 /* StripeApplePay.xcframework in Frameworks */, + D0D77ACF2903294E006FAFB7 /* Stripe.xcframework in Frameworks */, + D0D77AD329032950006FAFB7 /* Stripe3DS2.xcframework in Frameworks */, + D0D77AD929032956006FAFB7 /* StripeUICore.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0620CFEA265C296E000C1A92 = { + isa = PBXGroup; + children = ( + D0B52A37267A8BA700EECEB3 /* OloPaySDKTestHarness.entitlements */, + 0620CFF5265C296E000C1A92 /* OloPaySDKTestHarness */, + 0620CFF4265C296E000C1A92 /* Products */, + D0A82AC92677FE4D00370561 /* Frameworks */, + ); + sourceTree = ""; + }; + 0620CFF4265C296E000C1A92 /* Products */ = { + isa = PBXGroup; + children = ( + 0620CFF3265C296E000C1A92 /* OloPaySDKTestHarness.app */, + D0F53A06267806E000339DC9 /* OloPaySDKTestHarness.app */, + ); + name = Products; + sourceTree = ""; + }; + 0620CFF5265C296E000C1A92 /* OloPaySDKTestHarness */ = { + isa = PBXGroup; + children = ( + D0697C0B2A8BD340004D59D6 /* Models */, + D0697C012A8B53D8004D59D6 /* ViewControllers */, + D0697BE72A86E0A9004D59D6 /* ViewModels */, + D0732136269C838E007353E5 /* Storage */, + D0C2566A26981F1000792F44 /* OloApi */, + 06689A6F268133F600B7C877 /* OloPaySDKTestHarness.entitlements */, + 0620CFF6265C296E000C1A92 /* AppDelegate.swift */, + 0620CFF8265C296E000C1A92 /* SceneDelegate.swift */, + 0620CFFF265C296E000C1A92 /* Assets.xcassets */, + 0620D001265C296E000C1A92 /* LaunchScreen.storyboard */, + 0620D004265C296E000C1A92 /* Info.plist */, + 0637640426711521006F4BB5 /* exportOptions.plist */, + D0C2566E2698232300792F44 /* ThreadHelpers.swift */, + D07370D826CE1DAC005B9556 /* UITestingIdentifiers.swift */, + ); + path = OloPaySDKTestHarness; + sourceTree = ""; + }; + D041D65C2ADD657A0062338C /* Entities */ = { + isa = PBXGroup; + children = ( + D0732133269988F6007353E5 /* Order.swift */, + D073212C26996999007353E5 /* Product.swift */, + D0C2566B26981F3700792F44 /* Basket.swift */, + D041D65F2ADD681B0062338C /* User.swift */, + D041D6622ADD695C0062338C /* LoggedInUser.swift */, + D041D6682ADD935B0062338C /* BillingAccount.swift */, + D041D66B2ADDB38E0062338C /* BillingAccounts.swift */, + ); + path = Entities; + sourceTree = ""; + }; + D0697BE72A86E0A9004D59D6 /* ViewModels */ = { + isa = PBXGroup; + children = ( + D0697BFE2A8B4E79004D59D6 /* CvvTokenViewModel.swift */, + D0697C022A8B5454004D59D6 /* LogViewModel.swift */, + D0697C052A8B589D004D59D6 /* CardInputViewModel.swift */, + D0697C082A8B5ACB004D59D6 /* ApplePayViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + D0697C012A8B53D8004D59D6 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + D0697BF82A8752EC004D59D6 /* LogViewController.swift */, + D0697BF02A86E21C004D59D6 /* MainViewController.swift */, + D0C256532697622700792F44 /* SettingsViewController.swift */, + D0697BF42A86E26E004D59D6 /* ApplePayViewController.swift */, + D0697BED2A86E1B0004D59D6 /* CardInputViewController.swift */, + D0697BE82A86E0F7004D59D6 /* CvvTokenViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; + D0697C0B2A8BD340004D59D6 /* Models */ = { + isa = PBXGroup; + children = ( + D0C2565B26978C9400792F44 /* TestHarnessSettings.swift */, + ); + path = Models; + sourceTree = ""; + }; + D0732136269C838E007353E5 /* Storage */ = { + isa = PBXGroup; + children = ( + D0732137269C840C007353E5 /* Dev.xcconfig */, + D0732138269C840C007353E5 /* Production.xcconfig */, + D073213D269C88F2007353E5 /* ConfigUtils.swift */, + ); + path = Storage; + sourceTree = ""; + }; + D0A82AC92677FE4D00370561 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0677C5C293AC0C200ECE164 /* StripePayments.xcframework */, + D0677C5F293AC0C200ECE164 /* StripePaymentsUI.xcframework */, + D04FC03B28F894C40065BF1A /* StripeApplePay.xcframework */, + D04FC03E28F895460065BF1A /* StripeUICore.xcframework */, + D04FC04128F895540065BF1A /* StripeCore.xcframework */, + D04FC04428F8955C0065BF1A /* Stripe3DS2.xcframework */, + D04FC04728F895680065BF1A /* Stripe.xcframework */, + D0C9B9972881B6B3000F0CB0 /* OloPaySDK.xcodeproj */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0C2566A26981F1000792F44 /* OloApi */ = { + isa = PBXGroup; + children = ( + D041D65C2ADD657A0062338C /* Entities */, + D0C2566626980BA200792F44 /* OloApiClient.swift */, + D073212526995F41007353E5 /* HttpMethod.swift */, + D07321302699725A007353E5 /* OloApiClientExtensions.swift */, + D041D6652ADD71EF0062338C /* PaymentType.swift */, + ); + path = OloApi; + sourceTree = ""; + }; + D0C9B9982881B6B3000F0CB0 /* Products */ = { + isa = PBXGroup; + children = ( + D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */, + D0C9B9A22881B6B3000F0CB0 /* OloPaySDKTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0620CFF2265C296E000C1A92 /* OloPaySDKTestHarness */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0620D007265C296E000C1A92 /* Build configuration list for PBXNativeTarget "OloPaySDKTestHarness" */; + buildPhases = ( + 0620CFEF265C296E000C1A92 /* Sources */, + 0620CFF0265C296E000C1A92 /* Frameworks */, + 0620CFF1265C296E000C1A92 /* Resources */, + 0620D03C265C2D90000C1A92 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OloPaySDKTestHarness; + productName = OloPaySDKTestHarness; + productReference = 0620CFF3265C296E000C1A92 /* OloPaySDKTestHarness.app */; + productType = "com.apple.product-type.application"; + }; + D0F539F3267806E000339DC9 /* OloPaySDKTestHarness-Dev */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0F53A03267806E000339DC9 /* Build configuration list for PBXNativeTarget "OloPaySDKTestHarness-Dev" */; + buildPhases = ( + D0F539F6267806E000339DC9 /* Sources */, + D0F539FA267806E000339DC9 /* Frameworks */, + D0F539FC267806E000339DC9 /* Resources */, + D0F53A122678095500339DC9 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OloPaySDKTestHarness-Dev"; + productName = OloPaySDKTestHarness; + productReference = D0F53A06267806E000339DC9 /* OloPaySDKTestHarness.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0620CFEB265C296E000C1A92 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1240; + TargetAttributes = { + 0620CFF2265C296E000C1A92 = { + CreatedOnToolsVersion = 12.4; + }; + }; + }; + buildConfigurationList = 0620CFEE265C296E000C1A92 /* Build configuration list for PBXProject "OloPaySDKTestHarness" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0620CFEA265C296E000C1A92; + productRefGroup = 0620CFF4265C296E000C1A92 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = D0C9B9982881B6B3000F0CB0 /* Products */; + ProjectRef = D0C9B9972881B6B3000F0CB0 /* OloPaySDK.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 0620CFF2265C296E000C1A92 /* OloPaySDKTestHarness */, + D0F539F3267806E000339DC9 /* OloPaySDKTestHarness-Dev */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + D0C9B99E2881B6B3000F0CB0 /* OloPaySDK.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OloPaySDK.framework; + remoteRef = D0C9B99D2881B6B3000F0CB0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D0C9B9A22881B6B3000F0CB0 /* OloPaySDKTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OloPaySDKTests.xctest; + remoteRef = D0C9B9A12881B6B3000F0CB0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 0620CFF1265C296E000C1A92 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0620D003265C296E000C1A92 /* LaunchScreen.storyboard in Resources */, + 0620D000265C296E000C1A92 /* Assets.xcassets in Resources */, + 0637640626711876006F4BB5 /* exportOptions.plist in Resources */, + D073213B269C840C007353E5 /* Production.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0F539FC267806E000339DC9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0F539FD267806E000339DC9 /* LaunchScreen.storyboard in Resources */, + D0F539FE267806E000339DC9 /* Assets.xcassets in Resources */, + D073213A269C840C007353E5 /* Dev.xcconfig in Resources */, + D0F53A00267806E000339DC9 /* exportOptions.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0620CFEF265C296E000C1A92 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0C2566726980BA200792F44 /* OloApiClient.swift in Sources */, + D0697BFF2A8B4E79004D59D6 /* CvvTokenViewModel.swift in Sources */, + D0697BEE2A86E1B0004D59D6 /* CardInputViewController.swift in Sources */, + D07321312699725A007353E5 /* OloApiClientExtensions.swift in Sources */, + D041D66C2ADDB38E0062338C /* BillingAccounts.swift in Sources */, + D0C2566F2698232300792F44 /* ThreadHelpers.swift in Sources */, + D041D6632ADD695C0062338C /* LoggedInUser.swift in Sources */, + D073213E269C88F2007353E5 /* ConfigUtils.swift in Sources */, + D0697BF92A8752EC004D59D6 /* LogViewController.swift in Sources */, + D0C2566C26981F3700792F44 /* Basket.swift in Sources */, + D073212626995F41007353E5 /* HttpMethod.swift in Sources */, + D0732134269988F6007353E5 /* Order.swift in Sources */, + D0697BF12A86E21C004D59D6 /* MainViewController.swift in Sources */, + D041D6662ADD71EF0062338C /* PaymentType.swift in Sources */, + D0697C032A8B5454004D59D6 /* LogViewModel.swift in Sources */, + D0697C062A8B589D004D59D6 /* CardInputViewModel.swift in Sources */, + D041D6692ADD935B0062338C /* BillingAccount.swift in Sources */, + D0697BF52A86E26E004D59D6 /* ApplePayViewController.swift in Sources */, + D0697C092A8B5ACB004D59D6 /* ApplePayViewModel.swift in Sources */, + D0697BE92A86E0F7004D59D6 /* CvvTokenViewController.swift in Sources */, + 0620CFF7265C296E000C1A92 /* AppDelegate.swift in Sources */, + D0C2565C26978C9400792F44 /* TestHarnessSettings.swift in Sources */, + D073212D26996999007353E5 /* Product.swift in Sources */, + D07370D926CE1DAC005B9556 /* UITestingIdentifiers.swift in Sources */, + 0620CFF9265C296E000C1A92 /* SceneDelegate.swift in Sources */, + D0C256542697622700792F44 /* SettingsViewController.swift in Sources */, + D041D6602ADD681B0062338C /* User.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0F539F6267806E000339DC9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0C2566826980BA200792F44 /* OloApiClient.swift in Sources */, + D0697C002A8B4E79004D59D6 /* CvvTokenViewModel.swift in Sources */, + D0697BEF2A86E1B0004D59D6 /* CardInputViewController.swift in Sources */, + D07321322699725A007353E5 /* OloApiClientExtensions.swift in Sources */, + D041D66D2ADDB38E0062338C /* BillingAccounts.swift in Sources */, + D0C256702698232300792F44 /* ThreadHelpers.swift in Sources */, + D041D6642ADD695C0062338C /* LoggedInUser.swift in Sources */, + D073213F269C88F2007353E5 /* ConfigUtils.swift in Sources */, + D0697BFA2A8752EC004D59D6 /* LogViewController.swift in Sources */, + D0C2566D26981F3700792F44 /* Basket.swift in Sources */, + D073212726995F41007353E5 /* HttpMethod.swift in Sources */, + D0732135269988F6007353E5 /* Order.swift in Sources */, + D0697BF22A86E21C004D59D6 /* MainViewController.swift in Sources */, + D041D6672ADD71EF0062338C /* PaymentType.swift in Sources */, + D0697C042A8B5454004D59D6 /* LogViewModel.swift in Sources */, + D0697C072A8B589D004D59D6 /* CardInputViewModel.swift in Sources */, + D041D66A2ADD935B0062338C /* BillingAccount.swift in Sources */, + D0697BF62A86E26E004D59D6 /* ApplePayViewController.swift in Sources */, + D0697C0A2A8B5ACB004D59D6 /* ApplePayViewModel.swift in Sources */, + D0697BEA2A86E0F7004D59D6 /* CvvTokenViewController.swift in Sources */, + D0F539F8267806E000339DC9 /* AppDelegate.swift in Sources */, + D0C2565D26978C9400792F44 /* TestHarnessSettings.swift in Sources */, + D073212E26996999007353E5 /* Product.swift in Sources */, + D07370DA26CE1DAC005B9556 /* UITestingIdentifiers.swift in Sources */, + D0F539F9267806E000339DC9 /* SceneDelegate.swift in Sources */, + D0C256552697622700792F44 /* SettingsViewController.swift in Sources */, + D041D6612ADD681B0062338C /* User.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 0620D001265C296E000C1A92 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0620D002265C296E000C1A92 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0620D005265C296E000C1A92 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5247FXNRAV; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + 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_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0620D006265C296E000C1A92 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5247FXNRAV; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + 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_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0620D008265C296E000C1A92 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0732138269C840C007353E5 /* Production.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 5247FXNRAV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTestHarness/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTestHarness; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Development: OloPay SDK Test Harness"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Development: OloPay SDK Test Harness"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + 0620D009265C296E000C1A92 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0732138269C840C007353E5 /* Production.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 5247FXNRAV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTestHarness/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTestHarness; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "App Center: Olo Pay SDK Test Harness"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Development: OloPay SDK Test Harness"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; + D0F53A04267806E000339DC9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0732137269C840C007353E5 /* Dev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 5247FXNRAV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTestHarness/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTestHarness; + PRODUCT_NAME = OloPaySDKTestHarness; + PROVISIONING_PROFILE_SPECIFIER = "Development: OloPay SDK Test Harness"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Development: OloPay SDK Test Harness"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + D0F53A05267806E000339DC9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0732137269C840C007353E5 /* Dev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 5247FXNRAV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 5247FXNRAV; + INFOPLIST_FILE = OloPaySDKTestHarness/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.olo.OloPaySDKTestHarness; + PRODUCT_NAME = OloPaySDKTestHarness; + PROVISIONING_PROFILE_SPECIFIER = "App Center: Olo Pay SDK Test Harness"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "App Center: Olo Pay SDK Test Harness"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0620CFEE265C296E000C1A92 /* Build configuration list for PBXProject "OloPaySDKTestHarness" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0620D005265C296E000C1A92 /* Debug */, + 0620D006265C296E000C1A92 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0620D007265C296E000C1A92 /* Build configuration list for PBXNativeTarget "OloPaySDKTestHarness" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0620D008265C296E000C1A92 /* Debug */, + 0620D009265C296E000C1A92 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D0F53A03267806E000339DC9 /* Build configuration list for PBXNativeTarget "OloPaySDKTestHarness-Dev" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0F53A04267806E000339DC9 /* Debug */, + D0F53A05267806E000339DC9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0620CFEB265C296E000C1A92 /* Project object */; +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness-Dev.xcscheme b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness-Dev.xcscheme new file mode 100644 index 0000000..b315f6e --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness-Dev.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness.xcscheme b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness.xcscheme new file mode 100644 index 0000000..54f7037 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness.xcodeproj/xcshareddata/xcschemes/OloPaySDKTestHarness.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/AppDelegate.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/AppDelegate.swift new file mode 100644 index 0000000..291346e --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/AppDelegate.swift @@ -0,0 +1,38 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// AppDelegate.swift +// OloPaySDKTestHarness +// +// Created by Kyle Szklenski on 5/24/21. +// + +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 + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/AccentColor.colorset/Contents.json b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/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/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/Contents.json b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/Contents.json b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/Contents.json new file mode 100644 index 0000000..eb471c6 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "OloPayLogo.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/OloPayLogo.png b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/OloPayLogo.png new file mode 100644 index 0000000..34e0c1f Binary files /dev/null and b/src/TestHarness/iOS/OloPaySDKTestHarness/Assets.xcassets/OloPayLogo.imageset/OloPayLogo.png differ diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Base.lproj/LaunchScreen.storyboard b/src/TestHarness/iOS/OloPaySDKTestHarness/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Info.plist b/src/TestHarness/iOS/OloPaySDKTestHarness/Info.plist new file mode 100644 index 0000000..44b3404 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Info.plist @@ -0,0 +1,90 @@ + + + + + API Key + $(API_KEY) + Apple Pay Billing Scheme Id + $(APPLE_PAY_BILLING_SCHEME_ID) + Base API Url + $(BASE_API_URL) + 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 + Company Label + $(COMPANY_LABEL) + Complete Olo Pay Payment + $(COMPLETE_OLO_PAY_PAYMENT) + LSRequiresIPhoneOS + + Merchant ID + $(MERCHANT_ID) + Product Id + $(PRODUCT_ID) + Product Qty + $(PRODUCT_QTY) + Production Environment + $(PRODUCTION_ENVIRONMENT) + Restaurant Id + $(RESTAURANT_ID) + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + User Email + $(USER_EMAIL) + Use Logged In User + $(USE_LOGGED_IN_USER) + Saved Card Billing Account Id + $(SAVED_CARD_BILLING_ACCOUNT_ID) + User Password + $(USER_PASSWORD) + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Models/TestHarnessSettings.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/Models/TestHarnessSettings.swift new file mode 100644 index 0000000..38758a2 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Models/TestHarnessSettings.swift @@ -0,0 +1,116 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// TestHarnessSettings.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/8/21. +// + +import Foundation + +protocol TestHarnessSettingsObserver: AnyObject { + func settingsChanged(settings: TestHarnessSettingsProtocol) +} + +protocol TestHarnessSettingsProtocol: NSObjectProtocol { + // Settings not controlled by Plist Values + var logCardInputChanges: Bool { get } + var displayCardErrors: Bool { get } + var customCardErrorMessages: Bool { get } + var displayPostalCode: Bool { get } + var useSingleLinePayment: Bool { get } + var logFormValidChanges: Bool { get } + var displayCvvErrors: Bool { get } + var logCvvInputChanges: Bool { get } + var customCvvErrorMessages: Bool { get } + + // API Settings + var completeOloPayPayment: Bool { get } + var baseAPIUrl: String? { get } + var apiKey: String? { get } + var restaurantId: UInt64? { get } + var productId: UInt64? { get } + var productQty: UInt? { get } + var companyLabel: String? { get } + var merchantId: String? { get } + var productionEnvironment: Bool { get } + var applePayBillingSchemeId: String? { get } + + /// User settings + var useLoggedInUser: Bool { get } + var userEmail: String? { get } + var userPassword: String? { get } + var savedCardBillingAccountId: String? { get } + +} + +// Class for managing settings for testing various SDK features +// NOTE: These settings are NOT persisted between launches of the app +class TestHarnessSettings: NSObject, TestHarnessSettingsProtocol { + private var _observations = [ObjectIdentifier : Observation]() + + public var logCardInputChanges: Bool = false + public var displayCardErrors: Bool = true + public var customCardErrorMessages: Bool = false + public var displayPostalCode: Bool = true + public var useSingleLinePayment: Bool = true + public var logFormValidChanges: Bool = false + public var displayCvvErrors: Bool = true + public var logCvvInputChanges: Bool = false + public var customCvvErrorMessages: Bool = false + + public var completeOloPayPayment: Bool = ConfigUtils.getBoolPListValue(of: "Complete Olo Pay Payment") ?? false + + public var baseAPIUrl: String? = ConfigUtils.getStringPListValue(of: "Base API Url")?.replacingOccurrences(of: "\\", with: "") + public var apiKey: String? = ConfigUtils.getStringPListValue(of: "API Key") + public var restaurantId: UInt64? = ConfigUtils.getUInt64PListValue(of: "Restaurant Id") + public var productId: UInt64? = ConfigUtils.getUInt64PListValue(of: "Product Id") + public var productQty: UInt? = ConfigUtils.getUIntPListValue(of: "Product Qty") + + public var companyLabel: String? = ConfigUtils.getStringPListValue(of: "Company Label") + public var merchantId: String? = ConfigUtils.getStringPListValue(of: "Merchant ID") + public var productionEnvironment: Bool = ConfigUtils.getBoolPListValue(of: "Production Environment") ?? true + + public var applePayBillingSchemeId: String? = ConfigUtils.getStringPListValue(of: "Apple Pay Billing Scheme Id") + + public var useLoggedInUser: Bool = ConfigUtils.getBoolPListValue(of: "Use Logged In User") ?? false + public var userEmail: String? = ConfigUtils.getStringPListValue(of: "User Email") + public var userPassword: String? = ConfigUtils.getStringPListValue(of: "User Password") + public var savedCardBillingAccountId: String? = ConfigUtils.getStringPListValue(of: "Saved Card Billing Account Id") + + public var allSettings: TestHarnessSettingsProtocol { + get { return self } + } + + private override init() {} + + public func addObserver(_ observer: TestHarnessSettingsObserver) { + let id = ObjectIdentifier(observer) + _observations[id] = Observation(observer: observer) + } + + public func removeObserver(_ observer: TestHarnessSettingsObserver) { + let id = ObjectIdentifier(observer) + _observations.removeValue(forKey: id) + } + + public func notifySettingsChanged() { + for (id, observation) in _observations { + guard let observer = observation.observer else { + _observations.removeValue(forKey: id) + continue + } + + observer.settingsChanged(settings: self) + } + } + + static let sharedInstance = TestHarnessSettings() +} + +private extension TestHarnessSettings { + struct Observation { + weak var observer: TestHarnessSettingsObserver? + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Basket.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Basket.swift new file mode 100644 index 0000000..5e1abf0 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Basket.swift @@ -0,0 +1,37 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// Basket.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/9/21. +// + +import Foundation + +// This is just a bare-bones class that only stores the minimum amount of Basket-related data needed +// for the test harness +class Basket : NSObject, Decodable { + let id: String + let vendorId: String? + let mode: String? + let deliveryMode: String? + let timeWanted: String? + let total: Decimal? + let products: [Product?]? + + override var description: String { + let properties = [ + String(format: "%@: %p", NSStringFromClass(Basket.self), self), + "id = \(String(describing: id))", + "vendorId = \(String(describing: vendorId))", + "mode = \(String(describing: mode))", + "deliveryMode = \(String(describing: deliveryMode))", + "timeWanted = \(String(describing: timeWanted))", + "total = \(String(describing: total))", + "products = \(String(describing: products))" + ] + + return "<\(properties.joined(separator: "; "))>" + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccount.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccount.swift new file mode 100644 index 0000000..77a8609 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccount.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// BillingAccount.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 10/16/23. +// + +import Foundation + +class BillingAccount: NSObject, Decodable { + let accountid: Int64 + let accountidstring: String +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccounts.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccounts.swift new file mode 100644 index 0000000..d8c820e --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/BillingAccounts.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// BillingAccounts.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 10/16/23. +// + +import Foundation + +class BillingAccounts: NSObject, Decodable { + let billingaccounts: [BillingAccount]? +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/LoggedInUser.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/LoggedInUser.swift new file mode 100644 index 0000000..537fe51 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/LoggedInUser.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// LoggedInUser.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 10/16/23. +// + +import Foundation + +class LoggedInUser: NSObject, Decodable { + let token: String + let user: User +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Order.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Order.swift new file mode 100644 index 0000000..f74b8b2 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Order.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// Order.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/10/21. +// + +import Foundation + +class Order : NSObject, Decodable { + let id: String +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Product.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Product.swift new file mode 100644 index 0000000..6eb6770 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/Product.swift @@ -0,0 +1,31 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// Product.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/10/21. +// + +import Foundation + +// This is just a bare-bones class that only stores the minimum amount of Product-related data needed +// for the test harness +class Product: NSObject, Decodable { + let id: UInt64 + let productId: UInt64 + let name: String + let quantity: Int + + override var description: String { + let properties = [ + String(format: "%@: %p", NSStringFromClass(Product.self), self), + "id = \(String(describing: id))", + "productId = \(String(describing: productId))", + "name = \(String(describing: name))", + "quantity = \(String(describing: quantity))" + ] + + return "<\(properties.joined(separator: "; "))>" + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/User.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/User.swift new file mode 100644 index 0000000..b2399a5 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/Entities/User.swift @@ -0,0 +1,24 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// User.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 10/16/23. +// + +import Foundation + +class User : NSObject, Decodable { + let authtoken: String? + let emailaddress: String + let firstname: String + let lastname: String + + init(email: String, firstName: String, lastName: String) { + authtoken = nil + emailaddress = email + firstname = firstName + lastname = lastName + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/HttpMethod.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/HttpMethod.swift new file mode 100644 index 0000000..f31e72c --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/HttpMethod.swift @@ -0,0 +1,30 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// HttpMethod.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/9/21. +// + +import Foundation + +public enum HttpMethod : Int, CustomStringConvertible { + case get + case post + case put + case delete + + public var description: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .put: + return "PUT" + case .delete: + return "DELETE" + } + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClient.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClient.swift new file mode 100644 index 0000000..2094cfb --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClient.swift @@ -0,0 +1,242 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloApiClient.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/8/21. +// + +import Foundation +import UIKit +import OloPaySDK + +typealias BasketApiCompletionBlock = (_: Basket?, _: Error?, _: String?) -> Void +typealias OrderApiCompletionBlock = (_: Order?, _: Error?, _: String?) -> Void +typealias UserApiCompletionBlock = (_: User?, _: Error?, _: String?) -> Void +typealias BillingAccountApiCompletionBlock = (_: [BillingAccount]?, _: Error?, _: String?) -> Void +typealias VoidApiCompletionBlock = (_: Error?, _: String?) -> Void +private typealias DataApiCompletionBlock = (_: Data?, _: Error?) -> Void + +class OloApiClient { + private let _baseUrl: String + private let _apiKey: String + + init(baseUrl: String, apiKey: String) { + _baseUrl = baseUrl.hasSuffix("/") ? baseUrl : "\(baseUrl)/" + _apiKey = apiKey + } + + func login(email: String, password: String, completion: @escaping UserApiCompletionBlock) { + let url = createUrl(url: "users/authenticate") + let postData = [ + "login" : email, + "password" : password + ] + + makeRequest(apiUrl: url, data: postData) { data, error in + self.parseResponse(from: data, type: LoggedInUser.self) { loggedInUser, message in + completion(loggedInUser?.user, error, message) + } + } + } + + func logout(authToken: String, completion: @escaping VoidApiCompletionBlock) { + let url = createUrl(url: "users/\(authToken)") + let postData: [String : String] = [:] + + makeRequest(apiUrl: url, data: postData, httpMethod: .delete) { data, error in + self.parseResponse(from: data, type: User.self) { user, message in + completion(error, "") + } + } + } + + func createBasket(restaurantId: UInt64, completion: @escaping BasketApiCompletionBlock) { + let url = createUrl(url: "baskets/create") + let postData = [ + "vendorid" : String(describing: restaurantId), + "mode" : "orderahead" + ] + + makeRequest(apiUrl: url, data: postData) { data, error in + self.parseResponse(from: data, type: Basket.self) { basket, message in + completion(basket, error, message) + } + } + } + + func setBasketHandoffMode(to handoffMode: String, basketId: String, completion: @escaping BasketApiCompletionBlock) { + let url = createUrl(url: "baskets/\(basketId)/deliverymode") + let data = [ "deliverymode" : handoffMode ] + + makeRequest(apiUrl: url, data: data, httpMethod: .put) { data, error in + self.parseResponse(from: data, type: Basket.self) { basket, message in + completion(basket, error, message) + } + } + } + + func setBasketTimeModeAsap(basketId: String, completion: @escaping BasketApiCompletionBlock) { + let url = createUrl(url: "baskets/\(basketId)/timewanted") + + makeRequest(apiUrl: url, data: nil, httpMethod: .delete) { data, error in + self.parseResponse(from: data, type: Basket.self) { basket, message in + completion(basket, error, message) + } + } + } + + func addProductToBasket(productId: UInt64, productQuantity: UInt, basketId: String, completion: @escaping BasketApiCompletionBlock) { + let url = createUrl(url: "baskets/\(basketId)/products") + + let data = [ + "productid" : String(describing: productId), + "quantity" : String(describing: productQuantity), + "options" : "" + ] + + makeRequest(apiUrl: url, data: data) { data, error in + self.parseResponse(from: data, type: Basket.self) { basket, message in + completion(basket, error, message) + } + } + } + + func availableBillingAccounts(authToken: String, basketId: String, completion: @escaping BillingAccountApiCompletionBlock) { + let parameters = [ + "basket" : basketId + ] + + let url = createUrl(url: "users/\(authToken)/billingAccounts", parameters: parameters) + + makeRequest(apiUrl: url, data: nil, httpMethod: .get) { data, error in + self.parseResponse(from: data, type: BillingAccounts.self) { billingAccounts, message in + completion(billingAccounts?.billingaccounts, error, message) + } + } + } + + func submitBasket(with paymentType: PaymentType, user: User, basketId: String , billingId: String?, completion: @escaping OrderApiCompletionBlock) { + let url = createUrl(url: "baskets/\(basketId)/submit") + + var data = [ + "usertype" : user.authtoken == nil ? "guest" : "user", + "saveonfile" : "false", + "streetaddress" : "26 Broadway", + "city" : "NYC", + "state" : "NY", + "contactnumber" : "5555558901", + ] + + if let authToken = user.authtoken { + data["authtoken"] = authToken + } else { + data["firstname"] = user.firstname + data["lastname"] = user.lastname + data["emailaddress"] = user.emailaddress + data["guestoptin"] = "false" + } + + if let paymentMethod = paymentType.paymentMethod { + data["billingmethod"] = paymentMethod.isApplePay ? "digitalwallet" : "creditcardtoken" + data["expiryyear"] = String(describing: paymentMethod.expirationYear!) + data["expirymonth"] = String(describing: paymentMethod.expirationMonth!) + data["token"] = paymentMethod.id + data["cardtype"] = paymentMethod.cardType.description + data["cardlastfour"] = paymentMethod.last4! + data["zip"] = paymentMethod.postalCode ?? "" //NOTE: If postal code is empty the API call will fail + data["country"] = paymentMethod.country ?? "US" + + if let billingId = billingId, paymentMethod.isApplePay { + data["billingschemeid"] = billingId + } + + } else if let token = paymentType.cvvToken { + data["billingmethod"] = "billingaccount" + data["cvv"] = token.id + + if let billingId = billingId { + data["billingaccountid"] = billingId + } + } + + makeRequest(apiUrl: url, data: data) { data, error in + self.parseResponse(from: data, type: Order.self) { order, message in + completion(order, error, message) + } + } + } + + private func makeRequest(apiUrl: String, data: [String : String]?, httpMethod: HttpMethod = .post, completion: @escaping DataApiCompletionBlock) { + guard let url = URL(string: apiUrl) else { + return + } + + var request = URLRequest(url: url) + + if httpMethod == .put || httpMethod == .delete { + request.setValue(String(describing: httpMethod), forHTTPHeaderField: "X-HTTP-Method-Override") + request.httpMethod = String(describing: HttpMethod.post) + } else { + request.httpMethod = String(describing: httpMethod) + } + + if request.httpMethod == String(describing: HttpMethod.post) && data != nil { + let jsonData = try? JSONSerialization.data(withJSONObject: data!) + request.httpBody = jsonData + + request.setValue("\(String(describing: jsonData?.count))", forHTTPHeaderField: "Content-Length") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + + request.setValue("OloPaySDKTestHarness/1.0", forHTTPHeaderField: "User-Agent") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + if let deviceId = UIDevice.current.identifierForVendor?.uuidString { + request.setValue(deviceId, forHTTPHeaderField: "X-Device-Id") + } + + request.setValue("OloKey \(_apiKey)", forHTTPHeaderField: "Authorization") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + completion(data, error) + } + + task.resume() + } + + func parseResponse(from data: Data?, type: T.Type, completion: ((T?, String?) -> Void)) { + guard let data = data else { + completion(nil, "No response data to parse") + return + } + + do { + let obj = try JSONDecoder().decode(T.self, from: data) + completion(obj, nil) + } catch { + do { + let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] + completion(nil, String(describing: json)) + } catch { + completion(nil, "Unable to parse json data") + } + } + } + + private func createUrl(url: String, parameters: [String : String]? = nil) -> String { + let newUrl = url.trimmingCharacters(in: ["/"]) + + if (parameters == nil) { + return "\(_baseUrl)\(newUrl)" + } + + let queryString = createQueryString(with: parameters!) + return "\(_baseUrl)\(newUrl)?\(queryString)" + } + + private func createQueryString(with params: [String : String]) -> String { + return params.map { "\($0)=\($1)" }.joined(separator: "&") + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClientExtensions.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClientExtensions.swift new file mode 100644 index 0000000..2a854a0 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/OloApiClientExtensions.swift @@ -0,0 +1,163 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// OloApiClientExtensions.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/10/21. +// + +import Foundation +import OloPaySDK + +extension OloApiClient { + static func createFromSettings() -> OloApiClient? { + guard let apiUrl = TestHarnessSettings.sharedInstance.baseAPIUrl, let apiKey = TestHarnessSettings.sharedInstance.apiKey else { + return nil + } + + return OloApiClient(baseUrl: apiUrl, apiKey: apiKey) + } + + func submitBasketFromSettings(with paymentMethod: OPPaymentMethodProtocol, basketId: String, completion: @escaping OrderApiCompletionBlock) { + guard let email = TestHarnessSettings.sharedInstance.userEmail, !email.isEmpty else { + completion(nil, nil, "Unable to submit basket: User email not set") + return + } + + var billingId: String? = nil + if paymentMethod.isApplePay { + guard let applePayBillingId = TestHarnessSettings.sharedInstance.applePayBillingSchemeId else { + completion(nil, nil, "Unable to submit basket: Apple Pay Billing Scheme Id not set") + return + } + + billingId = applePayBillingId + } + + if !TestHarnessSettings.sharedInstance.useLoggedInUser { + submitBasket( + with: PaymentType(paymentMethod), + user: getGuestUser(), + basketId: basketId, + billingId: billingId, + completion: completion) + } else { + guard let password = TestHarnessSettings.sharedInstance.userPassword, !password.isEmpty else { + completion(nil, nil, "Unable to submit basket - User password not set") + return + } + + login(email: email, password: password) { user, error, message in + guard let user = user, let userAuthToken = user.authtoken else { + completion(nil, error, message) + return + } + + self.submitBasket( + with: PaymentType(paymentMethod), + user: user, + basketId: basketId, + billingId: billingId) { order, error, message in + + // In a real-world application you would not want to log the user out after completing an order + self.logout(authToken: userAuthToken) { error, message in + //Do nothing here... we don't need to notify about a failed logout call + } + + completion(order, error, message) + } + } + } + } + + func submitBasketFromSettings(with cvvToken: OPCvvUpdateTokenProtocol, basketId: String, completion: @escaping OrderApiCompletionBlock) { + guard let billingId = TestHarnessSettings.sharedInstance.savedCardBillingAccountId, !billingId.isEmpty else { + completion(nil, nil, "Unable to submit basket - Saved Card Billing Acount Id not set") + return + } + + guard let email = TestHarnessSettings.sharedInstance.userEmail, !email.isEmpty else { + completion(nil, nil, "Unable to submit basket - User email not set") + return + } + + guard let password = TestHarnessSettings.sharedInstance.userPassword, !password.isEmpty else { + completion(nil, nil, "Unable to submit basket - User password not set") + return + } + + login(email: email, password: password) { user, error, message in + guard let user = user, let authToken = user.authtoken else { + completion(nil, error, message) + return + } + + self.availableBillingAccounts(authToken: authToken, basketId: basketId) { billingAccounts, error, message in + guard let billingAccounts = billingAccounts else { + completion(nil, error, message) + return + } + + let validBillingAccount = !billingAccounts.filter({ $0.accountidstring == billingId}).isEmpty + guard validBillingAccount else { + completion(nil, nil, "Unable to submit basket - Saved billing account id not valid") + return + } + + self.submitBasket( + with: PaymentType(cvvToken), + user: user, + basketId: basketId, + billingId: billingId, + completion: completion) + } + } + } + + func createBasketWithProductFromSettings(completion: @escaping BasketApiCompletionBlock) { + guard let vendorId = TestHarnessSettings.sharedInstance.restaurantId else { + completion(nil, nil, "Basket Not Created - No restaurant id set") + return + } + + guard let productId = TestHarnessSettings.sharedInstance.productId else { + completion(nil, nil, "Basket Not Created - No Product Id set") + return + } + + guard let quantity = TestHarnessSettings.sharedInstance.productQty else { + completion(nil, nil, "Basket Not Created - No Product Qty set") + return + } + + createBasket(restaurantId: vendorId) { basket, error, message in + guard let basket = basket else { + completion(nil, error, message) + return + } + + self.setBasketHandoffMode(to: "pickup", basketId: basket.id) { handoffBasket, handoffError, handoffMessage in + guard let handoffBasket = handoffBasket else { + completion(nil, handoffError, handoffMessage) + return + } + + self.setBasketTimeModeAsap(basketId: handoffBasket.id) { asapBasket, asapError, asapMessage in + guard let asapBasket = asapBasket else { + completion(nil, asapError, asapMessage) + return + } + + self.addProductToBasket(productId: productId, productQuantity: quantity, basketId: asapBasket.id) { productBasket, productError, productMessage in + completion(productBasket, productError, productMessage) + } + } + } + } + } + + private func getGuestUser() -> User { + return User(email: TestHarnessSettings.sharedInstance.userEmail ?? "", firstName: "Ron", lastName: "Idaho") + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/PaymentType.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/PaymentType.swift new file mode 100644 index 0000000..e5b7c38 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloApi/PaymentType.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// PaymentType.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 10/16/23. +// + +import Foundation +import OloPaySDK + +class PaymentType { + let paymentMethod: OPPaymentMethodProtocol? + let cvvToken: OPCvvUpdateTokenProtocol? + + init(_ paymentMethod: OPPaymentMethodProtocol) { + self.paymentMethod = paymentMethod + cvvToken = nil + } + + init(_ cvvToken: OPCvvUpdateTokenProtocol) { + self.cvvToken = cvvToken + paymentMethod = nil + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements b/src/TestHarness/iOS/OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements new file mode 100644 index 0000000..7405134 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/OloPaySDKTestHarness.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.in-app-payments + + merchant.com.olopaysdktestharness + + + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/SceneDelegate.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/SceneDelegate.swift new file mode 100644 index 0000000..61c712e --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/SceneDelegate.swift @@ -0,0 +1,73 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// SceneDelegate.swift +// OloPaySDKTestHarness +// +// Created by Kyle Szklenski on 5/24/21. +// + +import UIKit +import OloPaySDK + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + let setupParams = OPSetupParameters( + withEnvironment: TestHarnessSettings.sharedInstance.productionEnvironment ? OPEnvironment.production : OPEnvironment.test, + withApplePayMerchantId: TestHarnessSettings.sharedInstance.merchantId, + withApplePayCompanyLabel: TestHarnessSettings.sharedInstance.companyLabel) + + let initializer: OloPayApiInitializerProtocol = OloPayApiInitializer() // This could be mocked for testing purposes + initializer.setup(with: setupParams) { + // SDK initialization complete + } + + guard let windowScene = (scene as? UIWindowScene) else { return } + let window = UIWindow(windowScene: windowScene) + + let vc = MainViewController() + let navController = UINavigationController(rootViewController: vc) + + window.rootViewController = navController + self.window = window + self.window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/ConfigUtils.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/ConfigUtils.swift new file mode 100644 index 0000000..0e27fdd --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/ConfigUtils.swift @@ -0,0 +1,48 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ConfigUtils.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/12/21. +// + +import Foundation + +class ConfigUtils { + + + // Code for reading plist from within a framework adapted from + // http://biercoff.com/reading-plist-resource-from-your-ios-framework-library/ + static func getStringPListValue(of key: String) -> String? { + guard let path = Bundle.main.path(forResource: "Info", ofType: "plist"), let dictionary = NSDictionary(contentsOfFile: path) else { + return nil + } + + return dictionary.object(forKey: key) as? String + } + + static func getBoolPListValue(of key: String) -> Bool? { + guard let stringValue = getStringPListValue(of: key), let keyValue = Bool(stringValue) else { + return nil + } + + return keyValue + } + + static func getUInt64PListValue(of key: String) -> UInt64? { + guard let stringValue = getStringPListValue(of: key), let keyValue = UInt64(stringValue) else { + return nil + } + + return keyValue + } + + static func getUIntPListValue(of key: String) -> UInt? { + guard let stringValue = getStringPListValue(of: key), let keyValue = UInt(stringValue) else { + return nil + } + + return keyValue + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Dev.xcconfig b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Dev.xcconfig new file mode 100644 index 0000000..1732ee8 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Dev.xcconfig @@ -0,0 +1,26 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// Dev.xcconfig +// OloPaySDK +// +// Created by Kyle Szklenski on 7/27/21. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +COMPLETE_OLO_PAY_PAYMENT = +APPLE_PAY_BILLING_SCHEME_ID = +BASE_API_URL = +API_KEY = +RESTAURANT_ID = +PRODUCT_ID = +PRODUCT_QTY = +MERCHANT_ID = +COMPANY_LABEL = +USER_EMAIL = +PRODUCTION_ENVIRONMENT = false +USE_LOGGED_IN_USER = false +SAVED_CARD_BILLING_ACCOUNT_ID = +USER_PASSWORD = diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Production.xcconfig b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Production.xcconfig new file mode 100644 index 0000000..c4db842 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/Storage/Production.xcconfig @@ -0,0 +1,26 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// Dev.xcconfig +// OloPaySDK +// +// Created by Kyle Szklenski on 7/27/21. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +COMPLETE_OLO_PAY_PAYMENT = +APPLE_PAY_BILLING_SCHEME_ID = 435 +BASE_API_URL = +API_KEY = +RESTAURANT_ID = +PRODUCT_ID = +PRODUCT_QTY = +MERCHANT_ID = +COMPANY_LABEL = +USER_EMAIL = +PRODUCTION_ENVIRONMENT = true +USE_LOGGED_IN_USER = false +SAVED_CARD_BILLING_ACCOUNT_ID = +USER_PASSWORD = diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ThreadHelpers.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ThreadHelpers.swift new file mode 100644 index 0000000..388c9bc --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ThreadHelpers.swift @@ -0,0 +1,18 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ThreadHelpers.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/9/21. +// + +import Foundation + +func dispatchToMainThreadIfNecessary(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async(execute: block) + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/UITestingIdentifiers.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/UITestingIdentifiers.swift new file mode 100644 index 0000000..b9fdefa --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/UITestingIdentifiers.swift @@ -0,0 +1,50 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// UITestingIdentifiers.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/18/21. +// + +import Foundation + +// NOTE: All Strings in this class must be unique. They are used for automated UI testing +class UITestingIdentifiers { + class TestHarness { + public static let navigationBar : String = "TestHarness.NavigationBar" + public static let cardView : String = "TestHarness.CardView" + public static let formView : String = "TestHarness.FormView" + public static let logView : String = "TestHarness.LogView" + public static let submitButton : String = "TestHarness.SubmitButton" + public static let applePayButton : String = "TestHarness.ApplePayButton" + public static let clearLogButton : String = "TestHarness.ClearLogButton" + public static let settingsButton : String = "TestHarness.SettingsButton" + } + + class Settings { + public static let logCardInputToggle : String = "Settings.CardInputToggle" + public static let completePaymentToggle : String = "Settings.CompletePaymentToggle" + public static let apiUrlTextField : String = "Settings.ApiUrlTextField" + public static let apiKeyTextField : String = "Settings.ApiKeyTextField" + public static let restaurantIdTextField : String = "Settings.RestaurantIdTextField" + public static let productIdTextField : String = "Settings.ProductIdTextField" + public static let productQtyTextField : String = "Settings.ProductQtyTextField" + public static let emailTextField : String = "Settings.EmailTextField" + public static let navigationBar : String = "Settings.NavigationBar" + public static let displayCardErrorsToggle : String = "Settings.DisplayCardErrorsToggle" + public static let customCardErrorMessagesToggle : String = "Settings.CustomCardErrorMessagesToggle" + public static let displayPostalCodeToggle : String = "Settings.DisplayPostalCodeToggle" + public static let useCardViewPaymentToggle : String = "Settings.UseCardViewPaymentToggle" + public static let useFormViewPaymentToggle : String = "Settings.UseFormViewPaymentToggle" + public static let logFormValidToggle : String = "Settings.LogFormValidToggle" + public static let doneButton : String = "Settings.DoneButton" + public static let applePayBillingSchemeIdTextField : String = "Settings.ApplePayBillingSchemeIdTextField" + public static let logCvvChangesToggle: String = "Settings.LogCvvChangesToggle" + public static let displayCvvErrorsToggle: String = "Settings.DisplayCvvErrorsToggle" + public static let customCvvErrorMessagesToggle: String = "Settings.CustomCvvErrorMessages" + public static let useLoggedInUserToggle: String = "Settings.UseLoggedInUser" + public static let userPasswordTextField: String = "Settings.UserPasswordTextField" + public static let userSavedCardBillingSchemeIdTextField: String = "Settings.UserSavedCardBillingSchemeIdTextField" + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/ApplePayViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/ApplePayViewController.swift new file mode 100644 index 0000000..3818690 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/ApplePayViewController.swift @@ -0,0 +1,90 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ApplePayViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit +import PassKit +import OloPaySDK + +class ApplePayViewController : UIViewController, ApplePayViewModelDelegate, ViewControllerWithSettingsProtocol { + private let _viewModel: ApplePayViewModel + + private let applePaySubmitHeader = "----------- APPLE PAY SUBMISSION -----------" + + private let _submitButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) + private let _logViewController: LogViewController + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(viewModel: ApplePayViewModel) { + _viewModel = viewModel + _logViewController = LogViewController(viewModel: _viewModel.logViewModel) + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + + _submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside) + _submitButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + _submitButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + _submitButton.accessibilityIdentifier = UITestingIdentifiers.TestHarness.applePayButton + + let positiveViewSpacing: CGFloat = 10.0 + let negativeViewSpacing: CGFloat = -10.0 + + let mainStack = UIStackView(arrangedSubviews: [_submitButton, _logViewController.view]) + mainStack.axis = .vertical + mainStack.distribution = .fill + mainStack.alignment = .fill + mainStack.spacing = positiveViewSpacing + + view.addSubview(mainStack) + + mainStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + mainStack.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1.0), + mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + + _logViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _logViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + _logViewController.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), + + _submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _submitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + ] + + NSLayoutConstraint.activate(constraints) + + self.addChild(_logViewController) + _logViewController.didMove(toParent: self) + } + + func settingsClicked() { + self.present(SettingsViewController(for: .digitalWallet), animated: true) + } + + func isBusyChanged(busy: Bool) { + dispatchToMainThreadIfNecessary { + self._submitButton.isUserInteractionEnabled = !busy + self._submitButton.isEnabled = !busy + } + } + + @objc func submit() { + _viewModel.log(applePaySubmitHeader, prependNewLine: false, appendNewLine: false) + _viewModel.createPaymentMethod() + } +} + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CardInputViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CardInputViewController.swift new file mode 100644 index 0000000..be313cf --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CardInputViewController.swift @@ -0,0 +1,220 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardInputViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit +import OloPaySDK + +class CardInputViewController : UIViewController, CardInputViewModelDelegate, ViewControllerWithSettingsProtocol { + private let _formSubmitHeader = "---------- FORM DETAILS SUBMISSION ----------" + private let _cardSubmitHeader = "---------- CARD DETAILS SUBMISSION ----------" + private let _viewModel: CardInputViewModel + + private let _submitButton = UIButton() + var _clearButton = UIButton() + var _clearFocusButton = UIButton() + private let _navigationBar = UINavigationBar() + private let _paymentStack = UIStackView() + private var _paymentView : UIView? = nil + private let _cardView = OPPaymentCardDetailsView() + private let _formView = OPPaymentCardDetailsForm() + private let _logViewController: LogViewController + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(viewModel: CardInputViewModel) { + _viewModel = viewModel + _logViewController = LogViewController(viewModel: _viewModel.logViewModel) + _paymentView = _cardView + + super.init(nibName: nil, bundle: nil) + + _viewModel.delegate = self + } + + override func viewDidLoad() { + super.viewDidLoad() + + _paymentStack.axis = NSLayoutConstraint.Axis.vertical + _paymentStack.distribution = UIStackView.Distribution.fill + _paymentStack.alignment = UIStackView.Alignment.fill + + // Set up keyboard done button for input fields + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.dismissKeyboard)) + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)) + toolbar.items = [doneButton] + + _cardView.inputAccessoryView = toolbar + _cardView.accessibilityIdentifier = UITestingIdentifiers.TestHarness.cardView + _cardView.cardDetailsDelegate = _viewModel + _cardView.backgroundColor = .white + _cardView.countryCode = "US" + + _formView.accessibilityIdentifier = UITestingIdentifiers.TestHarness.formView + _formView.cardDetailsDelegate = _viewModel + _formView.backgroundColor = .white + + // Set up card submission button + _submitButton.setTitle("Submit", for: .normal) + _submitButton.backgroundColor = .systemBlue + _submitButton.setTitleColor(UIColor.white, for: .normal) + _submitButton.setTitleColor(UIColor.darkGray, for: .disabled) + _submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside) + _submitButton.accessibilityIdentifier = UITestingIdentifiers.TestHarness.submitButton + + _clearButton.setTitle("Clear Card", for: .normal) + _clearButton.backgroundColor = .white + _clearButton.addTarget(self, action: #selector(clear), for: .touchUpInside) + _clearButton.layer.borderWidth = 2 + _clearButton.layer.borderColor = UIColor.systemBlue.cgColor + _clearButton.setTitleColor(UIColor.systemBlue, for: .normal) + _clearButton.setTitleColor(UIColor.darkGray, for: .disabled) + + _clearFocusButton.setTitle("Clear Focus", for: .normal) + _clearFocusButton.backgroundColor = .white + _clearFocusButton.addTarget(self, action: #selector(clearFocus), for: .touchUpInside) + _clearFocusButton.layer.borderWidth = 2 + _clearFocusButton.layer.borderColor = UIColor.systemBlue.cgColor + _clearFocusButton.setTitleColor(UIColor.systemBlue, for: .normal) + _clearFocusButton.setTitleColor(UIColor.darkGray, for: .disabled) + + // Set up constraints + let positiveViewSpacing: CGFloat = 10.0 + let negativeViewSpacing: CGFloat = -10.0 + + let buttonStack = UIStackView(arrangedSubviews: [_submitButton, _clearButton, _clearFocusButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.alignment = .fill + buttonStack.spacing = positiveViewSpacing + + loadSettings(settings: _viewModel.allSettings) + let mainStack = UIStackView(arrangedSubviews: [_paymentStack, buttonStack, _logViewController.view]) + mainStack.axis = .vertical + mainStack.distribution = .fill + mainStack.alignment = .fill + mainStack.spacing = positiveViewSpacing + + view.addSubview(mainStack) + mainStack.translatesAutoresizingMaskIntoConstraints = false + _paymentStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + mainStack.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1.0), + mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + + _paymentStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _paymentStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + + buttonStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + buttonStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + + _logViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _logViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + _logViewController.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), + ] + + NSLayoutConstraint.activate(constraints) + + self.addChild(_logViewController) + _logViewController.didMove(toParent: self) + } + + private func updatePaymentStack(useSingleLinePayment: Bool) { + guard let paymentView = _paymentView else { + return + } + paymentView.removeFromSuperview() + + if useSingleLinePayment { + _paymentView = _cardView + } else { + _paymentView = _formView + } + + _paymentStack.addArrangedSubview(_paymentView!) + } + + @objc func settingsClicked() { + _cardView.resignFirstResponder() + let _ = _formView.resignFirstResponder() + self.present(SettingsViewController(for: .creditCard), animated: true) + } + + @objc func submit() { + if _viewModel.allSettings.useSingleLinePayment { + submitCard() + } else { + submitForm() + } + } + + @objc func clear() { + _cardView.clear() + } + + @objc func clearFocus() { + let _ = _cardView.resignFirstResponder() + } + + @objc func dismissKeyboard() { + _cardView.resignFirstResponder() + } + + func settingsChanged(settings: TestHarnessSettingsProtocol) { + loadSettings(settings: settings) + } + + private func loadSettings(settings: TestHarnessSettingsProtocol) { + _cardView.clear() + _cardView.displayGeneratedErrorMessages = settings.displayCardErrors + _cardView.postalCodeEntryEnabled = settings.displayPostalCode + OPPaymentCardDetailsView.errorMessageHandler = settings.customCardErrorMessages ? _viewModel.customErrorMessagehandler(_:_:_:) : nil + + updatePaymentStack(useSingleLinePayment: settings.useSingleLinePayment) + _clearButton.isHidden = !settings.useSingleLinePayment + _clearFocusButton.isHidden = !settings.useSingleLinePayment + } + + @objc func submitCard() { + _viewModel.log(_cardSubmitHeader, prependNewLine: true, appendNewLine: false) + _viewModel.log("Card Is Valid: \(_cardView.isValid)") + + guard let paymentParams = _cardView.getPaymentMethodParams() else { + _viewModel.log(_cardView.getErrorMessage(false) + "\n", appendNewLine: true) + return + } + + _viewModel.createPaymentMethod(params: paymentParams) + } + + @objc func submitForm() { + _viewModel.log(_formSubmitHeader, prependNewLine: true, appendNewLine: false) + _viewModel.log("Form Is Valid: \(_formView.isValid)") + + guard let paymentParams = _formView.getPaymentMethodParams() else { + _viewModel.log("Payment Params not valid, returning...\n", appendNewLine: true) + return + } + + _viewModel.createPaymentMethod(params: paymentParams) + } + + func isBusyChanged(busy: Bool) { + dispatchToMainThreadIfNecessary { + self._submitButton.isUserInteractionEnabled = !busy + self._submitButton.isEnabled = !busy + } + } +} + diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CvvTokenViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CvvTokenViewController.swift new file mode 100644 index 0000000..5016bf3 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/CvvTokenViewController.swift @@ -0,0 +1,152 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CvvTokenViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit +import OloPaySDK + +class CvvTokenViewController : UIViewController, CvvTokenViewModelDelegate, ViewControllerWithSettingsProtocol { + + private let _cvvSubmitHeader = "---------- CVV FIELD SUBMISSION ----------" + + let _viewModel: CvvTokenViewModel + + var _cvvView = OPPaymentCardCvvView() + var _submitButton = UIButton() + var _clearButton = UIButton() + var _clearFocusButton = UIButton() + var _logViewController: LogViewController + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(viewModel: CvvTokenViewModel) { + _viewModel = viewModel + _logViewController = LogViewController(viewModel: _viewModel.logViewModel) + super.init(nibName: nil, bundle: nil) + + _viewModel.delegate = self + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Set up keyboard done button for input fields + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(clearFocus)) + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)) + toolbar.items = [doneButton] + + _cvvView.inputAccessoryView = toolbar + _cvvView.cvvDetailsDelegate = _viewModel + + _submitButton.setTitle("Submit Cvv", for: .normal) + _submitButton.backgroundColor = .systemBlue + _submitButton.setTitleColor(.white, for: .normal) + _submitButton.setTitleColor(.darkGray, for: .disabled) + _submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside) + + _clearButton.setTitle("Clear Cvv", for: .normal) + _clearButton.backgroundColor = .white + _clearButton.addTarget(self, action: #selector(clear), for: .touchUpInside) + _clearButton.layer.borderWidth = 2 + _clearButton.layer.borderColor = UIColor.systemBlue.cgColor + _clearButton.setTitleColor(UIColor.systemBlue, for: .normal) + _clearButton.setTitleColor(UIColor.darkGray, for: .disabled) + + _clearFocusButton.setTitle("Clear Focus", for: .normal) + _clearFocusButton.backgroundColor = .white + _clearFocusButton.addTarget(self, action: #selector(clearFocus), for: .touchUpInside) + _clearFocusButton.layer.borderWidth = 2 + _clearFocusButton.layer.borderColor = UIColor.systemBlue.cgColor + _clearFocusButton.setTitleColor(UIColor.systemBlue, for: .normal) + _clearFocusButton.setTitleColor(UIColor.darkGray, for: .disabled) + + let positiveViewSpacing: CGFloat = 10.0 + let negativeViewSpacing: CGFloat = -10.0 + + let buttonStack = UIStackView(arrangedSubviews: [_submitButton, _clearButton, _clearFocusButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.alignment = .fill + buttonStack.spacing = positiveViewSpacing + + let mainStack = UIStackView(arrangedSubviews: [_cvvView, buttonStack, _logViewController.view]) + mainStack.axis = .vertical + mainStack.distribution = .fill + mainStack.alignment = .fill + mainStack.spacing = positiveViewSpacing + + view.addSubview(mainStack) + mainStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + mainStack.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1.0), + mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + + _logViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _logViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + _logViewController.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), + + _cvvView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + _cvvView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing), + + buttonStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: positiveViewSpacing), + buttonStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: negativeViewSpacing) + ] + + NSLayoutConstraint.activate(constraints) + + self.addChild(_logViewController) + _logViewController.didMove(toParent: self) + } + + @objc func submit() { + _viewModel.log(_cvvSubmitHeader, prependNewLine: true, appendNewLine: false) + + guard let params = _cvvView.getCvvTokenParams() else { + _viewModel.log("CVV Params not valid") + return + } + + _viewModel.createToken(params: params) + + } + + @objc func clear() { + _cvvView.clear() + } + + @objc func clearFocus() { + let _ = _cvvView.resignFirstResponder() + } + + func isBusyChanged(busy: Bool) { + dispatchToMainThreadIfNecessary { + self._submitButton.isEnabled = !busy + } + } + + func settingsChanged(settings: TestHarnessSettingsProtocol) { + loadSettings(settings: settings) + } + + func settingsClicked() { + let _ = _cvvView.resignFirstResponder() + self.present(SettingsViewController(for: .cvvToken), animated: true) + } + + private func loadSettings(settings: TestHarnessSettingsProtocol) { + _cvvView.displayGeneratedErrorMessages = settings.displayCvvErrors + + OPPaymentCardCvvView.errorMessageHandler = settings.customCvvErrorMessages ? _viewModel.customErrorMessageHandler(_:_:) : nil + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/LogViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/LogViewController.swift new file mode 100644 index 0000000..88b16e8 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/LogViewController.swift @@ -0,0 +1,93 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// LogViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit +import OloPaySDK + +class LogViewController: UIViewController, LogViewModelDelegate { + let _viewModel: LogViewModel + + let _logTitle = UILabel() + let _logView = UITextView() + let _clearButton = UIButton() + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(viewModel: LogViewModel) { + _viewModel = viewModel + super.init(nibName: nil, bundle: nil) + _viewModel.delegate = self + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.autoresizingMask = .flexibleHeight + + _logTitle.text = "Output Log" + + _logView.textColor = .label + _logView.backgroundColor = UIColor.systemGray4 + _logView.isEditable = false + _logView.accessibilityIdentifier = UITestingIdentifiers.TestHarness.logView + + _clearButton.setTitle("Clear Log", for: .normal) + _clearButton.backgroundColor = .white + _clearButton.addTarget(self, action: #selector(clearLog), for: .touchUpInside) + _clearButton.accessibilityIdentifier = UITestingIdentifiers.TestHarness.clearLogButton + _clearButton.layer.borderWidth = 2 + _clearButton.layer.borderColor = UIColor.systemBlue.cgColor + _clearButton.setTitleColor(UIColor.systemBlue, for: .normal) + _clearButton.setTitleColor(UIColor.darkGray, for: .disabled) + + let positiveViewSpacing: CGFloat = 10.0 + + let mainStack = UIStackView(arrangedSubviews: [_logTitle, _logView, _clearButton]) + mainStack.axis = .vertical + mainStack.distribution = .fill + mainStack.alignment = .fill + mainStack.spacing = positiveViewSpacing + + view.addSubview(mainStack) + mainStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + mainStack.topAnchor.constraint(equalToSystemSpacingBelow: view.topAnchor, multiplier: 1.0), + mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainStack.bottomAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0), + + _logTitle.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + _logTitle.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + + _logView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + _logView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + _logView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), + + _clearButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + _clearButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + ] + + NSLayoutConstraint.activate(constraints) + } + + func logTextChanged(_ logText: String) { + dispatchToMainThreadIfNecessary { + self._logView.text = logText + + // Auto-scroll to end + let bottom = NSMakeRange(self._logView.text.count - 1, 1) + self._logView.scrollRangeToVisible(bottom) + } + } + + @objc func clearLog() { _viewModel.clearLog() } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/MainViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/MainViewController.swift new file mode 100644 index 0000000..f10aecc --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/MainViewController.swift @@ -0,0 +1,95 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/11/23. +// + +import Foundation +import UIKit +import OloPaySDK + +protocol ViewControllerWithSettingsProtocol : NSObjectProtocol { + func settingsClicked() +} + +class MainViewController: UITabBarController, UITabBarControllerDelegate { + lazy var _logoItem: UIBarButtonItem = { + let logoImage = UIImage.init(named: "OloPayLogo") + let logoImageView = UIImageView.init(image: logoImage) + logoImageView.contentMode = .scaleAspectFit + logoImageView.heightAnchor.constraint(equalToConstant: 30).isActive = true + logoImageView.widthAnchor.constraint(equalToConstant: 30).isActive = true + + let appName = UILabel() + appName.font = UIFont.boldSystemFont(ofSize: 18) + appName.text = "Olo Pay" + appName.sizeToFit() + + let logoStack = UIStackView(arrangedSubviews: [logoImageView, appName]) + logoStack.axis = .horizontal + logoStack.isLayoutMarginsRelativeArrangement = true + logoStack.distribution = .equalSpacing + logoStack.alignment = .fill + + return UIBarButtonItem(customView: logoStack) + }() + + private let _oloPayApi = OloPayAPI() + + override func viewDidLoad() { + super.viewDidLoad() + self.delegate = self + self.view.backgroundColor = UIColor.systemGray6 + self.navigationItem.leftBarButtonItem = _logoItem + + createTabBarController() + } + + private func createTabBarController() { + let settings = TestHarnessSettings.sharedInstance + + let cardInputViewModel = CardInputViewModel(logViewModel: LogViewModel(), settings: settings, oloPayApi: _oloPayApi) + let cardInputTab = CardInputViewController(viewModel: cardInputViewModel) + cardInputTab.title = "Credit Card" + cardInputTab.tabBarItem.image = UIImage(systemName: "creditcard") + + let applePayViewModel = ApplePayViewModel(logViewModel: LogViewModel(), settings: settings, oloPayApi: _oloPayApi) + let applePayTab = ApplePayViewController(viewModel: applePayViewModel) + applePayTab.title = "Apple Pay" + applePayTab.tabBarItem.image = UIImage(systemName: "apple.logo") + + let cvvTokenViewModel = CvvTokenViewModel(logViewModel: LogViewModel(), settings: settings, oloPayApi: _oloPayApi) + let cvvTab = CvvTokenViewController(viewModel: cvvTokenViewModel) + cvvTab.title = "CVV" + cvvTab.tabBarItem.image = UIImage(systemName: "creditcard.and.123") + + self.viewControllers = [cardInputTab, applePayTab, cvvTab] + self.tabBar.backgroundColor = UIColor.systemGray5 + + tabBarController(self, didSelect: cardInputTab) + } + + func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { + self.navigationItem.title = viewController.title + + guard viewController is ViewControllerWithSettingsProtocol else { + self.navigationItem.rightBarButtonItem = nil + return + } + + let settingsButton = UIBarButtonItem(title: "Settings", style: UIBarButtonItem.Style.plain, target: self, action: #selector(self.settingsClicked)) + settingsButton.accessibilityIdentifier = UITestingIdentifiers.TestHarness.settingsButton + self.navigationItem.rightBarButtonItem = settingsButton + } + + @objc func settingsClicked() { + guard let settingsViewController = self.selectedViewController as? ViewControllerWithSettingsProtocol else { + return + } + + settingsViewController.settingsClicked() + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/SettingsViewController.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/SettingsViewController.swift new file mode 100644 index 0000000..5249203 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewControllers/SettingsViewController.swift @@ -0,0 +1,603 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// SettingsViewController.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 7/8/21. +// + +import Foundation +import UIKit + +enum SettingsType { + case creditCard + case digitalWallet + case cvvToken +} + +// NOTE: This needs to be refactored to use MVVM +class SettingsViewController : UIViewController, UITextFieldDelegate { + var _settingsType: SettingsType = .creditCard + var _navigationBar = UINavigationBar() + var _scrollContent = UIScrollView() + + // Settings not controlled by Plist Values + var _useMultiLinePaymentToggle = UISwitch() + var _logCardInputToggle = UISwitch() + var _displayCardErrorsToggle = UISwitch() + var _customCardErrorMessagesToggle = UISwitch() + var _displayPostalCodeToggle = UISwitch() + var _useSingleLinePaymentToggle = UISwitch() + var _logFormValidToggle = UISwitch() + var _displayCvvErrorsToggle = UISwitch() + var _logCvvChangesToggle = UISwitch() + var _customCvvErrorsToggle = UISwitch() + + // Ordering API Settings + var _completePaymentToggle = UISwitch() + var _requireLoggedInUserLabel = UILabel() + var _apiUrlTextField = UITextField() + var _apiKeyTextField = UITextField() + var _restaurantIdTextField = UITextField() + var _productIdTextField = UITextField() + var _productQtyTextField = UITextField() + var _applePayBillingSchemeIdTextField = UITextField() + + // User Settings + var _useLoggedInUser = UISwitch() + var _userEmailTextField = UITextField() + var _userPasswordTextField = UITextField() + var _userSavedCardBillingAccountIdTextField = UITextField() + + @objc public required init?(coder: NSCoder) { + super.init(coder: coder) + self.view.backgroundColor = .systemBackground + self.isModalInPresentation = true + } + + public init(for type: SettingsType) { + super.init(nibName: nil, bundle: nil) + _settingsType = type + self.view.backgroundColor = .systemBackground + self.isModalInPresentation = true + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupViews() + } + + override func viewDidLoad() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + override func viewWillDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver(self) + } + + var onDismiss: (() -> Void) = {} + + func setUITestingIdentifiers() { + _logCardInputToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.logCardInputToggle + _completePaymentToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.completePaymentToggle + _apiUrlTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.apiUrlTextField + _apiKeyTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.apiKeyTextField + _restaurantIdTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.restaurantIdTextField + _productIdTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.productIdTextField + _productQtyTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.productQtyTextField + _userEmailTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.emailTextField + _navigationBar.accessibilityIdentifier = UITestingIdentifiers.Settings.navigationBar + _displayCardErrorsToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.displayCardErrorsToggle + _customCardErrorMessagesToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.customCardErrorMessagesToggle + _displayPostalCodeToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.displayPostalCodeToggle + _useSingleLinePaymentToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.useCardViewPaymentToggle + _useMultiLinePaymentToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.useFormViewPaymentToggle + _logFormValidToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.logFormValidToggle + _applePayBillingSchemeIdTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.applePayBillingSchemeIdTextField + _logCvvChangesToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.logCvvChangesToggle + _displayCvvErrorsToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.displayCvvErrorsToggle + _customCvvErrorsToggle.accessibilityIdentifier = UITestingIdentifiers.Settings.customCvvErrorMessagesToggle + _useLoggedInUser.accessibilityIdentifier = UITestingIdentifiers.Settings.useLoggedInUserToggle + _userPasswordTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.userPasswordTextField + _userSavedCardBillingAccountIdTextField.accessibilityIdentifier = UITestingIdentifiers.Settings.userSavedCardBillingSchemeIdTextField + } + + func setupViews() { + setUITestingIdentifiers() + + let navigationItem = UINavigationItem(title: "Olo Pay SDK: Settings") + let settingsButton = UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.plain, target: nil, action: #selector(self.close)) + settingsButton.accessibilityIdentifier = UITestingIdentifiers.Settings.doneButton + + navigationItem.rightBarButtonItem = settingsButton + _navigationBar.items = [navigationItem] + + createScrollingStack(scrollView: _scrollContent) + + let mainStack = UIStackView(arrangedSubviews: [_navigationBar, _scrollContent]) + mainStack.axis = NSLayoutConstraint.Axis.vertical + mainStack.distribution = UIStackView.Distribution.fill + mainStack.alignment = UIStackView.Alignment.fill + + self.view.addSubview(mainStack) + mainStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + mainStack.topAnchor.constraint(equalTo: view.topAnchor), + mainStack.leftAnchor.constraint(equalTo: view.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: view.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + _scrollContent.topAnchor.constraint(equalTo: _navigationBar.bottomAnchor), + _scrollContent.leftAnchor.constraint(equalTo: mainStack.leftAnchor), + _scrollContent.rightAnchor.constraint(equalTo: mainStack.rightAnchor), + _scrollContent.widthAnchor.constraint(equalTo: mainStack.widthAnchor), + _scrollContent.bottomAnchor.constraint(equalTo: mainStack.bottomAnchor), + ] + + NSLayoutConstraint.activate(constraints) + updateFieldEnabledStatus() + } + + func createScrollingStack(scrollView: UIScrollView) { + let scrollStack = UIStackView() + scrollStack.axis = NSLayoutConstraint.Axis.vertical + scrollStack.distribution = UIStackView.Distribution.fill + scrollStack.alignment = UIStackView.Alignment.fill + scrollStack.spacing = 10 + + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.dismissKeyboard)) + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)) + toolbar.items = [doneButton] + + if (_settingsType == .creditCard) { + scrollStack.addArrangedSubview(createCreditCardStack()) + } + + if (_settingsType == .cvvToken) { + scrollStack.addArrangedSubview(createCvvTokenStack()) + } + + scrollStack.addArrangedSubview(createOrderingApiStack(toolbar)) + scrollStack.addArrangedSubview(createUserStack(toolbar)) + + scrollView.addSubview(scrollStack) + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollStack.translatesAutoresizingMaskIntoConstraints = false + + let constraints = [ + scrollStack.topAnchor.constraint(equalTo: scrollView.topAnchor), + scrollStack.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 10), + scrollStack.rightAnchor.constraint(equalTo: scrollView.rightAnchor), + scrollStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + scrollStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -20) + ] + + NSLayoutConstraint.activate(constraints) + } + + func createCreditCardStack() -> UIStackView { + let cardStack = UIStackView() + cardStack.axis = .vertical + cardStack.distribution = .fill + cardStack.alignment = .fill + cardStack.spacing = 10 + + //########################### + //## Single-Line Section + //########################### + cardStack.addArrangedSubview(createSectionHeader(title: "Single-Line Credit Card Settings")) + + // Set up Use Single Line Input row + setupToggle(toggle: _useSingleLinePaymentToggle, isOn: TestHarnessSettings.sharedInstance.useSingleLinePayment, action: #selector(useSingleLinePaymentToggled)) + let useSingleLineRow = createHorizontalStack(leftView: createLabel(title: "Use Single-Line Payment"), rightView: _useSingleLinePaymentToggle) + cardStack.addArrangedSubview(useSingleLineRow) + + // Set up Log Card Input row + setupToggle(toggle: _logCardInputToggle, isOn: TestHarnessSettings.sharedInstance.logCardInputChanges, action: #selector(cardInputToggled)) + let logCardInputRow = createHorizontalStack(leftView: createLabel(title: "Log Card Input Changes"), rightView: _logCardInputToggle) + cardStack.addArrangedSubview(logCardInputRow) + + //Set up Display Postal Code + setupToggle(toggle: _displayPostalCodeToggle, isOn: TestHarnessSettings.sharedInstance.displayPostalCode, action: #selector(displayPostalCodeToggled)) + let displayPostalCodeRow = createHorizontalStack(leftView: createLabel(title: "Display Postal Code"), rightView: _displayPostalCodeToggle) + cardStack.addArrangedSubview(displayPostalCodeRow) + + //Set up Display Card Errors UI + setupToggle(toggle: _displayCardErrorsToggle, isOn: TestHarnessSettings.sharedInstance.displayCardErrors, action: #selector(displayCardErrorsToggled)) + let displayCardErrorsRow = createHorizontalStack(leftView: createLabel(title: "Display Card Errors"), rightView: _displayCardErrorsToggle) + cardStack.addArrangedSubview(displayCardErrorsRow) + + //Set up Use Custom Error Messages + setupToggle(toggle: _customCardErrorMessagesToggle, isOn: TestHarnessSettings.sharedInstance.customCardErrorMessages, action: #selector(customCardErrorMessagesToggled)) + let customCardErrorMessagesRow = createHorizontalStack(leftView: createLabel(title: "Use Custom Card Error Messages"), rightView: _customCardErrorMessagesToggle) + cardStack.addArrangedSubview(customCardErrorMessagesRow) + + //########################### + //## Multi-Line Section + //########################### + cardStack.addArrangedSubview(createSectionHeader(title: "Multi-Line Credit Card Settings")) + + // Set up Use Multiline Payment Row + setupToggle(toggle: _useMultiLinePaymentToggle, isOn: !TestHarnessSettings.sharedInstance.useSingleLinePayment, action: #selector(useMultiLinePaymentToggled)) + let multiLinePaymentRow = createHorizontalStack(leftView: createLabel(title: "Use Multi-Line Payment"), rightView: _useMultiLinePaymentToggle) + cardStack.addArrangedSubview(multiLinePaymentRow) + + // Set up Log Form Valid Row + setupToggle(toggle: _logFormValidToggle, isOn: TestHarnessSettings.sharedInstance.logFormValidChanges, action: #selector(logFormValidToggled)) + let logFormValidRow = createHorizontalStack(leftView: createLabel(title: "Log Form Valid Changes"), rightView: _logFormValidToggle) + cardStack.addArrangedSubview(logFormValidRow) + + return cardStack + } + + func createCvvTokenStack() -> UIStackView { + let cvvStack = UIStackView() + cvvStack.axis = .vertical + cvvStack.distribution = .fill + cvvStack.alignment = .fill + cvvStack.spacing = 10 + + cvvStack.addArrangedSubview(createSectionHeader(title: "CVV Token Settings")) + + // Set up Log CVV Input row + setupToggle(toggle: _logCvvChangesToggle, isOn: TestHarnessSettings.sharedInstance.logCvvInputChanges, action: #selector(logCvvInputToggled)) + let logCvvInputRow = createHorizontalStack(leftView: createLabel(title: "Log CVV Input Changes"), rightView: _logCvvChangesToggle) + cvvStack.addArrangedSubview(logCvvInputRow) + + //Set up Display Card Errors UI + setupToggle(toggle: _displayCvvErrorsToggle, isOn: TestHarnessSettings.sharedInstance.displayCvvErrors, action: #selector(displayCvvErrorsToggled)) + let displayCvvErrorsRow = createHorizontalStack(leftView: createLabel(title: "Display CVV Errors"), rightView: _displayCvvErrorsToggle) + cvvStack.addArrangedSubview(displayCvvErrorsRow) + + //Set up Use Custom Error Messages + setupToggle(toggle: _customCvvErrorsToggle, isOn: TestHarnessSettings.sharedInstance.customCvvErrorMessages, action: #selector(customCvvErrorMessagesToggled)) + let customCvvErrorMessagesRow = createHorizontalStack(leftView: createLabel(title: "Use Custom CVV Error Messages"), rightView: _customCvvErrorsToggle) + cvvStack.addArrangedSubview(customCvvErrorMessagesRow) + + return cvvStack + } + + func createOrderingApiStack(_ toolbar: UIToolbar) -> UIStackView { + let apiStack = UIStackView() + apiStack.axis = .vertical + apiStack.distribution = .fill + apiStack.alignment = .fill + apiStack.spacing = 10 + + apiStack.addArrangedSubview(createSectionHeader(title: "Ordering API Settings")) + + // Set up Complete Payment row + setupToggle(toggle: _completePaymentToggle, isOn: TestHarnessSettings.sharedInstance.completeOloPayPayment, action: #selector(completePaymentToggled)) + let completePaymentRow = createHorizontalStack(leftView: createLabel(title: "Create Basket & Complete Payment"), rightView: _completePaymentToggle) + apiStack.addArrangedSubview(completePaymentRow) + + // Set up logged in user warning + _requireLoggedInUserLabel.text = "(CVV Payments require logged in user)" + _requireLoggedInUserLabel.textColor = .systemRed + _requireLoggedInUserLabel.textAlignment = .center + _requireLoggedInUserLabel.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 12)) + apiStack.addArrangedSubview(_requireLoggedInUserLabel) + + // Set up API URL row + setupTextField(field: _apiUrlTextField, toolbar: toolbar, text: TestHarnessSettings.sharedInstance.baseAPIUrl) + let apiUrlRow = createHorizontalStack(leftView: createLabel(title: "API URL"), rightView: _apiUrlTextField) + apiStack.addArrangedSubview(apiUrlRow) + + // Set up API Key row + setupTextField(field: _apiKeyTextField, toolbar: toolbar, text: TestHarnessSettings.sharedInstance.apiKey) + let apiKeyRow = createHorizontalStack(leftView: createLabel(title: "API Key"), rightView: _apiKeyTextField) + apiStack.addArrangedSubview(apiKeyRow) + + // Set up Restaurant ID row + setupTextField(field: _restaurantIdTextField, toolbar: toolbar, text: nil) + _restaurantIdTextField.keyboardType = UIKeyboardType.numberPad + _restaurantIdTextField.delegate = self + if TestHarnessSettings.sharedInstance.restaurantId != nil { + _restaurantIdTextField.text = String(TestHarnessSettings.sharedInstance.restaurantId!) + } + let restaurantIdRow = createHorizontalStack(leftView: createLabel(title: "Restaurant ID"), rightView: _restaurantIdTextField) + apiStack.addArrangedSubview(restaurantIdRow) + + // Set up Product ID row + setupTextField(field: _productIdTextField, toolbar: toolbar, text: nil) + _productIdTextField.keyboardType = UIKeyboardType.numberPad + _productIdTextField.delegate = self + if TestHarnessSettings.sharedInstance.productId != nil { + _productIdTextField.text = String(TestHarnessSettings.sharedInstance.productId!) + } + let productIdRow = createHorizontalStack(leftView: createLabel(title: "Product ID"), rightView: _productIdTextField) + apiStack.addArrangedSubview(productIdRow) + + // Set up Product Qty row + setupTextField(field: _productQtyTextField, toolbar: toolbar, text: nil) + _productQtyTextField.keyboardType = UIKeyboardType.numberPad + _productQtyTextField.delegate = self + if TestHarnessSettings.sharedInstance.productQty != nil { + _productQtyTextField.text = String(TestHarnessSettings.sharedInstance.productQty!) + } + let productQtyRow = createHorizontalStack(leftView: createLabel(title: "Product Qty"), rightView: _productQtyTextField) + apiStack.addArrangedSubview(productQtyRow) + + + // Set up Apple Pay billing scheme Id row + if (_settingsType == .digitalWallet) { + setupTextField(field: _applePayBillingSchemeIdTextField, toolbar: toolbar, text: String(TestHarnessSettings.sharedInstance.applePayBillingSchemeId ?? "")) + _applePayBillingSchemeIdTextField.keyboardType = UIKeyboardType.numberPad + _applePayBillingSchemeIdTextField.delegate = self + + let schemeRow = createHorizontalStack(leftView: createLabel(title: "Apple Pay Billing Scheme Id"), rightView: _applePayBillingSchemeIdTextField) + apiStack.addArrangedSubview(schemeRow) + } + + return apiStack + } + + func createUserStack(_ toolbar: UIToolbar) -> UIStackView { + let userStack = UIStackView() + userStack.axis = .vertical + userStack.distribution = .fill + userStack.alignment = .fill + userStack.spacing = 10 + + userStack.addArrangedSubview(createSectionHeader(title: "User API Settings")) + + // Set up user type row + setupToggle(toggle: _useLoggedInUser, isOn: TestHarnessSettings.sharedInstance.useLoggedInUser, action: #selector(loggedInUserToggled)) + let guestUserRow = createHorizontalStack(leftView: createLabel(title: "Use Logged In User"), rightView: _useLoggedInUser) + userStack.addArrangedSubview(guestUserRow) + + // Set up user email row + setupTextField(field: _userEmailTextField, toolbar: toolbar, text: String(TestHarnessSettings.sharedInstance.userEmail ?? "")) + _userEmailTextField.keyboardType = UIKeyboardType.emailAddress + let emailRow = createHorizontalStack(leftView: createLabel(title: "Email"), rightView: _userEmailTextField) + userStack.addArrangedSubview(emailRow) + + // Set up user password + setupTextField(field: _userPasswordTextField, toolbar: toolbar, text: String(TestHarnessSettings.sharedInstance.userPassword ?? "")) + _userPasswordTextField.isSecureTextEntry = true + let passwordRow = createHorizontalStack(leftView: createLabel(title: "Password"), rightView: _userPasswordTextField) + userStack.addArrangedSubview(passwordRow) + + if _settingsType == .cvvToken { + setupTextField(field: _userSavedCardBillingAccountIdTextField, toolbar: toolbar, text: String(TestHarnessSettings.sharedInstance.savedCardBillingAccountId ?? "")) + _userSavedCardBillingAccountIdTextField.keyboardType = UIKeyboardType.numberPad + _userSavedCardBillingAccountIdTextField.delegate = self + + let schemeRow = createHorizontalStack(leftView: createLabel(title: "Saved Card Billing Account Id"), rightView: _userSavedCardBillingAccountIdTextField) + userStack.addArrangedSubview(schemeRow) + } + + return userStack + } + + func createHorizontalStack(leftView: UIView, rightView: UIView) -> UIStackView { + let stack = UIStackView(arrangedSubviews: [leftView, rightView]) + stack.axis = NSLayoutConstraint.Axis.horizontal + stack.distribution = UIStackView.Distribution.fill + stack.alignment = UIStackView.Alignment.fill + stack.spacing = 10 + + return stack + } + + func setupTextField(field: UITextField, toolbar: UIToolbar, text: String?) { + field.borderStyle = .roundedRect + field.inputAccessoryView = toolbar + field.text = text ?? "" + field.textColor = .label + field.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal) + field.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal) + } + + func createLabel(title: String) -> UILabel { + let label = UILabel() + label.text = title + label.textColor = .label + label.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal) + label.setContentCompressionResistancePriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.horizontal) + return label + } + + func setupToggle(toggle: UISwitch, isOn: Bool?, action: Selector) { + toggle.addTarget(self, action: action, for: .touchUpInside) + toggle.isOn = isOn ?? false + toggle.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal) + toggle.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal) + toggle.contentHorizontalAlignment = .right + } + + func createSectionHeader(title: String) -> UILabel { + let header = UILabel() + header.text = title + header.textColor = .systemGray6 + header.backgroundColor = UIColor.systemGray + header.font = UIFont.boldSystemFont(ofSize: 22) + header.textAlignment = .center + return header + } + + @objc func close() { + TestHarnessSettings.sharedInstance.baseAPIUrl = _apiUrlTextField.text + TestHarnessSettings.sharedInstance.apiKey = _apiKeyTextField.text + + if let restaurantIdString = _restaurantIdTextField.text, let restaurantId = UInt64(restaurantIdString) { + TestHarnessSettings.sharedInstance.restaurantId = restaurantId + } + + if let productIdString = _productIdTextField.text, let productId = UInt64(productIdString) { + TestHarnessSettings.sharedInstance.productId = productId + } + + if let productQtyString = _productQtyTextField.text, let productQty = UInt(productQtyString) { + TestHarnessSettings.sharedInstance.productQty = productQty + } + + if let email = _userEmailTextField.text { + TestHarnessSettings.sharedInstance.userEmail = email + } + + if let password = _userPasswordTextField.text { + TestHarnessSettings.sharedInstance.userPassword = password + } + + if let savedCardBillingAccount = _userSavedCardBillingAccountIdTextField.text, _settingsType == .cvvToken { + TestHarnessSettings.sharedInstance.savedCardBillingAccountId = savedCardBillingAccount + } + + if _settingsType == .digitalWallet, let billingScheme = _applePayBillingSchemeIdTextField.text { + TestHarnessSettings.sharedInstance.applePayBillingSchemeId = billingScheme + } + + onDismiss() + TestHarnessSettings.sharedInstance.notifySettingsChanged() + self.dismiss(animated: true, completion: nil) + } + + @objc func dismissKeyboard() { + _apiUrlTextField.resignFirstResponder() + _apiKeyTextField.resignFirstResponder() + _restaurantIdTextField.resignFirstResponder() + _productIdTextField.resignFirstResponder() + _productQtyTextField.resignFirstResponder() + _userEmailTextField.resignFirstResponder() + _userPasswordTextField.resignFirstResponder() + _userSavedCardBillingAccountIdTextField.resignFirstResponder() + } + + @objc func displayCardErrorsToggled() { + TestHarnessSettings.sharedInstance.displayCardErrors = _displayCardErrorsToggle.isOn + } + + @objc func customCardErrorMessagesToggled() { + TestHarnessSettings.sharedInstance.customCardErrorMessages = _customCardErrorMessagesToggle.isOn + } + + @objc func displayPostalCodeToggled() { + TestHarnessSettings.sharedInstance.displayPostalCode = _displayPostalCodeToggle.isOn + } + + @objc func cardInputToggled() { + TestHarnessSettings.sharedInstance.logCardInputChanges = _logCardInputToggle.isOn + } + + @objc func logCvvInputToggled() { + TestHarnessSettings.sharedInstance.logCvvInputChanges = _logCvvChangesToggle.isOn + } + + @objc func loggedInUserToggled() { + TestHarnessSettings.sharedInstance.useLoggedInUser = _useLoggedInUser.isOn + updateFieldEnabledStatus() + } + + @objc func displayCvvErrorsToggled() { + TestHarnessSettings.sharedInstance.displayCvvErrors = _displayCvvErrorsToggle.isOn + } + + @objc func customCvvErrorMessagesToggled() { + TestHarnessSettings.sharedInstance.customCvvErrorMessages = _customCvvErrorsToggle.isOn + } + + @objc func completePaymentToggled() { + let completePayment = _completePaymentToggle.isOn + TestHarnessSettings.sharedInstance.completeOloPayPayment = completePayment + updateFieldEnabledStatus() + } + + func updateFieldEnabledStatus() { + let completePayment = TestHarnessSettings.sharedInstance.completeOloPayPayment + let useLoggedInUser = TestHarnessSettings.sharedInstance.useLoggedInUser + + // Update Ordering API Settings + setTextFieldEnabled(_apiUrlTextField, completePayment) + setTextFieldEnabled(_apiKeyTextField, completePayment) + setTextFieldEnabled(_restaurantIdTextField, completePayment) + setTextFieldEnabled(_productIdTextField, completePayment) + setTextFieldEnabled(_productQtyTextField, completePayment) + + if (_settingsType == .cvvToken) { + let showLabel = completePayment && !useLoggedInUser + _requireLoggedInUserLabel.isHidden = !showLabel + } else { + _requireLoggedInUserLabel.isHidden = true + } + + if _settingsType == .digitalWallet { + setTextFieldEnabled(_applePayBillingSchemeIdTextField, completePayment) + } + + setToggleFieldEnabled(_useLoggedInUser, completePayment) + setTextFieldEnabled(_userEmailTextField, completePayment) + setTextFieldEnabled(_userPasswordTextField, completePayment && useLoggedInUser) + setTextFieldEnabled(_userSavedCardBillingAccountIdTextField, completePayment && useLoggedInUser) + } + + func setTextFieldEnabled(_ textField: UITextField, _ isEnabled: Bool) { + textField.isEnabled = isEnabled + textField.backgroundColor = isEnabled ? .secondarySystemFill : .systemBackground + textField.textColor = isEnabled ? .label : .secondaryLabel + } + + func setToggleFieldEnabled(_ toggleField: UISwitch, _ isEnabled: Bool) { + toggleField.isEnabled = isEnabled + } + + @objc func useSingleLinePaymentToggled() { + let useSingleLine = _useSingleLinePaymentToggle.isOn + TestHarnessSettings.sharedInstance.useSingleLinePayment = useSingleLine + + updateSingleLineSettingsEnabledState(singleLineEnabled: useSingleLine) + } + + @objc func useMultiLinePaymentToggled() { + let useMultiLine = _useMultiLinePaymentToggle.isOn + TestHarnessSettings.sharedInstance.useSingleLinePayment = !useMultiLine + + updateSingleLineSettingsEnabledState(singleLineEnabled: !useMultiLine) + } + + @objc func logFormValidToggled() { + let logFormValid = _logFormValidToggle.isOn + TestHarnessSettings.sharedInstance.logFormValidChanges = logFormValid + } + + @objc func updateSingleLineSettingsEnabledState(singleLineEnabled: Bool) { + // Single line settings + _useSingleLinePaymentToggle.isOn = singleLineEnabled + _logCardInputToggle.isEnabled = singleLineEnabled + _displayPostalCodeToggle.isEnabled = singleLineEnabled + _displayCardErrorsToggle.isEnabled = singleLineEnabled + _displayCardErrorsToggle.isEnabled = singleLineEnabled + _customCardErrorMessagesToggle.isEnabled = singleLineEnabled + + // Multi line settings + _useMultiLinePaymentToggle.isOn = !singleLineEnabled + _logFormValidToggle.isEnabled = !singleLineEnabled + } + + @objc private func keyboardWillShow(notification: NSNotification){ + guard let keyboardFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + _scrollContent.contentInset.bottom = view.convert(keyboardFrame.cgRectValue, from: nil).size.height + } + + @objc private func keyboardWillHide(notification: NSNotification){ + _scrollContent.contentInset.bottom = 0 + } + + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard !string.isEmpty else { + return true + } + + if textField.keyboardType == .numberPad { + if CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) { + return true + } + } + + return false + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/ApplePayViewModel.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/ApplePayViewModel.swift new file mode 100644 index 0000000..65971e0 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/ApplePayViewModel.swift @@ -0,0 +1,243 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// ApplePayViewModel.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/15/23. +// + +import Foundation +import OloPaySDK + +protocol ApplePayViewModelDelegate: NSObjectProtocol { + func isBusyChanged(busy: Bool) +} + +class ApplePayViewModel: NSObject, OPApplePayContextDelegate, TestHarnessSettingsObserver { + private let newSettingsHeader = "--------------- NEW SETTINGS ---------------" + + private let _oloPayApi: OloPayAPIProtocol + private let _settings: TestHarnessSettings + private var _apiClient: OloApiClient? + + private var _applePayFlowCompleted = false + private var _applePayContext: OPApplePayContextProtocol? = nil + private let _applePayCondition = NSCondition() + + public var delegate: ApplePayViewModelDelegate? = nil + private(set) var logViewModel: LogViewModel + + private(set) var isBusy: Bool { + didSet { + delegate?.isBusyChanged(busy: self.isBusy) + } + } + + public var allSettings: TestHarnessSettingsProtocol { + get { _settings.allSettings } + } + + required init(logViewModel: LogViewModel, settings:TestHarnessSettings, oloPayApi: OloPayAPIProtocol) { + _oloPayApi = oloPayApi + _settings = settings + _apiClient = OloApiClient.createFromSettings() + self.logViewModel = logViewModel + isBusy = false + super.init() + + settings.addObserver(self) + } + + func createPaymentMethod() { + guard _oloPayApi.deviceSupportsApplePay() else { + log("Apple Pay not supported") + return + } + + isBusy = true + guard _settings.completeOloPayPayment else { + // Just create a payment method via Apple Pay and be done + beginApplePayFlow() + return + } + + // Create a basket via the Olo Ordering API. Then create a payment method + // via Apple Pay, and submit the basket to the ordering API with the + // payment method + createBasket() { basket in + guard let basket = basket else { + self.isBusy = false + return + } + + self.beginApplePayFlow(for: basket) + } + } + + func settingsChanged(settings: TestHarnessSettingsProtocol) { + logSettings() + _apiClient = OloApiClient.createFromSettings() + } + + private func beginApplePayFlow(for basket: Basket? = nil) { + do + { + _applePayFlowCompleted = false + var total: NSDecimalNumber = 0.12 + var basketId: String? = nil + if let basket = basket { + if let basketTotal = basket.total { + total = NSDecimalNumber(decimal: basketTotal) + } + basketId = basket.id + } + + let pkPaymentRequest = try _oloPayApi.createPaymentRequest(forAmount: total, inCountry: "US", withCurrency: "USD") + + // This can be mocked for testing purposes + _applePayContext = OPApplePayContext(paymentRequest: pkPaymentRequest, delegate: self, basketId: basketId) + + try _applePayContext?.presentApplePay() { + self.log("Apple Pay Sheet Presented", appendNewLine: false) + self.log("Payment Request:\n\(String(describing: pkPaymentRequest))") + } + } catch OPApplePayContextError.missingMerchantId { + log("Error: Missing merchant ID") + isBusy = false + } catch OPApplePayContextError.emptyMerchantId { + log("Error: Empty merchant ID") + isBusy = false + } catch OPApplePayContextError.missingCompanyLabel { + log("Error: Missing Company Label") + isBusy = false + } catch OPApplePayContextError.emptyCompanyLabel { + log("Error: Empty Company Label") + isBusy = false + } catch { + log("Unspecified error") + isBusy = false + } + } + + private func createBasket(completion: @escaping (_: Basket?) -> Void) { + guard let apiClient = _apiClient else { + log("Unable to complete payment... apiClient is nil") + completion(nil) + return + } + + log("Creating Basket...", appendNewLine: false) + + apiClient.createBasketWithProductFromSettings() { basket, error, message in + guard let basket = basket else { + self.logError(error: error) + self.log(message) + completion(nil) + return + } + + self.log("Basket Created: \(String(describing: basket))") + completion(basket) + } + } + + private func createError(with message: String) -> NSError { + let userInfo: [String : String] = [ NSLocalizedDescriptionKey : message ] + return NSError(domain: "com.olo.olopaysdktestharness", code: 400, userInfo: userInfo) + } + + private func logSettings() { + log(self.newSettingsHeader, appendNewLine: false) + + let useOrderingApi = _settings.completeOloPayPayment + log("Create Basket & Complete Payment: \(useOrderingApi)", appendNewLine: false) + + if (useOrderingApi) { + log("API URL: \(_settings.baseAPIUrl ?? "")", appendNewLine: false) + log("Restaurant Id: \(String(describing: _settings.restaurantId))", appendNewLine: false) + log("Product Id: \(String(describing: _settings.productId))", appendNewLine: false) + log("Product Qty: \(String(describing: _settings.productQty))", appendNewLine: false) + log("Email: \(String(describing: _settings.userEmail))", appendNewLine: false) + } + + log("") + } + + @objc func applePaymentMethodCreated(_ context: OPApplePayContextProtocol, didCreatePaymentMethod paymentMethod: OPPaymentMethodProtocol) -> NSError? { + logPaymentMethod(paymentMethod: paymentMethod) + + guard _settings.completeOloPayPayment else { + return nil + } + + guard let apiClient = _apiClient else { + log("Unable to submit order... api client is nil") + return createError(with: "Unable to submit order... api client is nil") //We should never get this far if apiClient is nil... + } + + guard let basketId = context.basketId else { + log("Unable to submit order... basket id is nil") + return createError(with: "Unable to submit order... basket id is nil") //Likewise this should never happen + } + + log("Submitting ApplePay order...", appendNewLine: false) + + var createdOrder: Order? = nil + var orderMessage: String? = nil + var orderError: Error? = nil + + _applePayCondition.lock() // Lock this thread until submit basket completes + apiClient.submitBasketFromSettings(with: paymentMethod, basketId: basketId) { order, error, message in + createdOrder = order + orderError = error + orderMessage = message + self._applePayCondition.signal() //Tell the waiting thread to wake and check the condition again + } + + // Check the condition and wait until the condition is no longer true + while _applePayFlowCompleted == false && createdOrder == nil && orderMessage == nil && orderError == nil { + _applePayCondition.wait() + } + + _applePayCondition.unlock() //Unlock this thread so it can continue processing + + guard let order = createdOrder else { + logError(error: orderError) + + if let orderMessage = orderMessage { + log(orderMessage) + } + + //Return an error to trigger an Apple Pay Error Dismissal + return createError(with: (orderMessage ?? orderError?.localizedDescription) ?? "Unexpected error") + } + + log("Order created: \(order.id)") + + //Return nil to trigger an Apple Pay Success Dismissal + return nil + } + + @objc func applePaymentCompleted(_ context: OPApplePayContextProtocol, didCompleteWith status: OPPaymentStatus, error: Error?) { + _applePayFlowCompleted = true + _applePayCondition.signal() + + log("ApplePay Flow Completed") + log("Status: \(String(describing: status))\n", appendNewLine: true) + logError(error: error) + isBusy = false + } + + public func log(_ message: String?, prependNewLine: Bool = true, appendNewLine: Bool = true) { + logViewModel.log(message, prependNewLine: prependNewLine, appendNewLine: appendNewLine) + } + + func logError(error: Error?) { + logViewModel.logError(error: error) + } + + func logPaymentMethod(paymentMethod: OloPaySDK.OPPaymentMethodProtocol?) { + logViewModel.logPaymentMethod(paymentMethod: paymentMethod) + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CardInputViewModel.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CardInputViewModel.swift new file mode 100644 index 0000000..3c96fc1 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CardInputViewModel.swift @@ -0,0 +1,263 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CardInputViewModel.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/15/23. +// + +import Foundation +import OloPaySDK + +protocol CardInputViewModelDelegate: NSObjectProtocol { + func isBusyChanged(busy: Bool) + func settingsChanged(settings: TestHarnessSettingsProtocol) +} + +class CardInputViewModel: NSObject, OPPaymentCardDetailsViewDelegate, OPPaymentCardDetailsFormDelegate, TestHarnessSettingsObserver { + private let newSettingsHeader = "--------------- NEW SETTINGS ---------------" + + private let _oloPayApi: OloPayAPIProtocol + private let _settings: TestHarnessSettings + private var _apiClient: OloApiClient? + + public var delegate: CardInputViewModelDelegate? = nil + private(set) var logViewModel: LogViewModel + + private(set) var isBusy: Bool { + didSet { + delegate?.isBusyChanged(busy: self.isBusy) + } + } + + public var allSettings: TestHarnessSettingsProtocol { + get { _settings.allSettings } + } + + required init(logViewModel: LogViewModel, settings: TestHarnessSettings, oloPayApi: OloPayAPIProtocol) { + _oloPayApi = oloPayApi + _settings = settings + _apiClient = OloApiClient.createFromSettings() + self.logViewModel = logViewModel + isBusy = false + + super.init() + + settings.addObserver(self) + } + + public func createPaymentMethod(params: OPPaymentMethodParamsProtocol) { + isBusy = true + + guard _settings.completeOloPayPayment else { + createPaymentMethod(with: params) + isBusy = false + return + } + + createBasket() { basket in + guard let basket = basket else { + self.isBusy = false + return + } + + self.createPaymentMethod(with: params, for: basket) + self.isBusy = false + } + } + + public func log(_ message: String?, prependNewLine: Bool = true, appendNewLine: Bool = true) { + logViewModel.log(message, prependNewLine: prependNewLine, appendNewLine: appendNewLine) + } + + public func logError(error: Error?) { + logViewModel.logError(error: error) + } + + func settingsChanged(settings: TestHarnessSettingsProtocol) { + logSettings() + _apiClient = OloApiClient.createFromSettings() + delegate?.settingsChanged(settings: settings) + } + + // Error message handler used to display custom error messages when the custom error messages setting is turned on + @objc public func customErrorMessagehandler(_ cardState: NSDictionary, _ cardBrand: OPCardBrand, _ ignoreUneditedFieldErrors: Bool) -> String { + var errorMessage: String? = nil + + let state = cardState as! Dictionary + + let numberState = state[.number]! + let ignoreNumber = ignoreUneditedFieldErrors && (!numberState.wasEdited || !numberState.wasFirstResponder) + + let expirationState = state[.expiration]! + let ignoreExpiration = ignoreUneditedFieldErrors && (!expirationState.wasEdited || !expirationState.wasFirstResponder) + + let cvvState = state[.cvv]! + let ignoreCvv = ignoreUneditedFieldErrors && (!cvvState.wasEdited || !cvvState.wasFirstResponder) + + let postalCodeState = state[.postalCode]! + let ignorePostalCode = ignoreUneditedFieldErrors && (!postalCodeState.wasEdited || !postalCodeState.wasFirstResponder) + + if !numberState.isValid && !ignoreNumber { + if numberState.isEmpty { + errorMessage = OPStrings.emptyCardNumberError + } else if cardBrand == OPCardBrand.unsupported { + errorMessage = OPStrings.unsupportedCardError + } else { + errorMessage = OPStrings.invalidCardNumberError + } + } else if !expirationState.isValid && !ignoreExpiration { + errorMessage = expirationState.isEmpty ? + OPStrings.emptyExpirationError : + OPStrings.invalidExpirationError + } else if !cvvState.isValid && !ignoreCvv { + errorMessage = cvvState.isEmpty ? + OPStrings.emptyCvvError : + OPStrings.invalidCvvError + } else if !postalCodeState.isValid && !ignorePostalCode { + errorMessage = postalCodeState.isEmpty ? + OPStrings.emptyPostalCodeError : + OPStrings.invalidPostalCodeError + } + + return errorMessage == nil ? "" : "Custom: \(errorMessage!)" + } + + private func createPaymentMethod(with params: OPPaymentMethodParamsProtocol, for basket: Basket? = nil) { + log("Creating payment method...", appendNewLine: false) + + _oloPayApi.createPaymentMethod(with: params) { paymentMethod, error in + self.logViewModel.logPaymentMethod(paymentMethod: paymentMethod) + self.logViewModel.logError(error: error) + + guard let basket = basket, let paymentMethod = paymentMethod else { + self.isBusy = false + return + } + + self.submitBasket(basket: basket, paymentMethod: paymentMethod) + } + } + + private func createBasket(completion: @escaping (_: Basket?) -> Void) { + guard let apiClient = _apiClient else { + log("Unable to complete payment... apiClient is nil") + completion(nil) + return + } + + log("Creating Basket For Card...", appendNewLine: false) + + apiClient.createBasketWithProductFromSettings() { basket, error, message in + guard let basket = basket else { + self.logViewModel.logError(error: error) + self.log(message) + completion(nil) + return + } + + self.log("Basket Created: \(String(describing: basket))") + completion(basket) + } + } + + private func submitBasket(basket: Basket, paymentMethod: OPPaymentMethodProtocol) { + guard let apiClient = _apiClient else { + log("Unable to complete payment... apiClient is nil") + isBusy = false + return + } + + log("Submitting order...", appendNewLine: false) + apiClient.submitBasketFromSettings(with: paymentMethod, basketId: basket.id) { order, error, message in + self.log(message) + self.logViewModel.logError(error: error) + + guard let order = order else { + self.isBusy = false + return + } + + self.log("Order created: \(order.id)") + self.isBusy = false + } + } + + private func logSettings() { + log(self.newSettingsHeader, appendNewLine: false) + + let useSingleLinePayment = _settings.useSingleLinePayment + log("Payment Type: \(useSingleLinePayment ? "Single-Line" : "Multi-Line")", appendNewLine: false) + + if (useSingleLinePayment) { + log("Log card details: \(_settings.logCardInputChanges)", appendNewLine: false) + log("Display card errors: \(_settings.displayCardErrors)", appendNewLine: false) + log("Use custom card errors: \(_settings.customCardErrorMessages)", appendNewLine: false) + log("Display postal code: \(_settings.displayPostalCode)", appendNewLine: false) + } else { + log("Log form valid changes: \(_settings.logFormValidChanges)", appendNewLine: false) + } + + let useOrderingApi = _settings.completeOloPayPayment + log("Create Basket & Complete Payment: \(useOrderingApi)", appendNewLine: false) + + if (useOrderingApi) { + log("API URL: \(_settings.baseAPIUrl ?? "")", appendNewLine: false) + log("Restaurant Id: \(String(describing: _settings.restaurantId))", appendNewLine: false) + log("Product Id: \(String(describing: _settings.productId))", appendNewLine: false) + log("Product Qty: \(String(describing: _settings.productQty))", appendNewLine: false) + log("Email: \(String(describing: _settings.userEmail))", appendNewLine: false) + } + + log("") + } + + @objc func paymentCardDetailsViewDidChange(with fieldStates: NSDictionary, isValid: Bool) { + guard _settings.logCardInputChanges else { + return + } + + log("CardDetails Changed: IsValid: \(isValid)") + } + + @objc func paymentCardDetailsViewDidBeginEditing(with fieldStates: NSDictionary, isValid: Bool) { + guard _settings.logCardInputChanges else { + return + } + + log("CardDetails Begin Editing: CardValid: \(isValid)") + } + + @objc func paymentCardDetailsViewDidEndEditing(with fieldStates: NSDictionary, isValid: Bool) { + guard _settings.logCardInputChanges else { + return + } + + log("CardDetails End Editing: CardValid: \(isValid)") + } + + @objc func paymentCardDetailsViewFieldDidBeginEditing(with fieldStates: NSDictionary, field: OPCardField, isValid: Bool) { + guard _settings.logCardInputChanges else { + return + } + + log("Begin Editing: \(String(describing: field)) - CardValid: \(isValid)") + } + + @objc func paymentCardDetailsViewFieldDidEndEditing(with fieldStates: NSDictionary, field: OPCardField, isValid: Bool) { + guard _settings.logCardInputChanges else { + return + } + + log("End Editing: \(String(describing: field)) - CardValid: \(isValid)") + } + + @objc func isValidChanged(_ isValid: Bool) { + guard _settings.logFormValidChanges else { + return + } + + log("Form Is Valid: \(isValid)") + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CvvTokenViewModel.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CvvTokenViewModel.swift new file mode 100644 index 0000000..69faab7 --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/CvvTokenViewModel.swift @@ -0,0 +1,203 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// CvvTokenViewModel.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/15/23. +// + +import Foundation +import OloPaySDK + +protocol CvvTokenViewModelDelegate: NSObjectProtocol { + func isBusyChanged(busy: Bool) + func settingsChanged(settings: TestHarnessSettingsProtocol) +} + +class CvvTokenViewModel: NSObject, OPPaymentCardCvvViewDelegate, TestHarnessSettingsObserver { + private let newSettingsHeader = "--------------- NEW SETTINGS ---------------" + + private let _oloPayApi: OloPayAPIProtocol + private let _settings: TestHarnessSettings + private var _apiClient: OloApiClient? + + public var delegate: CvvTokenViewModelDelegate? = nil + private(set) var logViewModel: LogViewModel + + private(set) var isBusy: Bool { + didSet { + delegate?.isBusyChanged(busy: self.isBusy) + } + } + + public var allSettings: TestHarnessSettingsProtocol { + get { _settings.allSettings } + } + + required init(logViewModel: LogViewModel, settings: TestHarnessSettings, oloPayApi: OloPayAPIProtocol) { + _oloPayApi = oloPayApi + _settings = settings + _apiClient = OloApiClient.createFromSettings() + self.logViewModel = logViewModel + isBusy = false + + super.init() + + settings.addObserver(self) + } + + func createToken(params: OPCvvTokenParamsProtocol) { + isBusy = true + + guard _settings.completeOloPayPayment else { + createToken(with: params) + isBusy = false + return + } + + guard _settings.useLoggedInUser else { + createToken(with: params) + log("Warning: CVV token orders can only be completed with a logged in user. Creating CVV token without completing order.") + isBusy = false + return + } + + createBasket() { basket in + guard let basket = basket else { + self.isBusy = false + return + } + + self.createToken(with: params, for: basket) + self.isBusy = false + } + } + + public func log(_ message: String?, prependNewLine: Bool = true, appendNewLine: Bool = true) { + logViewModel.log(message, prependNewLine: prependNewLine, appendNewLine: appendNewLine) + } + + @objc public func customErrorMessageHandler(_ fieldState: OPCardFieldStateProtocol, _ ignoreUneditedFieldErrors: Bool) -> String { + if (fieldState.isValid) { + return "" + } + + if ignoreUneditedFieldErrors && (!fieldState.wasEdited || !fieldState.wasFirstResponder) { + return "" + } + + let errorMessage = fieldState.isEmpty ? OPStrings.emptyCvvError : OPStrings.incompleteCvvError + + return "Custom: \(errorMessage)" + } + + func settingsChanged(settings: TestHarnessSettingsProtocol) { + logSettings() + _apiClient = OloApiClient.createFromSettings() + delegate?.settingsChanged(settings: settings) + } + + + @objc func fieldChanged(with state: OPCardFieldStateProtocol) { + guard _settings.logCvvInputChanges else { + return + } + + log("CVV Input Changed: \(String(describing: state))", appendNewLine: true) + } + + + @objc func didBeginEditing(with state: OPCardFieldStateProtocol) { + guard _settings.logCvvInputChanges else { + return + } + + log("CVV Begin Editing: \(String(describing: state))", appendNewLine: true) + } + + @objc func didEndEditing(with state: OPCardFieldStateProtocol) { + guard _settings.logCvvInputChanges else { + return + } + + log("CVV End Editing: \(String(describing: state))", appendNewLine: true) + } + + @objc func validStateChanged(with state: OPCardFieldStateProtocol) { + guard _settings.logCvvInputChanges else { + return + } + + log("IsValid Changed: \(state.isValid)") + } + + private func logSettings() { + log(self.newSettingsHeader, appendNewLine: false) + + log("Log CVV Input Changes: \(_settings.logCvvInputChanges)", appendNewLine: false) + log("Display CVV errors: \(_settings.displayCvvErrors)", appendNewLine: false) + log("Use custom CVV errors: \(_settings.customCvvErrorMessages)", appendNewLine: true) + } + + private func createToken(with params: OPCvvTokenParamsProtocol, for basket: Basket? = nil) { + log("Creating CVV Token...", appendNewLine: false) + + _oloPayApi.createCvvUpdateToken(with: params) { token, error in + self.logViewModel.logCvvToken(token: token) + self.logViewModel.logError(error: error) + + guard let basket = basket, let token = token else { + self.isBusy = false + return + } + + self.submitBasket(basket: basket, token: token) + } + } + + private func createBasket(completion: @escaping (_: Basket?) -> Void) { + guard let apiClient = _apiClient else { + log("Unable to complete payment... apiClient is nil") + completion(nil) + return + } + + log("Creating Basket For CVV Token...", appendNewLine: false) + + apiClient.createBasketWithProductFromSettings() { basket, error, message in + guard let basket = basket else { + self.logViewModel.logError(error: error) + self.log(message) + completion(nil) + return + } + + self.log("Basket Created: \(String(describing: basket))") + completion(basket) + } + } + + private func submitBasket(basket: Basket, token: OPCvvUpdateTokenProtocol) { + guard let apiClient = _apiClient else { + log("Unable to complete payment... apiClient is nil") + isBusy = false + return + } + + log("Submitting order...", appendNewLine: false) + apiClient.submitBasketFromSettings(with: token, basketId: basket.id) { order, error, message in + self.log(message) + self.logViewModel.logError(error: error) + + guard let order = order else { + self.isBusy = false + self.log("Order not created") + return + } + + self.log("Order created: \(order.id)") + self.isBusy = false + } + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/LogViewModel.swift b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/LogViewModel.swift new file mode 100644 index 0000000..48f5c5a --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/ViewModels/LogViewModel.swift @@ -0,0 +1,89 @@ +// Copyright © 2022 Olo Inc. All rights reserved. +// This software is made available under the Olo Pay SDK License (See LICENSE.md file) +// +// LogViewModel.swift +// OloPaySDKTestHarness +// +// Created by Justin Anderson on 8/15/23. +// + +import Foundation +import OloPaySDK + +public protocol LogViewModelDelegate: NSObjectProtocol { + func logTextChanged(_ logText: String) +} + +public protocol LogViewModelProtocol: NSObjectProtocol { + func clearLog() + func log(_ message : String?, prependNewLine: Bool, appendNewLine: Bool) + func logError(error: Error?) + func logPaymentMethod(paymentMethod: OPPaymentMethodProtocol?) +} + +class LogViewModel: NSObject, LogViewModelProtocol { + private var _logText: String = "" + + weak var delegate: LogViewModelDelegate? + + public func clearLog() { + _logText = "" + delegate?.logTextChanged(_logText) + } + + public func log(_ message : String?, prependNewLine: Bool = true, appendNewLine: Bool = true) { + if (prependNewLine) { + self._logText += "\n" + } + + if let unwrappedMessage = message { + self._logText += unwrappedMessage + } + + if (appendNewLine) { + self._logText += "\n" + } + + delegate?.logTextChanged(_logText) + } + + func logError(error: Error?) { + guard let unwrappedError = error else { + return + } + + self.log(String(describing: unwrappedError as NSError)) + + if let opError = unwrappedError as? OPError { + self.log("OP Error Details:", appendNewLine: false) + self.log("Error Type: \(opError.errorType)", appendNewLine: false) + + if let errorType = opError.cardErrorType { + self.log("Card Error Type: \(errorType)", appendNewLine: false) + } else { + self.log("Card Error Type: nil", appendNewLine: false) + } + + self.log("Card Error Message: \(opError.cardErrorMessage ?? "nil")") + } + } + + func logPaymentMethod(paymentMethod: OPPaymentMethodProtocol?) { + guard let unwrappedPaymentMethod = paymentMethod else { + self.log("Payment method not created") + return + } + + self.log(String(describing: unwrappedPaymentMethod)) + } + + func logCvvToken(token: OPCvvUpdateTokenProtocol?) { + guard let unwrappedToken = token else { + self.log("CVV token not created") + return + } + + self.log("CVV Token Created") + self.log(String(describing: unwrappedToken)) + } +} diff --git a/src/TestHarness/iOS/OloPaySDKTestHarness/exportOptions.plist b/src/TestHarness/iOS/OloPaySDKTestHarness/exportOptions.plist new file mode 100644 index 0000000..cd95f6e --- /dev/null +++ b/src/TestHarness/iOS/OloPaySDKTestHarness/exportOptions.plist @@ -0,0 +1,15 @@ + + + + + provisioningProfiles + + com.olo.OloPaySDKTestHarness + 05644f28-d041-464e-9aca-4875ef2716fa + + method + ad-hoc + destination + export + +