├── Sources
├── PayKitUI
│ ├── Shared
│ │ ├── Assets
│ │ │ └── Resources
│ │ │ │ ├── en.lproj
│ │ │ │ └── Localizable.strings
│ │ │ │ ├── Colors.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── PolyChrome.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── TextPrimary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── TextSecondary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── TextTernary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── ButtonTextPrimary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── SurfacePrimary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── SurfaceSecondary.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── SurfacePrimaryDisabled.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Images.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── MonoChromeLogo.imageset
│ │ │ │ └── Contents.json
│ │ │ │ ├── PolyChromeLogo.imageset
│ │ │ │ └── Contents.json
│ │ │ │ └── MonoChromeLogoReverse.imageset
│ │ │ │ └── Contents.json
│ │ ├── Style
│ │ │ └── SizingCategory.swift
│ │ └── Generated
│ │ │ ├── Strings.generated.swift
│ │ │ └── Assets.swift
│ ├── PrivacyInfo.xcprivacy
│ ├── swiftgen.yml
│ ├── UIKit
│ │ ├── Extensions
│ │ │ └── UIColor+SwiftUI.swift
│ │ ├── CashAppPayButton.swift
│ │ └── CashAppPaymentMethod.swift
│ └── SwiftUI
│ │ └── CashAppPaymentMethodView.swift
└── PayKit
│ ├── PrivacyInfo.xcprivacy
│ ├── Services
│ ├── Logger
│ │ └── Log.swift
│ ├── Networking
│ │ ├── HTTPRequest.swift
│ │ ├── RetryPolicy.swift
│ │ ├── RESTService.swift
│ │ └── UserAgent.swift
│ └── Analytics
│ │ ├── CashAppPayEndpoint+Analytics.swift
│ │ ├── JSONEncoder+Analytics.swift
│ │ ├── AnalyticsDataSource.swift
│ │ ├── AnalyticsClient.swift
│ │ └── AnalyticsService.swift
│ ├── CustomerRequest+Extensions.swift
│ ├── JSONCoder+PayKit.swift
│ ├── Errors.swift
│ ├── ObjcWrapper
│ └── ObjCWrapper.swift
│ └── CashAppPay.swift
├── Gemfile
├── Tests
├── PayKitTests
│ ├── Resources
│ │ └── Fixtures
│ │ │ ├── Errors
│ │ │ ├── emptyErrorArray.json
│ │ │ ├── invalidJSON.json
│ │ │ ├── brandNotFound.json
│ │ │ ├── internalServerError.json
│ │ │ ├── idempotencyKeyReused.json
│ │ │ └── unauthorized.json
│ │ │ ├── updateRequestParams-clearAllButActions.json
│ │ │ ├── updateRequestParams-clearAmountFromOneTime.json
│ │ │ ├── createRequestParams-fullyPopulated.json
│ │ │ ├── pendingRequest-fullyPopulated.json
│ │ │ └── approvedRequest-fullyPopulated.json
│ ├── DateFormatterTests.swift
│ ├── Helpers
│ │ ├── MockNotificationCenter.swift
│ │ ├── MockRestService.swift
│ │ └── MockURLProtocol.swift
│ ├── UnexpectedErrorTests.swift
│ ├── CashAppPay+EndpointTest.swift
│ ├── UserAgentTests.swift
│ ├── APIErrorTests.swift
│ ├── RetryPolicyTests.swift
│ ├── CustomerRequest+ExtensionsTests.swift
│ ├── AnalyticsDataSourceTests.swift
│ ├── AnalyticsClientTests.swift
│ ├── UpdateCustomerRequestParamsTests.swift
│ ├── CreateCustomerRequestParamsTests.swift
│ ├── CustomerRequestTests.swift
│ ├── IntegrationErrorTests.swift
│ ├── ResilientRestServiceTests.swift
│ ├── AccessModifierTests.swift
│ ├── Errors+ObjCTests.swift
│ └── LoggableTests.swift
└── PayKitUITests
│ ├── __Snapshots__
│ ├── CashAppPayButtonSnapshotTests
│ │ ├── test_dark_mode.1.png
│ │ ├── test_large_button.1.png
│ │ ├── test_small_button.1.png
│ │ ├── test_button_expands.1.png
│ │ ├── test_button_expands.2.png
│ │ ├── test_button_disabled.1.png
│ │ └── test_button_disabled.2.png
│ ├── CashAppPayButtonViewSnapshotTests
│ │ ├── test_dark_mode.1.png
│ │ ├── test_large_button.1.png
│ │ ├── test_minimum_size.1.png
│ │ ├── test_minimum_size.2.png
│ │ ├── test_small_button.1.png
│ │ ├── test_button_disabled.1.png
│ │ └── test_button_disabled.2.png
│ ├── CashAppPaymentMethodSnapshotTests
│ │ ├── test_dark_mode.1.png
│ │ ├── test_expands_full_width.1.png
│ │ ├── test_text_custom_font.1.png
│ │ ├── test_large_payment_method.1.png
│ │ └── test_small_payment_method.1.png
│ └── CashAppPaymentMethodViewSnapshotTests
│ │ ├── test_dark_mode.1.png
│ │ ├── test_large_button.1.png
│ │ ├── test_minimum_size.1.png
│ │ ├── test_small_button.1.png
│ │ └── test_text_custom_font.1.png
│ ├── CashAppPaymentMethodViewSnapshotTests.swift
│ ├── CashAppPayButtonSnapshotTests.swift
│ ├── CashAppPayButtonViewSnapshotTests.swift
│ ├── CashAppPaymentMethodSnapshotTests.swift
│ └── BaseSnapshotTestCase.swift
├── Demo
├── PayKitDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── TabBarController.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── AppDelegate.swift
│ └── ComponentsViewController.swift
└── PayKitDemo.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── PayKitDemo.xcscheme
├── THIRD_PARTY_LICENSE.txt
├── PayKit.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── IDETemplateMacros.plist
├── .github
├── CODEOWNERS
├── workflows
│ ├── codelint.yml
│ └── ci.yml
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CashAppPayKit.podspec
├── CashAppPayKitUI.podspec
├── Contributing.md
├── .swiftlint.yml
├── Package.swift
├── Gemfile.lock
└── RELEASE-NOTES.md
/Sources/PayKitUI/Shared/Assets/Resources/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org' do
2 | gem 'cocoapods', '1.11.3'
3 | end
4 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/emptyErrorArray.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors":
3 | [
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/THIRD_PARTY_LICENSE.txt:
--------------------------------------------------------------------------------
1 | 3RD PARTY LICENSES
2 |
3 | IOS is a trademark or registered trademark of Cisco in the U.S. and other countries and is used under license.
4 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/invalidJSON.json:
--------------------------------------------------------------------------------
1 | {
2 | "textbook": "Invalid JSON Error",
3 | "seems_like" : [
4 | "someone",
5 | "should have",
6 | "caught this"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PayKit.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_dark_mode.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_dark_mode.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_large_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_large_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_small_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_small_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_expands.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_expands.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_expands.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_expands.2.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_dark_mode.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_dark_mode.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_dark_mode.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_dark_mode.1.png
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_disabled.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_disabled.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_disabled.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonSnapshotTests/test_button_disabled.2.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_large_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_large_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_minimum_size.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_minimum_size.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_minimum_size.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_minimum_size.2.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_small_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_small_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_dark_mode.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_dark_mode.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_button_disabled.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_button_disabled.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_button_disabled.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPayButtonViewSnapshotTests/test_button_disabled.2.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_expands_full_width.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_expands_full_width.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_text_custom_font.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_text_custom_font.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_large_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_large_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_minimum_size.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_minimum_size.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_small_button.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_small_button.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_large_payment_method.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_large_payment_method.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_small_payment_method.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodSnapshotTests/test_small_payment_method.1.png
--------------------------------------------------------------------------------
/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_text_custom_font.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-ios-sdk/HEAD/Tests/PayKitUITests/__Snapshots__/CashAppPaymentMethodViewSnapshotTests/test_text_custom_font.1.png
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/brandNotFound.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors":
3 | [
4 | {
5 | "category": "INVALID_REQUEST_ERROR",
6 | "code": "BRAND_NOT_FOUND",
7 | "detail": "The requested brand could not be found."
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/internalServerError.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors":
3 | [
4 | {
5 | "category": "API_ERROR",
6 | "code": "INTERNAL_SERVER_ERROR",
7 | "detail": "An internal server error has occurred."
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/idempotencyKeyReused.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors":
3 | [
4 | {
5 | "category": "INVALID_REQUEST_ERROR",
6 | "code": "IDEMPOTENCY_KEY_REUSED",
7 | "detail": "Idempotency key already in use, request body checksum does not match"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/swiftgen.yml:
--------------------------------------------------------------------------------
1 | input_dir: Shared/Assets/Resources
2 | output_dir: Shared/Generated
3 |
4 | strings:
5 | inputs:
6 | - en.lproj/Localizable.strings
7 | outputs:
8 | - templateName: structured-swift5
9 | output: Strings.generated.swift
10 | xcassets:
11 | inputs:
12 | - Colors.xcassets
13 | - Images.xcassets
14 | outputs:
15 | templatePath: custom-xcassets-template.stencil
16 | output: Assets.swift
17 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/updateRequestParams-clearAllButActions.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "actions": [
4 | {
5 | "type": "ON_FILE_PAYMENT",
6 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
7 | "account_reference_id": null
8 | }
9 | ],
10 | "metadata": null,
11 | "reference_id": null
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/Errors/unauthorized.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors":
3 | [
4 | {
5 | "category": "AUTHENTICATION_ERROR",
6 | "code": "UNAUTHORIZED",
7 | "detail": "The request specified a client ID of 'FAKE_CLIENT_ID', but a client with that ID does not exist. Please ensure the client ID being passed is correct and contact Cash App API support if you are unable to resolve this issue."
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/updateRequestParams-clearAmountFromOneTime.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "actions": [
4 | {
5 | "type": "ONE_TIME_PAYMENT",
6 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
7 | "amount": null,
8 | "currency": null
9 | }
10 | ],
11 | "metadata": null,
12 | "reference_id": null
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # CODEOWNERS syntax https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
2 | #
3 | # Code owners are automatically requested for review when someone opens a pull request that modifies code that they own.
4 | # Code owners are not automatically requested to review draft pull requests
5 | # When you mark a draft pull request as ready for review, code owners are automatically notified.
6 |
7 | * @squareup/cash-commerce-ios
8 |
9 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Images.xcassets/MonoChromeLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "MonoChromeDark.svg",
5 | "idiom" : "universal"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "filename" : "MonoChromeLight.svg",
15 | "idiom" : "universal"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Images.xcassets/PolyChromeLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "PolyChromeDark.svg",
5 | "idiom" : "universal"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "filename" : "PolyChromeLight.svg",
15 | "idiom" : "universal"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Images.xcassets/MonoChromeLogoReverse.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "MonoChromeLight.svg",
5 | "idiom" : "universal"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "filename" : "MonoChromeDark.svg",
15 | "idiom" : "universal"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/codelint.yml:
--------------------------------------------------------------------------------
1 | name: Codelint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | paths:
9 | - '.github/workflows/codelint.yml'
10 | - '.swiftlint.yml'
11 | - '**/*.swift'
12 | jobs:
13 | swiftlint:
14 | runs-on: ubuntu-latest
15 | container:
16 | image: ghcr.io/realm/swiftlint:0.47.1
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | with:
21 | fetch-depth: 1
22 |
23 | - name: SwiftLint
24 | run: |
25 | swiftlint --reporter github-actions-logging
26 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Standards checklist:
2 |
3 |
4 |
5 | - [ ] The PR title is descriptive.
6 | - [ ] The PR doesn't replicate another PR which is already open.
7 | - [ ] I have read the contribution guide and followed all the instructions.
8 | - [ ] The code is mine or it's from somewhere with an Apache 2.0 compatible license.
9 | - [ ] The code is efficient, to the best of my ability, and does not waste computer resources.
10 | - [ ] The code is stable and I have tested it myself, to the best of my abilities.
11 |
12 | ## Tests:
13 |
14 | - [ ] PayKit Tests
15 | - [ ] PayKitUI Tests
16 |
17 | ## Changes:
18 |
19 | - [...]
20 |
21 | ## Other comments:
22 |
23 | ...
24 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/createRequestParams-fullyPopulated.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "actions": [
4 | {
5 | "type": "ON_FILE_PAYMENT",
6 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
7 | "account_reference_id": "account4"
8 | }
9 | ],
10 | "metadata": {
11 | "key1": "Valuation",
12 | "key2": "ValuWorld",
13 | "key3": "Valuminous"
14 | },
15 | "channel": "IN_APP",
16 | "redirect_url": "paykitdemo://callback",
17 | "reference_id": "refer_to_me"
18 | },
19 | "idempotency_key": "e345c3fb-1caa-46fd-b0d3-aa6c7b00ab19"
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/PayKit/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyCollectedDataTypes
6 |
7 |
8 | NSPrivacyCollectedDataType
9 | NSPrivacyCollectedDataTypeUserID
10 | NSPrivacyCollectedDataTypeLinked
11 |
12 | NSPrivacyCollectedDataTypeTracking
13 |
14 | NSPrivacyCollectedDataTypePurposes
15 |
16 | NSPrivacyCollectedDataTypePurposeAnalytics
17 |
18 |
19 |
20 | NSPrivacyTracking
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Logger/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | struct Log {
20 | static func write(_ message: String) {
21 | #if LOGGING
22 | print(message)
23 | #endif
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/PolyChrome.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x00",
9 | "green" : "0x00",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x4F",
27 | "green" : "0xD6",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/TextPrimary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x00",
9 | "green" : "0x00",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFF",
27 | "green" : "0xFF",
28 | "red" : "0xFF"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/TextSecondary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x66",
9 | "green" : "0x66",
10 | "red" : "0x66"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "102",
27 | "green" : "102",
28 | "red" : "102"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/TextTernary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x99",
9 | "green" : "0x99",
10 | "red" : "0x99"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFA",
27 | "green" : "0xFA",
28 | "red" : "0xFA"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/ButtonTextPrimary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0xFF",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x00",
27 | "green" : "0x00",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/SurfacePrimary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x00",
9 | "green" : "0x00",
10 | "red" : "0x00"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFF",
27 | "green" : "0xFF",
28 | "red" : "0xFF"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/SurfaceSecondary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0xFF",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x00",
27 | "green" : "0x00",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Style/SizingCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SizingCategory.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | /// The styles defined for particular sizing
20 | public enum SizingCategory {
21 | // Preferred for small screen sizes
22 | case small
23 | // Preferred for large screen sizes
24 | case large
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | .DS_Store
3 | build/
4 | .build/
5 | .swiftpm/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 | *.moved-aside
16 | DerivedData
17 | .idea/
18 | *.gcno
19 | *.gcda
20 | *.iml
21 | Package.resolved
22 |
23 | # Gradle
24 | .gradle
25 | local.properties
26 |
27 | Code/SquareShared/build
28 |
29 | # Cocoapods
30 | Pods
31 | Podfile.local
32 |
33 | !Pods/Pods.xcodeproj/xcshareddata/xcschemes/CommonCashUI-Unit-SnapshotTests.xcscheme
34 |
35 | # cocoapods-dependencies
36 | Podfile.gv
37 | Podfile.png
38 |
39 | # cocoapods-generate
40 | gen
41 |
42 | # Fastlane
43 | Fastlane/.env
44 | Fastlane/Preview.html
45 | Fastlane/README.md
46 | Fastlane/report.xml
47 | Fastlane/bin
48 |
49 | # ios-builder
50 | .bin
51 | .bundle
52 |
53 | # Bazel
54 | bazel-*
55 | user.bazelrc
56 | Code/*/BUILD.bazel
57 | Pods/*/BUILD.bazel
--------------------------------------------------------------------------------
/PayKit.xcworkspace/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___FILENAME___
8 | //
9 | // Licensed under the Apache License, Version 2.0 (the "License");
10 | // you may not use this file except in compliance with the License.
11 | // You may obtain a copy of the License at
12 | //
13 | // http://www.apache.org/licenses/LICENSE-2.0
14 | //
15 | // Unless required by applicable law or agreed to in writing, software
16 | // distributed under the License is distributed on an "AS IS" BASIS,
17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | // See the License for the specific language governing permissions and
19 | // limitations under the License.
20 | //
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Assets/Resources/Colors.xcassets/SurfacePrimaryDisabled.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x99",
9 | "green" : "0x99",
10 | "red" : "0x99"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x66",
27 | "green" : "0x66",
28 | "red" : "0x66"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/UIKit/Extensions/UIColor+SwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+SwiftUI.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import SwiftUI
18 |
19 | @available(iOS 13.0, *)
20 | extension UIColor {
21 | var swiftUIColor: Color? {
22 | guard let rgb = cgColor.components, rgb.count >= 3 else {
23 | return nil
24 | }
25 | return Color(red: rgb[0], green: rgb[1], blue: rgb[2])
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/PayKit/CustomerRequest+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | extension CustomerRequest.AuthFlowTriggers {
20 | // Jitter to account for the requests that are about to expire
21 | private static let expiryJitter: TimeInterval = 10
22 |
23 | func isExpired(on date: Date = Date()) -> Bool {
24 | refreshesAt <= date.addingTimeInterval(CustomerRequest.AuthFlowTriggers.expiryJitter)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/CashAppPayKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'CashAppPayKit'
3 | s.version = '0.6.3'
4 | s.summary = 'PayKit iOS SDK'
5 | s.homepage = 'https://github.com/cashapp/cash-app-pay-ios-sdk'
6 | s.license = 'Apache License, Version 2.0'
7 | s.author = 'Cash App'
8 | s.source = { :git => 'https://github.com/cashapp/cash-app-pay-ios-sdk.git', :tag => "v#{s.version}" }
9 | s.module_name = 'PayKit'
10 |
11 | s.swift_version = ['5.0']
12 | s.ios.deployment_target = '12.0'
13 |
14 | s.source_files = 'Sources/PayKit/**/*.swift'
15 |
16 | s.test_spec 'Tests' do |test_spec|
17 | test_spec.source_files = 'Tests/PayKitTests/**/*.swift'
18 | test_spec.framework = 'XCTest'
19 | test_spec.library = 'swiftos'
20 | test_spec.requires_app_host = true
21 | test_spec.resources = 'Tests/PayKitTests/Resources/Fixtures/**/*.json'
22 |
23 | test_spec.resource_bundles = {
24 | "PayKitTestFixtures" => [
25 | "Tests/PayKitTests/Resources/Fixtures/**/*.json"
26 | ]
27 | }
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/CashAppPayKitUI.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'CashAppPayKitUI'
3 | s.version = "0.6.3"
4 | s.summary = 'UI components for the PayKit iOS SDK'
5 | s.homepage = 'https://github.com/cashapp/cash-app-pay-ios-sdk'
6 | s.license = 'Apache License, Version 2.0'
7 | s.author = 'Cash App'
8 | s.source = { :git => 'https://github.com/cashapp/cash-app-pay-ios-sdk.git', :tag => "v#{s.version}" }
9 | s.module_name = 'PayKitUI'
10 |
11 | ios_deployment_target = '12.0'
12 |
13 | s.swift_version = ['5.0']
14 | s.ios.deployment_target = ios_deployment_target
15 |
16 | s.resources = "Sources/PayKitUI/Shared/Assets/Resources/**/**.xcassets"
17 | s.source_files = 'Sources/PayKitUI/**/**.swift'
18 |
19 | s.test_spec 'Tests' do |test_spec|
20 | test_spec.dependency 'SnapshotTesting', '~> 1.9'
21 | test_spec.requires_app_host = true
22 | test_spec.source_files = 'Tests/PayKitUITests/**/*.swift'
23 |
24 | test_spec.ios.resource_bundle = {
25 | 'SnapshotTestImages' => 'Tests/PayKitUITests/*'
26 | }
27 | end
28 | end
29 |
30 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLName
11 | app.cash.paykitdemo
12 | CFBundleURLSchemes
13 |
14 | paykitdemo
15 |
16 |
17 |
18 | UIApplicationSceneManifest
19 |
20 | UIApplicationSupportsMultipleScenes
21 |
22 | UISceneConfigurations
23 |
24 | UIWindowSceneSessionRoleApplication
25 |
26 |
27 | UISceneConfigurationName
28 | Default Configuration
29 | UISceneDelegateClassName
30 | $(PRODUCT_MODULE_NAME).SceneDelegate
31 | UISceneStoryboardFile
32 | Main
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Networking/HTTPRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPRequest.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | struct HTTPRequest {
20 | let urlRequest: URLRequest
21 | let retryPolicy: RetryPolicy?
22 | let handler: (Data?, URLResponse?, Error?) -> Void
23 |
24 | init(
25 | urlRequest: URLRequest,
26 | retryPolicy: RetryPolicy?,
27 | handler: @escaping (Data?, URLResponse?, Error?) -> Void) {
28 | self.urlRequest = urlRequest
29 | self.retryPolicy = retryPolicy
30 | self.handler = handler
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | ## Sign the CLA
2 |
3 | All contributors to your PR must sign our [Individual Contributor License Agreement (CLA)][1]. The CLA is a short form that ensures that you are eligible to contribute.
4 |
5 | You only need to do this once, so if you've done this for another Square open source project, you're all set. If you are submitting a pull request for the first time, just let us know that you have completed the CLA (we'll look up your github user name).
6 |
7 | ## One issue or bug per Pull Request
8 |
9 | Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged.
10 |
11 | ## Issues before features
12 |
13 | If you want to add a feature, please file an Issue first. An Issue gives us the opportunity to discuss the requirements and implications of a feature with you before you start writing code.
14 |
15 | ## Backwards compatibility
16 |
17 | Existing installs of PayKit should continue to work after upgrading to a version that includes your changes. Some changes may need to include a migration plan.
18 |
19 | ## Forwards compatibility
20 |
21 | Please do not write new code using deprecated APIs.
22 |
23 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | pod-lint:
11 | name: Pod Lint
12 | runs-on: macos-13
13 | steps:
14 | - name: Checkout Repo
15 | uses: actions/checkout@v4
16 | - name: Bundle Install
17 | run: bundle install --gemfile=Gemfile
18 | - name: Lint PayKit Podspec
19 | run: bundle exec --gemfile=Gemfile pod lib lint --verbose --fail-fast CashAppPayKit.podspec
20 | - name: Lint PayKitUI Podspec
21 | run: bundle exec --gemfile=Gemfile pod lib lint --verbose --fail-fast CashAppPayKitUI.podspec
22 | - uses: actions/upload-artifact@v4
23 | if: failure()
24 | with:
25 | name: failed_snaphots
26 | path: /Users/runner/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*/tmp/
27 | spm:
28 | name: SPM Build
29 | runs-on: macos-latest
30 | steps:
31 | - name: Checkout Repo
32 | uses: actions/checkout@v4
33 | - name: Select Xcode Version (15.1.0)
34 | run: sudo xcode-select --switch /Applications/Xcode_15.1.0.app/Contents/Developer
35 | - name: Build
36 | run: xcodebuild build -scheme PayKit -sdk "`xcrun --sdk iphonesimulator --show-sdk-path`"
37 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Analytics/CashAppPayEndpoint+Analytics.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPayEndpoint+Analytics.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | extension CashAppPay.Endpoint {
20 | var analyticsEndpoint: AnalyticsClient.Endpoint {
21 | switch self {
22 | case .production, .sandbox:
23 | return .production
24 | case .staging:
25 | return .staging
26 | }
27 | }
28 |
29 | var analyticsField: String {
30 | switch self {
31 | case .production:
32 | return "production"
33 | case .sandbox:
34 | return "sandbox"
35 | case .staging:
36 | return "staging"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/DateFormatterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormatterTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class DateFormatterTests: XCTestCase {
21 | func test_formatter() throws {
22 | let date = try XCTUnwrap(
23 | DateComponents(
24 | calendar: Calendar(identifier: .gregorian),
25 | timeZone: .init(secondsFromGMT: 0),
26 | year: 2022,
27 | month: 4,
28 | day: 20,
29 | hour: 8,
30 | minute: 30,
31 | second: 45
32 | ).date
33 | )
34 | XCTAssertEqual(DateFormatter.payKitFormatter().string(from: date), "2022-04-20T08:30:45.000Z")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Helpers/MockNotificationCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockNotificationCenter.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | import UIKit
19 |
20 | class MockNotificationCenter: NotificationCenter {
21 | var addObserverStub: ((Any, Selector, NSNotification.Name?, Any?) -> Void)
22 |
23 | init(addObserverStub: @escaping (Any, Selector, NSNotification.Name?, Any?) -> Void) {
24 | self.addObserverStub = addObserverStub
25 | }
26 |
27 | override func addObserver(
28 | _ observer: Any,
29 | selector aSelector: Selector,
30 | name aName: NSNotification.Name?,
31 | object anObject: Any?
32 | ) {
33 | addObserverStub(observer, aSelector, aName, anObject)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/UnexpectedErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnexpectedErrorTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class UnexpectedErrorTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_deserialize_unexpectedError() throws {
26 | let fixtureJSON = try fixtureDataForFilename(idempotencyKeyReusedFilename, in: .errors)
27 | let errorWrapper = try jsonDecoder.decode(UnexpectedErrorWrapper.self, from: fixtureJSON)
28 | XCTAssertEqual(errorWrapper.errors.first, TestValues.idempotencyKeyReusedError)
29 | }
30 |
31 | let idempotencyKeyReusedFilename = "idempotencyKeyReused"
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Helpers/MockRestService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockRestService.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | @testable import PayKit
19 |
20 | class MockRestService: RESTService {
21 | var executeStub: (URLRequest, RetryPolicy?, @escaping ((Data?, URLResponse?, Error?) -> Void)) -> Void
22 |
23 | // swiftlint:disable:next line_length
24 | init(executeStub: @escaping (URLRequest, RetryPolicy?, @escaping (Data?, URLResponse?, Error?) -> Void) -> Void = { _, _, _ in }) {
25 | self.executeStub = executeStub
26 | }
27 |
28 | // swiftlint:disable:next line_length
29 | func execute(request: URLRequest, retryPolicy: RetryPolicy?, completion: @escaping ((Data?, URLResponse?, Error?) -> Void)) {
30 | executeStub(request, retryPolicy, completion)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Generated/Strings.generated.swift:
--------------------------------------------------------------------------------
1 | // swiftlint:disable all
2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
3 |
4 | import Foundation
5 |
6 | // swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references
7 |
8 | // MARK: - Strings
9 |
10 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
11 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
12 | internal enum L10n {
13 | }
14 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
15 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
16 |
17 | // MARK: - Implementation Details
18 |
19 | extension L10n {
20 | private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
21 | let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table)
22 | return String(format: format, locale: Locale.current, arguments: args)
23 | }
24 | }
25 |
26 | // swiftlint:disable convenience_type
27 | private final class BundleToken {
28 | static let bundle: Bundle = {
29 | #if SWIFT_PACKAGE
30 | return Bundle.module
31 | #else
32 | return Bundle(for: BundleToken.self)
33 | #endif
34 | }()
35 | }
36 | // swiftlint:enable convenience_type
37 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKit
18 | import UIKit
19 |
20 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
21 |
22 | var window: UIWindow?
23 |
24 | func scene(
25 | _ scene: UIScene,
26 | willConnectTo session: UISceneSession,
27 | options connectionOptions: UIScene.ConnectionOptions
28 | ) {
29 | guard (scene as? UIWindowScene) != nil else { return }
30 | }
31 |
32 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
33 | if let url = URLContexts.first?.url {
34 | NotificationCenter.default.post(
35 | name: CashAppPay.RedirectNotification,
36 | object: nil,
37 | userInfo: [UIApplication.LaunchOptionsKey.url: url]
38 | )
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/TabBarController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarController.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 |
19 | class TabBarController: UITabBarController, UITabBarControllerDelegate {
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 | view.backgroundColor = UIColor.systemGroupedBackground
23 |
24 | let payKitController = PayKitViewController()
25 | payKitController.tabBarItem = UITabBarItem(title: "PayKit", image: UIImage(systemName: "briefcase"), tag: 0)
26 |
27 | let componentController = ComponentsViewController(style: .grouped)
28 | componentController.tabBarItem = UITabBarItem(
29 | title: "Components",
30 | image: UIImage(systemName: "pencil.and.outline"),
31 | tag: 1
32 | )
33 |
34 | self.viewControllers = [
35 | payKitController,
36 | UINavigationController(rootViewController: componentController),
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Analytics/JSONEncoder+Analytics.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONEncoder+Analytics.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | // MARK: - JSONEncoder
20 |
21 | extension JSONEncoder {
22 | static func eventStream2Encoder() -> JSONEncoder {
23 | let encoder = JSONEncoder()
24 | encoder.keyEncodingStrategy = .convertToSnakeCase
25 | encoder.dateEncodingStrategy = .microsecondsSince1970
26 | encoder.outputFormatting = .sortedKeys
27 | return encoder
28 | }
29 | }
30 |
31 | extension JSONEncoder.DateEncodingStrategy {
32 | // Encode the data to ms rounding to remove fractions
33 | static let microsecondsSince1970 = custom { date, encoder in
34 | var container = encoder.singleValueContainer()
35 | try container.encode(date.microsecondsSince1970)
36 | }
37 | }
38 |
39 | extension Date {
40 | var microsecondsSince1970: UInt {
41 | UInt((timeIntervalSince1970 * 1_000_000).rounded())
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/PayKit/JSONCoder+PayKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONCoder+PayKit.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | // MARK: - Date Formatter
20 |
21 | extension DateFormatter {
22 | static func payKitFormatter() -> DateFormatter {
23 | let dateFormatter = DateFormatter()
24 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
25 | dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
26 | return dateFormatter
27 | }
28 | }
29 |
30 | // MARK: - JSON Encoder
31 |
32 | extension JSONEncoder {
33 | static func payKitEncoder() -> JSONEncoder {
34 | let encoder = JSONEncoder()
35 | encoder.keyEncodingStrategy = .convertToSnakeCase
36 | encoder.dateEncodingStrategy = .formatted(.payKitFormatter())
37 | return encoder
38 | }
39 | }
40 |
41 | // MARK: - JSON Decoder
42 |
43 | extension JSONDecoder {
44 | static func payKitDecoder() -> JSONDecoder {
45 | let decoder = JSONDecoder()
46 | decoder.keyDecodingStrategy = .convertFromSnakeCase
47 | decoder.dateDecodingStrategy = .formatted(.payKitFormatter())
48 | return decoder
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/pendingRequest-fullyPopulated.json:
--------------------------------------------------------------------------------
1 | {
2 | "request":
3 | {
4 | "id": "GRR_mg3saamyqdm29jj9pqjqkedm",
5 | "status": "PENDING",
6 | "actions":
7 | [
8 | {
9 | "type": "ON_FILE_PAYMENT",
10 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
11 | "account_reference_id": "account4"
12 | }
13 | ],
14 | "origin":
15 | {
16 | "type": "DIRECT"
17 | },
18 | "auth_flow_triggers":
19 | {
20 | "qr_code_image_url": "https://sandbox.api.cash.app/qr/sandbox/v1/GRR_mg3saamyqdm29jj9pqjqkedm-t61pfg?rounded=0&format=png",
21 | "qr_code_svg_url": "https://sandbox.api.cash.app/qr/sandbox/v1/GRR_mg3saamyqdm29jj9pqjqkedm-t61pfg?rounded=0&format=svg",
22 | "mobile_url": "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial",
23 | "refreshes_at": "2022-10-20T20:16:48.036Z"
24 | },
25 | "reference_id": "refer_to_me",
26 | "created_at": "2022-10-20T20:16:18.051Z",
27 | "updated_at": "2022-10-20T20:16:18.051Z",
28 | "expires_at": "2022-10-20T21:16:18.024Z",
29 | "requester_profile":
30 | {
31 | "name": "SDK Hacking: The Brand",
32 | "logo_url": "https://franklin-assets.s3.amazonaws.com/merchants/assets/v3/generic/m_category_shopping.png"
33 | },
34 | "metadata":
35 | {
36 | "key1": "Valuation",
37 | "key2": "ValuWorld",
38 | "key3": "Valuminous"
39 | },
40 | "redirect_url": "paykitdemo://callback",
41 | "channel": "IN_APP"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Analytics/AnalyticsDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsDataSource.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | class AnalyticsDataSource {
20 |
21 | // MARK: - Static Properties
22 |
23 | static let maxEventCount = 300
24 |
25 | // MARK: - Private Properties
26 |
27 | private var events: [AnalyticsEvent]
28 |
29 | // MARK: - Lifecycle
30 |
31 | init(events: [AnalyticsEvent] = []) {
32 | self.events = events
33 | }
34 |
35 | // MARK: - Public Properties
36 |
37 | var count: Int {
38 | events.count
39 | }
40 |
41 | // MARK: - Public
42 |
43 | func insert(event: AnalyticsEvent) {
44 | guard count < AnalyticsDataSource.maxEventCount else {
45 | Log.write("Too many Analytics Events dropping event id: \(event.id)")
46 | return
47 | }
48 | events.append(event)
49 | }
50 |
51 | func fetch(limit: Int) -> [AnalyticsEvent] {
52 | events.prefix(limit).map { $0 }
53 | }
54 |
55 | func remove(_ eventIds: [UUID]) {
56 | let eventIdSet = Set(eventIds)
57 | events.removeAll(where: { eventIdSet.contains($0.id) })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/CashAppPay+EndpointTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayKit+EndpointTest.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class CashAppPay_EndpointTest: XCTestCase {
21 | func test_endpoint_url() {
22 | XCTAssertEqual(CashAppPay.Endpoint.staging.baseURL.absoluteString, "https://api.cashstaging.app/")
23 | XCTAssertEqual(CashAppPay.Endpoint.sandbox.baseURL.absoluteString, "https://sandbox.api.cash.app/")
24 | XCTAssertEqual(CashAppPay.Endpoint.production.baseURL.absoluteString, "https://api.cash.app/")
25 | }
26 |
27 | func test_endpoint_analytics_endpoint() {
28 | XCTAssertEqual(CashAppPay.Endpoint.staging.analyticsEndpoint, .staging)
29 | XCTAssertEqual(CashAppPay.Endpoint.production.analyticsEndpoint, .production)
30 | XCTAssertEqual(CashAppPay.Endpoint.sandbox.analyticsEndpoint, .production)
31 | }
32 |
33 | func test_analytics_field() {
34 | XCTAssertEqual(CashAppPay.Endpoint.staging.analyticsField, "staging")
35 | XCTAssertEqual(CashAppPay.Endpoint.production.analyticsField, "production")
36 | XCTAssertEqual(CashAppPay.Endpoint.sandbox.analyticsField, "sandbox")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 |
2 | # For a list of available rules, see:
3 | # https://github.com/realm/SwiftLint/blob/master/Rules.md
4 |
5 | disabled_rules:
6 | - file_length
7 | - identifier_name
8 | - nesting
9 | - notification_center_detachment
10 | - shorthand_operator
11 | - todo
12 | - type_body_length
13 | - type_name
14 | - unused_setter_value
15 |
16 | opt_in_rules:
17 | - modifier_order
18 | - multiline_arguments
19 | - multiline_arguments_brackets
20 | - multiline_literal_brackets
21 | - prefer_self_type_over_type_of_self
22 | - shorthand_optional_binding
23 | - sorted_imports
24 | - unavailable_function
25 | - file_header
26 |
27 | excluded:
28 | - Package.swift
29 |
30 | trailing_comma:
31 | mandatory_comma: true
32 |
33 | trailing_whitespace:
34 | ignores_comments: false
35 |
36 | modifier_order:
37 | preferred_modifier_order:
38 | - acl
39 | - setterACL
40 | - override
41 | - dynamic
42 | - mutators
43 | - lazy
44 | - final
45 | - required
46 | - convenience
47 | - typeMethods
48 | - owned
49 |
50 | file_header:
51 | severity: error
52 | required_pattern: |
53 | \/\/
54 | \/\/ .*?\.swift
55 | \/\/
56 | \/\/ Licensed under the Apache License, Version 2\.0 \(the "License"\);
57 | \/\/ you may not use this file except in compliance with the License\.
58 | \/\/ You may obtain a copy of the License at
59 | \/\/
60 | \/\/ http:\/\/www\.apache\.org\/licenses/LICENSE-2\.0
61 | \/\/
62 | \/\/ Unless required by applicable law or agreed to in writing, software
63 | \/\/ distributed under the License is distributed on an "AS IS" BASIS,
64 | \/\/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
65 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/UserAgentTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserAgentTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class UserAgentTests: XCTestCase {
21 | func test_user_agent() {
22 | let appIdentifier = infoDictionaryString(forKey: kCFBundleIdentifierKey as String)
23 | let appVersion = infoDictionaryString(forKey: kCFBundleVersionKey as String)
24 | let payKitVersion = CashAppPay.version
25 |
26 | let model = UIDevice.current.deviceModel ?? UserAgent.unknownValue
27 | let language = Locale.current.languageCode?.lowercased() ?? UserAgent.unknownValue
28 | let country = Locale.current.regionCode?.lowercased() ?? UserAgent.unknownValue
29 | let osVersion = UIDevice.current.systemVersion
30 |
31 | let expectedUserAgent = String(
32 | format: "Mozilla/5.0 (%@; CPU iPhone OS %@ like Mac OS X; %@-%@) PayKitVersion/%@ %@/%@",
33 | model,
34 | osVersion,
35 | language,
36 | country,
37 | payKitVersion,
38 | appIdentifier,
39 | appVersion
40 | )
41 | XCTAssertEqual(UserAgent.userAgent, expectedUserAgent)
42 | }
43 |
44 | func test_unknown_value() {
45 | XCTAssertEqual(UserAgent.unknownValue, "unknown")
46 | }
47 |
48 | private func infoDictionaryString(forKey key: String) -> String {
49 | return Bundle.main.object(forInfoDictionaryKey: key) as? String ?? UserAgent.unknownValue
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Networking/RetryPolicy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RetryPolicy.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | enum RetryPolicy: Equatable {
20 | case exponential(delay: TimeInterval = 2, attempt: Int = 0, maximumNumberOfAttempts: Int)
21 |
22 | /// The delay before the request can be retried.
23 | var lockout: TimeInterval {
24 | switch self {
25 | case .exponential(delay: let interval, attempt: let attempt, maximumNumberOfAttempts: _):
26 | return RetryPolicy.exponentialBackoffCalculation(interval: interval, retryNumber: attempt)
27 | }
28 | }
29 |
30 | /// Returns the retry policy for the next attempt or nil if the policy indiciates it should not run again.
31 | func decrement() -> RetryPolicy? {
32 | switch self {
33 | case .exponential(delay: let delay, attempt: let attempt, maximumNumberOfAttempts: let maximumNumberOfAttempts):
34 | let attempts = attempt + 1
35 | guard attempts < maximumNumberOfAttempts else { return nil }
36 | return .exponential(delay: delay, attempt: attempts, maximumNumberOfAttempts: maximumNumberOfAttempts)
37 | }
38 | }
39 | }
40 |
41 | // MARK: - Calculations
42 |
43 | private extension RetryPolicy {
44 | /// Calculation based on https://en.wikipedia.org/wiki/Exponential_backoff#Expected_backoff
45 | private static func exponentialBackoffCalculation(interval: TimeInterval, retryNumber: Int) -> TimeInterval {
46 | interval * (pow(2, Double(retryNumber)) - 1)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/APIErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIErrorTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class APIErrorTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_serialize_internalServerError() throws {
26 | // Try serializing the error to JSON.
27 | let serializedError = try jsonEncoder.encode(APIErrorWrapper(errors: [TestValues.internalServerError]))
28 |
29 | // Try loading the fixture JSON from file.
30 | let fixtureError = try fixtureDataForFilename(internalServerErrorFilename, in: .errors)
31 |
32 | // The JSON data is unordered, so convert it to a dictionary to compare.
33 | let fixtureDict = try XCTUnwrap(JSONSerialization.jsonObject(with: fixtureError) as? [String: AnyHashable])
34 | let serializedDict = try XCTUnwrap(
35 | JSONSerialization.jsonObject(with: serializedError) as? [String: AnyHashable]
36 | )
37 | XCTAssertEqual(fixtureDict, serializedDict)
38 | }
39 |
40 | func test_deserialize_internalServerError() throws {
41 | let fixtureJSON = try fixtureDataForFilename(internalServerErrorFilename, in: .errors)
42 | let errorWrapper = try jsonDecoder.decode(APIErrorWrapper.self, from: fixtureJSON)
43 | XCTAssertEqual(errorWrapper.errors.first, TestValues.internalServerError)
44 | }
45 |
46 | let internalServerErrorFilename = "internalServerError"
47 | }
48 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/RetryPolicyTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RetryPolicyTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class RetryPolicyTests: XCTestCase {
21 | func test_exponential_lockout() {
22 | XCTAssertEqual(RetryPolicy.exponential(delay: 5, attempt: 0, maximumNumberOfAttempts: 5).lockout, 0)
23 | XCTAssertEqual(RetryPolicy.exponential(delay: 5, attempt: 1, maximumNumberOfAttempts: 5).lockout, 5)
24 | XCTAssertEqual(RetryPolicy.exponential(delay: 5, attempt: 2, maximumNumberOfAttempts: 5).lockout, 15)
25 | XCTAssertEqual(RetryPolicy.exponential(delay: 5, attempt: 3, maximumNumberOfAttempts: 5).lockout, 35)
26 | XCTAssertEqual(RetryPolicy.exponential(delay: 5, attempt: 4, maximumNumberOfAttempts: 5).lockout, 75)
27 | }
28 |
29 | func test_exponential_default_values() {
30 | XCTAssertEqual(
31 | RetryPolicy.exponential(maximumNumberOfAttempts: 5),
32 | .exponential(delay: 2, attempt: 0, maximumNumberOfAttempts: 5)
33 | )
34 | }
35 |
36 | func test_decrement_exponential_lockout() {
37 | let policy = RetryPolicy.exponential(delay: 5, attempt: 0, maximumNumberOfAttempts: 3)
38 | let firstAttempt = policy.decrement()
39 | XCTAssertEqual(firstAttempt, RetryPolicy.exponential(delay: 5, attempt: 1, maximumNumberOfAttempts: 3))
40 | let secondAttempt = firstAttempt?.decrement()
41 | XCTAssertEqual(secondAttempt, RetryPolicy.exponential(delay: 5, attempt: 2, maximumNumberOfAttempts: 3))
42 | XCTAssertNil(secondAttempt?.decrement())
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "PayKit",
6 | defaultLocalization: "en",
7 | platforms: [
8 | .iOS(.v12),
9 | .macOS(.v10_13),
10 | ],
11 | products: [
12 | .library(
13 | name: "PayKit",
14 | targets: ["PayKit"]
15 | ),
16 | .library(
17 | name: "PayKitUI",
18 | targets: ["PayKitUI"]
19 | ),
20 | ],
21 | dependencies: [
22 | .package(url: "https://github.com/SwiftGen/SwiftGenPlugin.git", from: "6.6.2"),
23 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0"),
24 | ],
25 | targets: [
26 | .target(
27 | name: "PayKit",
28 | resources: [
29 | .process("PrivacyInfo.xcprivacy")
30 | ],
31 | swiftSettings: [
32 | .define("LOGGING", .when(configuration: .debug))
33 | ]
34 | ),
35 | .target(
36 | name: "PayKitUI",
37 | resources: [
38 | .copy("custom-xcassets-template.stencil"),
39 | .copy("Shared/Assets/Resources/Colors.xcassets"),
40 | .copy("swiftgen.yml"),
41 | .copy("Shared/Assets/Resources/Images.xcassets"),
42 | .process("PrivacyInfo.xcprivacy")
43 | ], plugins: [
44 | .plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin"),
45 | ]
46 | ),
47 | .testTarget(
48 | name: "PayKitTests",
49 | dependencies: ["PayKit"],
50 | resources: [
51 | .copy("Resources/Fixtures"),
52 | .copy("Resources/Fixtures/Errors"),
53 | ]
54 | ),
55 | .testTarget(
56 | name: "PayKitUITests",
57 | dependencies: [
58 | "PayKitUI",
59 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
60 | ],
61 | resources: [
62 | .copy("__Snapshots__/"),
63 | ]
64 | )
65 | ]
66 | )
67 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/CustomerRequest+ExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | final class CustomerRequest_ExtensionsTests: XCTestCase {
21 | private var now: Date!
22 | private var url: URL!
23 |
24 | override func setUpWithError() throws {
25 | try super.setUpWithError()
26 | now = Date()
27 | url = try XCTUnwrap(URL(string: "https://block.xyz"))
28 | }
29 |
30 | override func tearDown() {
31 | now = nil
32 | url = nil
33 | super.tearDown()
34 | }
35 |
36 | func test_expired() throws {
37 | let authFlowTrigger = CustomerRequest.AuthFlowTriggers(
38 | qrCodeImageURL: url,
39 | qrCodeSVGURL: url,
40 | mobileURL: url,
41 | refreshesAt: Date(timeInterval: -30, since: now)
42 | )
43 |
44 | XCTAssertTrue(authFlowTrigger.isExpired(on: now))
45 | }
46 |
47 | func test_notExpired() throws {
48 | let authFlowTrigger = CustomerRequest.AuthFlowTriggers(
49 | qrCodeImageURL: url,
50 | qrCodeSVGURL: url,
51 | mobileURL: url,
52 | refreshesAt: Date(timeInterval: 30, since: now)
53 | )
54 | XCTAssertFalse(authFlowTrigger.isExpired(on: now))
55 | }
56 |
57 | func test_expiredBecauseOfJitter() {
58 | let authFlowTrigger = CustomerRequest.AuthFlowTriggers(
59 | qrCodeImageURL: url,
60 | qrCodeSVGURL: url,
61 | mobileURL: url,
62 | refreshesAt: Date(timeInterval: 10, since: now)
63 | )
64 | XCTAssertTrue(authFlowTrigger.isExpired(on: now))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Resources/Fixtures/approvedRequest-fullyPopulated.json:
--------------------------------------------------------------------------------
1 | {
2 | "request":
3 | {
4 | "id": "GRR_mg3saamyqdm29jj9pqjqkedm",
5 | "status": "APPROVED",
6 | "actions":
7 | [
8 | {
9 | "type": "ON_FILE_PAYMENT",
10 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
11 | "account_reference_id": "account4"
12 | }
13 | ],
14 | "origin":
15 | {
16 | "type": "DIRECT"
17 | },
18 | "grants":
19 | [
20 | {
21 | "id": "GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g",
22 | "status": "ACTIVE",
23 | "type": "EXTENDED",
24 | "action":
25 | {
26 | "type": "ON_FILE_PAYMENT",
27 | "scope_id": "BRAND_9kx6p0mkuo97jnl025q9ni94t",
28 | "account_reference_id": "account4"
29 | },
30 | "channel": "IN_APP",
31 | "customer_id": "CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc",
32 | "expires_at": "2027-10-19T21:03:43.159Z",
33 | "created_at": "2022-10-20T21:03:43.249Z",
34 | "updated_at": "2022-10-20T21:03:43.249Z"
35 | }
36 | ],
37 | "reference_id": "refer_to_me",
38 | "created_at": "2022-10-20T20:16:18.051Z",
39 | "updated_at": "2022-10-20T21:04:10.701Z",
40 | "expires_at": "2022-10-20T22:03:43.113Z",
41 | "requester_profile":
42 | {
43 | "name": "SDK Hacking: The Brand",
44 | "logo_url": "https://franklin-assets.s3.amazonaws.com/merchants/assets/v3/generic/m_category_shopping.png"
45 | },
46 | "customer_profile":
47 | {
48 | "id": "CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc",
49 | "cashtag": "$CASHTAG_C_TOKEN"
50 | },
51 | "metadata":
52 | {
53 | "key1": "Valuation",
54 | "key2": "ValuWorld",
55 | "key3": "Valuminous"
56 | },
57 | "redirect_url": "paykitdemo://callback",
58 | "channel": "IN_APP"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Helpers/MockURLProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockURLProtocol.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | class MockURLSession {
20 | let session: URLSession
21 |
22 | // swiftlint:disable:next large_tuple
23 | init(handler: ((URLRequest) -> (Data?, URLResponse?, Error?))?) {
24 | URLProtocolStub.loadingHandler = handler
25 |
26 | let configuration = URLSessionConfiguration.ephemeral
27 | configuration.protocolClasses = [URLProtocolStub.self]
28 | self.session = URLSession(configuration: configuration)
29 | }
30 |
31 | deinit {
32 | URLProtocolStub.loadingHandler = nil
33 | }
34 | }
35 |
36 | private final class URLProtocolStub: URLProtocol {
37 |
38 | // swiftlint:disable:next large_tuple
39 | static var loadingHandler: ((URLRequest) -> (Data?, URLResponse?, Error?))?
40 |
41 | override class func canInit(with request: URLRequest) -> Bool {
42 | true
43 | }
44 |
45 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
46 | request
47 | }
48 |
49 | override func startLoading() {
50 | let (data, response, error) = Self.loadingHandler!(request)
51 |
52 | if let error {
53 | client?.urlProtocol(self, didFailWithError: error)
54 | } else if let response, let data {
55 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
56 | client?.urlProtocol(self, didLoad: data)
57 | } else {
58 | fatalError("No stubs provided")
59 | }
60 |
61 | client?.urlProtocolDidFinishLoading(self)
62 | }
63 |
64 | override func stopLoading() {
65 | // Intentionally left blank
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/CashAppPaymentMethodViewSnapshotTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPaymentMethodViewSnapshotTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKitUI
18 | import SnapshotTesting
19 | import SwiftUI
20 |
21 | @available(iOS 13.0, *)
22 | class CashAppPaymentMethodViewSnapshotTests: BaseSnapshotTestCase {
23 | func test_small_button() {
24 | assertSnapshot(
25 | matching: CashAppPaymentMethodView(size: .small, cashTag: "$jack").frame(height: 100),
26 | as: .image(on: .iPhone8)
27 | )
28 | }
29 |
30 | func test_large_button() {
31 | assertSnapshot(
32 | matching: CashAppPaymentMethodView(size: .large, cashTag: "$jack"),
33 | as: .image(on: .iPhone8)
34 | )
35 | }
36 |
37 | func test_dark_mode() {
38 | assertSnapshot(
39 | matching: CashAppPaymentMethodView(size: .large, cashTag: "$jack"),
40 | as: .image(on: .iPhone8, userInterfaceStyle: .dark)
41 | )
42 | }
43 |
44 | func test_text_custom_font() {
45 | assertSnapshot(
46 | matching: CashAppPaymentMethodView(
47 | size: .large,
48 | cashTag: "$jack",
49 | cashTagFont: Font.caption,
50 | cashTagTextColor: .red
51 | ),
52 | as: .image(on: .iPhone8, userInterfaceStyle: .dark)
53 | )
54 | }
55 |
56 | func test_minimum_size() {
57 | let view = HStack {
58 | Spacer().frame(idealWidth: .infinity)
59 | CashAppPaymentMethodView(size: .small, cashTag: "$jack")
60 | Spacer().frame(idealWidth: .infinity)
61 | }
62 | assertSnapshot(matching: view, as: .image(on: .iPhone8, userInterfaceStyle: .dark))
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/CashAppPayButtonSnapshotTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPayButtonSnapshotTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKitUI
18 | import SnapshotTesting
19 | import XCTest
20 |
21 | @available(iOS 13.0, *)
22 | class CashAppPayButtonSnapshotTests: BaseSnapshotTestCase {
23 | func test_small_button() {
24 | assertSnapshot(matching: CashAppPayButton(size: .small, onClickHandler: {}), as: .image(centeredIn: .iPhone8))
25 | }
26 |
27 | func test_large_button() {
28 | assertSnapshot(matching: CashAppPayButton(size: .large, onClickHandler: {}), as: .image(centeredIn: .iPhone8))
29 | }
30 |
31 | func test_button_disabled() {
32 | let lightButton = CashAppPayButton(size: .large, onClickHandler: {})
33 | lightButton.isEnabled = false
34 | assertSnapshot(matching: lightButton, as: .image(centeredIn: .iPhone8, userInterfaceStyle: .light))
35 |
36 | let darkButton = CashAppPayButton(size: .large, onClickHandler: {})
37 | darkButton.isEnabled = false
38 | assertSnapshot(matching: darkButton, as: .image(centeredIn: .iPhone8, userInterfaceStyle: .dark))
39 | }
40 |
41 | func test_dark_mode() {
42 | assertSnapshot(
43 | matching: CashAppPayButton(onClickHandler: {}),
44 | as: .image(centeredIn: .iPhone8, userInterfaceStyle: .dark)
45 | )
46 | }
47 |
48 | func test_button_expands() {
49 | assertSnapshot(
50 | matching: CashAppPayButton(size: .large, onClickHandler: {}),
51 | as: .image(filling: .iPhone8, userInterfaceStyle: .dark)
52 | )
53 | assertSnapshot(
54 | matching: CashAppPayButton(size: .small, onClickHandler: {}),
55 | as: .image(filling: .iPhone8, userInterfaceStyle: .dark)
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/AnalyticsDataSourceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsDataSourceTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class AnalyticsDataSourceTests: XCTestCase {
21 | private var dataSource: AnalyticsDataSource!
22 |
23 | override func setUp() {
24 | super.setUp()
25 | self.dataSource = AnalyticsDataSource()
26 | }
27 |
28 | override func tearDown() {
29 | self.dataSource = nil
30 | super.tearDown()
31 | }
32 |
33 | func test_insert() {
34 | let event = AnalyticsEvent(catalog: "test", fields: ["test": "key"])
35 |
36 | XCTAssertEqual(dataSource.count, 0)
37 | dataSource.insert(event: event)
38 | XCTAssertEqual(dataSource.count, 1)
39 | }
40 |
41 | func test_query_limit() {
42 | let event1 = AnalyticsEvent(catalog: "test", fields: ["test": "key"])
43 | let event2 = AnalyticsEvent(catalog: "test", fields: ["test": 5])
44 |
45 | dataSource.insert(event: event1)
46 | dataSource.insert(event: event2)
47 |
48 | XCTAssertEqual(dataSource.fetch(limit: 1).count, 1)
49 | }
50 |
51 | func test_remove() {
52 | let id = UUID()
53 | let event = AnalyticsEvent(id: id, catalog: "test", fields: ["test": "key"])
54 |
55 | dataSource.insert(event: event)
56 | dataSource.remove([event.id])
57 |
58 | XCTAssertEqual(dataSource.count, 0)
59 | }
60 |
61 | func test_max_event_count_is_not_exceeded() {
62 | let events = Array(repeating: AnalyticsEvent(catalog: "", fields: [:]), count: 300)
63 | let store = AnalyticsDataSource(events: events)
64 | XCTAssertEqual(store.count, 300)
65 | store.insert(event: AnalyticsEvent(catalog: "", fields: [:]))
66 | XCTAssertEqual(store.count, 300)
67 | }
68 |
69 | func test_max_event_count() {
70 | XCTAssertEqual(AnalyticsDataSource.maxEventCount, 300)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/CashAppPayButtonViewSnapshotTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPayButtonViewSnapshotTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKitUI
18 | import SnapshotTesting
19 | import SwiftUI
20 |
21 | @available(iOS 13.0, *)
22 | class CashAppPayButtonViewSnapshotTests: BaseSnapshotTestCase {
23 | func test_small_button() {
24 | assertSnapshot(
25 | matching: CashAppPayButtonView(size: .small, onClickHandler: {}),
26 | as: .image(on: .iPhone8)
27 | )
28 | }
29 |
30 | func test_large_button() {
31 | assertSnapshot(
32 | matching: CashAppPayButtonView(size: .large, onClickHandler: {}),
33 | as: .image(on: .iPhone8)
34 | )
35 | }
36 |
37 | func test_button_disabled() {
38 | assertSnapshot(
39 | matching: CashAppPayButtonView(size: .large, isEnabled: false, onClickHandler: {}),
40 | as: .image(on: .iPhone8, userInterfaceStyle: .light)
41 | )
42 |
43 | assertSnapshot(
44 | matching: CashAppPayButtonView(size: .large, isEnabled: false, onClickHandler: {}),
45 | as: .image(on: .iPhone8, userInterfaceStyle: .dark)
46 | )
47 | }
48 |
49 | func test_dark_mode() {
50 | assertSnapshot(
51 | matching: CashAppPayButtonView(size: .large, onClickHandler: {}),
52 | as: .image(on: .iPhone8, userInterfaceStyle: .dark)
53 | )
54 | }
55 |
56 | func test_minimum_size() {
57 | let smallButton = HStack {
58 | Spacer().frame(idealWidth: .infinity)
59 | CashAppPayButtonView(size: .small, onClickHandler: {})
60 | Spacer().frame(idealWidth: .infinity)
61 | }
62 | assertSnapshot(matching: smallButton, as: .image(on: .iPhone8))
63 |
64 | let largeButton = HStack {
65 | Spacer().frame(idealWidth: .infinity)
66 | CashAppPayButtonView(size: .large, onClickHandler: {})
67 | Spacer().frame(idealWidth: .infinity)
68 | }
69 | assertSnapshot(matching: largeButton, as: .image(on: .iPhone8))
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 |
19 | @main
20 | class AppDelegate: UIResponder, UIApplicationDelegate {
21 |
22 | func application(
23 | _ application: UIApplication,
24 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
25 | ) -> Bool {
26 | // Override point for customization after application launch.
27 |
28 | let appearance = UINavigationBarAppearance()
29 | appearance.configureWithOpaqueBackground()
30 | UINavigationBar.appearance().standardAppearance = appearance
31 | UINavigationBar.appearance().scrollEdgeAppearance = appearance
32 |
33 | let tabBarAppearance: UITabBarAppearance = UITabBarAppearance()
34 | tabBarAppearance.configureWithDefaultBackground()
35 | tabBarAppearance.backgroundColor = .white
36 | UITabBar.appearance().standardAppearance = tabBarAppearance
37 | UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
38 |
39 | return true
40 | }
41 |
42 | // MARK: UISceneSession Lifecycle
43 |
44 | func application(
45 | _ application: UIApplication,
46 | configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions
47 | ) -> UISceneConfiguration {
48 | // Called when a new scene session is being created.
49 | // Use this method to select a configuration to create the new scene with.
50 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
51 | }
52 |
53 | func application(
54 | _ application: UIApplication,
55 | didDiscardSceneSessions sceneSessions: Set
56 | ) {
57 | // Called when the user discards a scene session.
58 | // If any sessions were discarded while the application was not running,
59 | // this will be called shortly after application:didFinishLaunchingWithOptions.
60 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/CashAppPaymentMethodSnapshotTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPaymentMethodSnapshotTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKitUI
18 | import SnapshotTesting
19 | import UIKit
20 |
21 | @available(iOS 13.0, *)
22 | class CashAppPaymentMethodSnapshotTests: BaseSnapshotTestCase {
23 | func test_small_payment_method() {
24 | let paymentMethod = CashAppPaymentMethod(size: .small)
25 | paymentMethod.cashTag = "$jack"
26 | assertSnapshot(matching: paymentMethod, as: .image(centeredIn: .iPhone8))
27 | }
28 |
29 | func test_large_payment_method() {
30 | let paymentMethod = CashAppPaymentMethod(size: .large)
31 | paymentMethod.cashTag = "$jack"
32 | assertSnapshot(matching: paymentMethod, as: .image(centeredIn: .iPhone8))
33 | }
34 |
35 | func test_dark_mode() {
36 | let paymentMethod = CashAppPaymentMethod(size: .large)
37 | assertSnapshot(matching: paymentMethod, as: .image(centeredIn: .iPhone8, userInterfaceStyle: .dark))
38 | }
39 |
40 | func test_text_custom_font() {
41 | let paymentMethod = CashAppPaymentMethod(size: .large)
42 | paymentMethod.cashTag = "$jack"
43 | paymentMethod.setCashTagFont(.italicSystemFont(ofSize: 12))
44 | paymentMethod.setCashTagTextColor(.red)
45 | assertSnapshot(matching: paymentMethod, as: .image(centeredIn: .iPhone8, userInterfaceStyle: .dark))
46 | }
47 |
48 | func test_expands_full_width() {
49 | let paymentMethod = CashAppPaymentMethod(size: .large)
50 | paymentMethod.cashTag = "$jack"
51 |
52 | let container = UIView()
53 | paymentMethod.translatesAutoresizingMaskIntoConstraints = false
54 | container.addSubview(paymentMethod)
55 |
56 | NSLayoutConstraint.activate([
57 | container.leadingAnchor.constraint(equalTo: paymentMethod.leadingAnchor),
58 | container.trailingAnchor.constraint(equalTo: paymentMethod.trailingAnchor),
59 | container.centerYAnchor.constraint(equalTo: paymentMethod.centerYAnchor),
60 | ])
61 | assertSnapshot(matching: container, as: .image(filling: .iPhone8, userInterfaceStyle: .dark))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/AnalyticsClientTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsClientTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class AnalyticsClientTests: XCTestCase {
21 |
22 | private let event = AnalyticsEvent(catalog: "catlog", fields: ["hello": "world"])
23 |
24 | func test_event_stream_2_params() throws {
25 | let param = EventStream2Params(appName: "App", event: event)
26 | XCTAssertEqual(param.appName, "App")
27 |
28 | XCTAssertEqual(try param.event.jsonString(), try event.jsonString())
29 | }
30 |
31 | func test_endpoint() {
32 | XCTAssertEqual(AnalyticsClient.Endpoint.staging.baseURL.absoluteString, "https://api.squareupstaging.com/")
33 | XCTAssertEqual(AnalyticsClient.Endpoint.production.baseURL.absoluteString, "https://api.squareup.com/")
34 | }
35 |
36 | func test_request_body() {
37 | let expectation = self.expectation(description: "Uploaded")
38 | let mock = MockRestService { request, retry, _ in
39 | self.XCTAssertEqual(retry, .exponential(maximumNumberOfAttempts: 5))
40 | self.XCTAssertEqual(request.url?.absoluteString, "https://api.squareup.com/2.0/log/eventstream")
41 | self.XCTAssertEqual(request.httpMethod, "POST")
42 | XCTAssertNotNil(request.httpBody)
43 | expectation.fulfill()
44 | }
45 |
46 | let client = AnalyticsClient(restService: mock, endpoint: .production)
47 | client.upload(appName: "app", events: [event]) { _ in }
48 |
49 | waitForExpectations(timeout: 0.5)
50 | }
51 |
52 | func test_analytics_date_encoder_uses_rounded_ms() throws {
53 | let date = try XCTUnwrap(
54 | DateComponents(
55 | calendar: Calendar(identifier: .gregorian),
56 | timeZone: .init(secondsFromGMT: 0),
57 | year: 2022,
58 | month: 4,
59 | day: 20,
60 | hour: 8,
61 | minute: 30,
62 | second: 45
63 | ).date
64 | )
65 | let data = try JSONEncoder.eventStream2Encoder().encode(date)
66 | XCTAssertEqual(String(data: data, encoding: .utf8), "1650443445000000")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | specs:
3 | CFPropertyList (3.0.6)
4 | rexml
5 | activesupport (6.1.7.2)
6 | concurrent-ruby (~> 1.0, >= 1.0.2)
7 | i18n (>= 1.6, < 2)
8 | minitest (>= 5.1)
9 | tzinfo (~> 2.0)
10 | zeitwerk (~> 2.3)
11 | addressable (2.8.1)
12 | public_suffix (>= 2.0.2, < 6.0)
13 | algoliasearch (1.27.5)
14 | httpclient (~> 2.8, >= 2.8.3)
15 | json (>= 1.5.1)
16 | atomos (0.1.3)
17 | claide (1.1.0)
18 | cocoapods (1.11.3)
19 | addressable (~> 2.8)
20 | claide (>= 1.0.2, < 2.0)
21 | cocoapods-core (= 1.11.3)
22 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
23 | cocoapods-downloader (>= 1.4.0, < 2.0)
24 | cocoapods-plugins (>= 1.0.0, < 2.0)
25 | cocoapods-search (>= 1.0.0, < 2.0)
26 | cocoapods-trunk (>= 1.4.0, < 2.0)
27 | cocoapods-try (>= 1.1.0, < 2.0)
28 | colored2 (~> 3.1)
29 | escape (~> 0.0.4)
30 | fourflusher (>= 2.3.0, < 3.0)
31 | gh_inspector (~> 1.0)
32 | molinillo (~> 0.8.0)
33 | nap (~> 1.0)
34 | ruby-macho (>= 1.0, < 3.0)
35 | xcodeproj (>= 1.21.0, < 2.0)
36 | cocoapods-core (1.11.3)
37 | activesupport (>= 5.0, < 7)
38 | addressable (~> 2.8)
39 | algoliasearch (~> 1.0)
40 | concurrent-ruby (~> 1.1)
41 | fuzzy_match (~> 2.0.4)
42 | nap (~> 1.0)
43 | netrc (~> 0.11)
44 | public_suffix (~> 4.0)
45 | typhoeus (~> 1.0)
46 | cocoapods-deintegrate (1.0.5)
47 | cocoapods-downloader (1.6.3)
48 | cocoapods-plugins (1.0.0)
49 | nap
50 | cocoapods-search (1.0.1)
51 | cocoapods-trunk (1.6.0)
52 | nap (>= 0.8, < 2.0)
53 | netrc (~> 0.11)
54 | cocoapods-try (1.2.0)
55 | colored2 (3.1.2)
56 | concurrent-ruby (1.2.0)
57 | escape (0.0.4)
58 | ethon (0.16.0)
59 | ffi (>= 1.15.0)
60 | ffi (1.15.5)
61 | fourflusher (2.3.1)
62 | fuzzy_match (2.0.4)
63 | gh_inspector (1.1.3)
64 | httpclient (2.8.3)
65 | i18n (1.12.0)
66 | concurrent-ruby (~> 1.0)
67 | json (2.6.3)
68 | minitest (5.17.0)
69 | molinillo (0.8.0)
70 | nanaimo (0.3.0)
71 | nap (1.1.0)
72 | netrc (0.11.0)
73 | public_suffix (4.0.7)
74 | rexml (3.2.5)
75 | ruby-macho (2.5.1)
76 | typhoeus (1.4.0)
77 | ethon (>= 0.9.0)
78 | tzinfo (2.0.6)
79 | concurrent-ruby (~> 1.0)
80 | xcodeproj (1.22.0)
81 | CFPropertyList (>= 2.3.3, < 4.0)
82 | atomos (~> 0.1.3)
83 | claide (>= 1.0.2, < 2.0)
84 | colored2 (~> 3.1)
85 | nanaimo (~> 0.3.0)
86 | rexml (~> 3.2.4)
87 | zeitwerk (2.6.6)
88 |
89 | GEM
90 | remote: https://rubygems.org/
91 | specs:
92 |
93 | PLATFORMS
94 | arm64-darwin-22
95 |
96 | DEPENDENCIES
97 | cocoapods (= 1.11.3)!
98 |
99 | BUNDLED WITH
100 | 2.4.6
101 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Networking/RESTService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RESTService.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | protocol RESTService {
20 | func execute(
21 | request: URLRequest,
22 | retryPolicy: RetryPolicy?,
23 | completion: @escaping ((Data?, URLResponse?, Error?) -> Void)
24 | )
25 | }
26 |
27 | final class ResilientRESTService: RESTService {
28 |
29 | // MARK: - Properties
30 |
31 | private var urlSession: URLSession
32 |
33 | // MARK: - Lifecycle
34 |
35 | init(urlSession: URLSession = .shared) {
36 | self.urlSession = urlSession
37 | }
38 |
39 | // MARK: - RESTService
40 |
41 | func execute(
42 | request: URLRequest,
43 | retryPolicy: RetryPolicy?,
44 | completion: @escaping ((Data?, URLResponse?, Error?) -> Void)
45 | ) {
46 | let request = HTTPRequest(urlRequest: request, retryPolicy: retryPolicy, handler: completion)
47 | performRequest(request: request)
48 | }
49 |
50 | // MARK: Private
51 |
52 | private func performRequest(request: HTTPRequest) {
53 | let task = urlSession.dataTask(with: request.urlRequest) { [weak self] data, response, error in
54 | self?.handleResponse(for: request, data: data, response: response, error: error)
55 | }
56 | task.resume()
57 | }
58 |
59 | private func handleResponse(for request: HTTPRequest, data: Data?, response: URLResponse?, error: Error?) {
60 | // Retry transport errors only.
61 | let isSuccessful = (data != nil) && (error == nil) && (response as? HTTPURLResponse != nil)
62 | guard !isSuccessful, let retryLockout = request.retryPolicy?.lockout else {
63 | // Complete the request.
64 | request.handler(data, response, error)
65 | return
66 | }
67 | retry(request: request, delay: retryLockout)
68 | }
69 |
70 | private func retry(request: HTTPRequest, delay: TimeInterval) {
71 | let retryRequest = HTTPRequest(
72 | urlRequest: request.urlRequest,
73 | retryPolicy: request.retryPolicy?.decrement(),
74 | handler: request.handler
75 | )
76 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
77 | self?.performRequest(request: retryRequest)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Networking/UserAgent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserAgent.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 |
19 | /// A utility struct used in generating a user agent string for the customer's device.
20 | /// An example is
21 | /// "Mozilla/5.0 (iPhone7,2; CPU iPhone OS 10.0 like Mac OS X; en-us) PayKitVersion/2.1.5 com.squareup.PayKitDemo/1".
22 | enum UserAgent {
23 | // MARK: - Private Static Properties
24 |
25 | static let unknownValue = "unknown"
26 |
27 | // MARK: - Public Static Properties
28 |
29 | static let userAgent: String = {
30 | func infoDictionaryString(forKey key: String) -> String {
31 | let bundleValue = Bundle.main.object(forInfoDictionaryKey: key) as? String
32 | return bundleValue ?? unknownValue
33 | }
34 | let appIdentifier = infoDictionaryString(forKey: kCFBundleIdentifierKey as String)
35 | let appVersion = infoDictionaryString(forKey: kCFBundleVersionKey as String)
36 | var payKitVersion = CashAppPay.version
37 |
38 | let model = UIDevice.current.deviceModel ?? unknownValue
39 | let language = Locale.current.languageCode?.lowercased() ?? unknownValue
40 | let country = Locale.current.regionCode?.lowercased() ?? unknownValue
41 | let osVersion = UIDevice.current.systemVersion
42 |
43 | return String(
44 | format: "Mozilla/5.0 (%@; CPU iPhone OS %@ like Mac OS X; %@-%@) PayKitVersion/%@ %@/%@",
45 | model,
46 | osVersion,
47 | language,
48 | country,
49 | payKitVersion,
50 | appIdentifier,
51 | appVersion
52 | )
53 | }()
54 | }
55 |
56 | // MARK: - UIDevice
57 |
58 | extension UIDevice {
59 | // Returns the human readable version of the device being used
60 | var deviceModel: String? {
61 | if let sim = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] {
62 | return "Simulator(\(sim))"
63 | } else {
64 | var systemInfo = utsname()
65 | uname(&systemInfo)
66 | let modelCode = withUnsafePointer(to: &systemInfo.machine) {
67 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in
68 | String(validatingUTF8: ptr)
69 | }
70 | }
71 | return modelCode
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/UpdateCustomerRequestParamsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateCustomerRequestParamsTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class UpdateCustomerRequestParamsTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_serializeUpdateParams_clearAllButActions() throws {
26 | let params = TestValues.updateCustomerRequestParams_clearAllButActions
27 |
28 | // Try serializing the params to JSON.
29 | let serializedParams = try jsonEncoder.encode(UpdateCustomerRequestParamsWrapper(params))
30 | // Load the param fixture from file.
31 | let fixtureParams = try fixtureDataForFilename(clearAllButActionsFilename)
32 |
33 | // The wrappers won't match because they have different idempotency keys, but the underlying requests
34 | // should be identical.
35 | let serializedDict = try XCTUnwrap(
36 | JSONSerialization.jsonObject(with: serializedParams) as? [String: AnyHashable]
37 | )
38 | let fixtureDict = try XCTUnwrap(
39 | JSONSerialization.jsonObject(with: fixtureParams) as? [String: AnyHashable]
40 | )
41 | XCTAssertEqual(serializedDict["request"]!, fixtureDict["request"]!)
42 | }
43 |
44 | func test_serializeUpdateParams_clearAmount() throws {
45 | let params = TestValues.updateCustomerRequestParams_clearAmount
46 |
47 | // Try serializing the params to JSON.
48 | let serializedParams = try jsonEncoder.encode(UpdateCustomerRequestParamsWrapper(params))
49 | // Load the param fixture from file.
50 | let fixtureParams = try fixtureDataForFilename(clearAmountFilename)
51 |
52 | // The wrappers won't match because they have different idempotency keys, but the underlying requests
53 | // should be identical.
54 | let serializedDict = try XCTUnwrap(
55 | JSONSerialization.jsonObject(with: serializedParams) as? [String: AnyHashable]
56 | )
57 | let fixtureDict = try XCTUnwrap(
58 | JSONSerialization.jsonObject(with: fixtureParams) as? [String: AnyHashable]
59 | )
60 | XCTAssertEqual(serializedDict["request"]!, fixtureDict["request"]!)
61 | }
62 |
63 | let clearAllButActionsFilename = "updateRequestParams-clearAllButActions"
64 | let clearAmountFilename = "updateRequestParams-clearAmountFromOneTime"
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/CreateCustomerRequestParamsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateCustomerRequestParamsTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class CreateCustomerRequestParamsTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_constructor_defaults_to_in_app_channel() throws {
26 | let url = try XCTUnwrap(URL(string: "https://block.xyz/"))
27 | let params = CreateCustomerRequestParams(actions: [], redirectURL: url, referenceID: nil, metadata: nil)
28 | XCTAssertEqual(params.channel, .IN_APP)
29 | }
30 |
31 | func test_serializeCreateParams_fullyPopulated() throws {
32 | let params = TestValues.createCustomerRequestParams
33 |
34 | // Try serializing the params to JSON.
35 | let serializedParams = try jsonEncoder.encode(CreateCustomerRequestParamsWrapper(params))
36 |
37 | // Try loading the param fixture from file.
38 | let fixtureParams = try fixtureDataForFilename(createRequestParamsFilename)
39 |
40 | // The wrappers won't match because they have different idempotency keys, but the underlying requests
41 | // should be identical.
42 | let serializedDict = try XCTUnwrap(JSONSerialization.jsonObject(with: serializedParams) as? [String: Any])
43 | let fixtureDict = try XCTUnwrap(JSONSerialization.jsonObject(with: fixtureParams) as? [String: Any])
44 | XCTAssertNotNil(serializedDict["request"])
45 | XCTAssertNotNil(fixtureDict["request"])
46 | XCTAssertEqual(
47 | try XCTUnwrap(JSONSerialization.data(withJSONObject: serializedDict["request"]!, options: .sortedKeys)),
48 | try XCTUnwrap(JSONSerialization.data(withJSONObject: fixtureDict["request"]!, options: .sortedKeys))
49 | )
50 | }
51 |
52 | func test_deserializeCreateParams_fullyPopulated() throws {
53 | // Load the param fixture from file.
54 | let fixtureParams = try fixtureDataForFilename(createRequestParamsFilename)
55 |
56 | // Try deserializing the params to a struct.
57 | let deserializedParams = try jsonDecoder.decode(CreateCustomerRequestParamsWrapper.self, from: fixtureParams)
58 |
59 | let params = TestValues.createCustomerRequestParams
60 | XCTAssertEqual(deserializedParams.request, params)
61 | }
62 |
63 | let createRequestParamsFilename = "createRequestParams-fullyPopulated"
64 | }
65 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo.xcodeproj/xcshareddata/xcschemes/PayKitDemo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/UIKit/CashAppPayButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPayButton.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import SwiftUI
18 |
19 | @available(iOS 13.0, *)
20 | public class CashAppPayButton: UIView {
21 |
22 | // MARK: - Public Properties
23 |
24 | /// View size of the view
25 | public var size: SizingCategory {
26 | get { cashAppButton.viewModel.size }
27 | set { cashAppButton.viewModel.size = newValue }
28 | }
29 |
30 | /// True if the button is enabled
31 | public var isEnabled: Bool {
32 | get { cashAppButton.viewModel.isEnabled }
33 | set { cashAppButton.viewModel.isEnabled = newValue }
34 | }
35 |
36 | // MARK: - Private Properties
37 |
38 | private let onClickHandler: () -> Void
39 | private let cashAppButton: CashAppPayButtonView
40 |
41 | // MARK: - Life Cycle
42 |
43 | /**
44 | Initializes a button with the Cash App Logo and name.
45 |
46 | - Parameters:
47 | - size: The size of the button. Defaults to `large`.
48 | - onClickHandler: The handler called when the button is tapped.
49 | - usePolychromeAsset: Toggle usage of polychrome UI
50 | */
51 | public init(
52 | size: SizingCategory = .large,
53 | onClickHandler: @escaping () -> Void,
54 | usePolychromeAsset: Bool = false
55 | ) {
56 | self.cashAppButton = CashAppPayButtonView(
57 | size: size,
58 | onClickHandler: onClickHandler,
59 | usePolychromeAsset: usePolychromeAsset
60 | )
61 | self.onClickHandler = onClickHandler
62 | super.init(frame: .zero)
63 | guard let view = makeView() else { return }
64 | addSubview(view)
65 | NSLayoutConstraint.activate([
66 | view.leadingAnchor.constraint(equalTo: leadingAnchor),
67 | view.trailingAnchor.constraint(equalTo: trailingAnchor),
68 | view.topAnchor.constraint(equalTo: topAnchor),
69 | view.bottomAnchor.constraint(equalTo: bottomAnchor),
70 | ])
71 | }
72 |
73 | @available(*, unavailable)
74 | required init?(coder: NSCoder) {
75 | fatalError("init(coder:) has not been implemented")
76 | }
77 | }
78 |
79 | // MARK: - View Building
80 |
81 | @available(iOS 13.0, *)
82 | private extension CashAppPayButton {
83 | private func makeView() -> UIView? {
84 | guard let view = UIHostingController(rootView: cashAppButton).view else {
85 | return nil
86 | }
87 | view.backgroundColor = .clear
88 | view.translatesAutoresizingMaskIntoConstraints = false
89 | return view
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/CustomerRequestTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomerRequestTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class CustomerRequestTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_deserializePendingRequest_fullyPopulated() throws {
26 | let json = try fixtureDataForFilename(pendingRequestFilename)
27 | let requestWrapper = try jsonDecoder.decode(CustomerRequestWrapper.self, from: json)
28 | XCTAssertEqual(requestWrapper.request, TestValues.fullyPopulatedPendingRequest)
29 | }
30 |
31 | func test_serializePendingRequest_fullyPopulated() throws {
32 | let serializedRequest = try jsonEncoder.encode(
33 | CustomerRequestWrapper(
34 | request: TestValues.fullyPopulatedPendingRequest
35 | )
36 | )
37 | let fixtureJSON = try fixtureDataForFilename(pendingRequestFilename)
38 |
39 | // The JSON data is unordered, so convert it to a dictionary to compare.
40 | let fixtureDict = try XCTUnwrap(JSONSerialization.jsonObject(with: fixtureJSON) as? [String: AnyHashable])
41 | let serializedDict = try XCTUnwrap(
42 | JSONSerialization.jsonObject(with: serializedRequest) as? [String: AnyHashable]
43 | )
44 | XCTAssertEqual(fixtureDict, serializedDict)
45 | }
46 |
47 | func test_deserializeApprovedRequest_fullyPopulated() throws {
48 | let json = try fixtureDataForFilename(approvedRequestFilename)
49 | let requestWrapper = try jsonDecoder.decode(CustomerRequestWrapper.self, from: json)
50 | XCTAssertEqual(requestWrapper.request, TestValues.fullyPopulatedApprovedRequest)
51 | }
52 |
53 | func test_serializeApprovedRequest_fullyPopulated() throws {
54 | let serializedRequest = try jsonEncoder.encode(
55 | CustomerRequestWrapper(
56 | request: TestValues.fullyPopulatedApprovedRequest
57 | )
58 | )
59 | let fixtureJSON = try fixtureDataForFilename(approvedRequestFilename)
60 |
61 | // The JSON data is unordered, so convert it to a dictionary to compare.
62 | let fixtureDict = try XCTUnwrap(JSONSerialization.jsonObject(with: fixtureJSON) as? [String: AnyHashable])
63 | let serializedDict = try XCTUnwrap(
64 | JSONSerialization.jsonObject(with: serializedRequest) as? [String: AnyHashable]
65 | )
66 | XCTAssertEqual(fixtureDict, serializedDict)
67 | }
68 |
69 | let pendingRequestFilename = "pendingRequest-fullyPopulated"
70 | let approvedRequestFilename = "approvedRequest-fullyPopulated"
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/UIKit/CashAppPaymentMethod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPaymentMethod.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import SwiftUI
18 |
19 | @available(iOS 13.0, *)
20 | public class CashAppPaymentMethod: UIView {
21 |
22 | // MARK: - Private Properties
23 |
24 | private let paymentMethodView: CashAppPaymentMethodView
25 |
26 | // MARK: - Public Properties
27 |
28 | /// View size of the view
29 | public var size: SizingCategory {
30 | get { paymentMethodView.viewModel.size }
31 | set { paymentMethodView.viewModel.size = newValue }
32 | }
33 |
34 | /// Cash Tag representing the customer.
35 | public var cashTag: String {
36 | get { paymentMethodView.viewModel.cashTag }
37 | set { paymentMethodView.viewModel.cashTag = newValue }
38 | }
39 |
40 | // MARK: - Lifecycle
41 |
42 | /**
43 | Initializes a view with the Cash App logo on the left and the Cash Tag
44 | representing the customer on the right or the bottom.
45 |
46 | - Parameters:
47 | - size: The size of the view where the `small` is vertically stacked while `large` is horizontally stacked.
48 | Defaults to `large`.
49 | - cashTag: The Customer ID. Defaults to `nil`.
50 | - usePolychromeAsset: Toggle usage of polychrome UI
51 | */
52 | public init(size: SizingCategory = .large, cashTag: String = "", usePolychromeAsset: Bool = false) {
53 | self.paymentMethodView = CashAppPaymentMethodView(
54 | size: size,
55 | cashTag: cashTag,
56 | usePolychromeAsset: usePolychromeAsset
57 | )
58 | super.init(frame: .zero)
59 | guard let view = makeView() else { return }
60 | addSubview(view)
61 | NSLayoutConstraint.activate([
62 | view.leadingAnchor.constraint(equalTo: leadingAnchor),
63 | view.trailingAnchor.constraint(equalTo: trailingAnchor),
64 | view.topAnchor.constraint(equalTo: topAnchor),
65 | view.bottomAnchor.constraint(equalTo: bottomAnchor),
66 | ])
67 | }
68 |
69 | @available(*, unavailable)
70 | required init?(coder: NSCoder) {
71 | fatalError("init(coder:) has not been implemented")
72 | }
73 |
74 | /// Set the Cash Tag text color
75 | public func setCashTagTextColor(_ color: UIColor) {
76 | guard let uiColor = color.swiftUIColor else { return }
77 | paymentMethodView.viewModel.cashTagTextColor = uiColor
78 | }
79 |
80 | /// Set the Cash Tag font
81 | public func setCashTagFont(_ font: UIFont) {
82 | paymentMethodView.viewModel.cashTagFont = Font(font)
83 | }
84 | }
85 |
86 | // MARK: - View Building
87 |
88 | @available(iOS 13.0, *)
89 | private extension CashAppPaymentMethod {
90 | private func makeView() -> UIView? {
91 | guard let view = UIHostingController(rootView: paymentMethodView).view else {
92 | return nil
93 | }
94 | view.layer.cornerRadius = CashAppPaymentMethodView.Constants.cornerRadius
95 | view.translatesAutoresizingMaskIntoConstraints = false
96 | return view
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/IntegrationErrorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntegrationErrorTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class IntegrationErrorTests: XCTestCase {
21 |
22 | private lazy var jsonEncoder: JSONEncoder = .payKitEncoder()
23 | private lazy var jsonDecoder: JSONDecoder = .payKitDecoder()
24 |
25 | func test_serialize_unauthorizedError() throws {
26 | // Try serializing the error to JSON.
27 | let serializedError = try jsonEncoder.encode(IntegrationErrorWrapper(errors: [TestValues.unauthorizedError]))
28 |
29 | // Try loading the fixture JSON from file.
30 | let fixtureError = try fixtureDataForFilename(unauthorizedFilename, in: .errors)
31 |
32 | // The JSON data is unordered, so convert it to a dictionary to compare.
33 | let fixtureDict = try XCTUnwrap(JSONSerialization.jsonObject(with: fixtureError) as? [String: AnyHashable])
34 | let serializedDict = try XCTUnwrap(
35 | JSONSerialization.jsonObject(with: serializedError) as? [String: AnyHashable]
36 | )
37 | XCTAssertEqual(fixtureDict, serializedDict)
38 | }
39 |
40 | func test_deserialize_unauthorizedError() throws {
41 | let fixtureJSON = try fixtureDataForFilename(unauthorizedFilename, in: .errors)
42 | let errorWrapper = try jsonDecoder.decode(IntegrationErrorWrapper.self, from: fixtureJSON)
43 | XCTAssertEqual(errorWrapper.errors.first, TestValues.unauthorizedError)
44 | }
45 |
46 | func test_serialize_brandNotFoundError() throws {
47 | // Try serializing the error to JSON.
48 | let serializedError = try jsonEncoder.encode(IntegrationErrorWrapper(errors: [TestValues.brandNotFoundError]))
49 |
50 | // Try loading the fixture JSON from file.
51 | let fixtureError = try fixtureDataForFilename(brandNotFoundFilename, in: .errors)
52 |
53 | // The JSON data is unordered, so convert it to a dictionary to compare.
54 | let fixtureDict = try XCTUnwrap(
55 | JSONSerialization.jsonObject(with: fixtureError) as? [String: AnyHashable]
56 | )
57 | let serializedDict = try XCTUnwrap(
58 | JSONSerialization.jsonObject(with: serializedError) as? [String: AnyHashable]
59 | )
60 | XCTAssertEqual(fixtureDict, serializedDict)
61 | }
62 |
63 | func test_deserialize_brandNotFoundError() throws {
64 | let fixtureJSON = try fixtureDataForFilename(brandNotFoundFilename, in: .errors)
65 | let errorWrapper = try jsonDecoder.decode(IntegrationErrorWrapper.self, from: fixtureJSON)
66 | XCTAssertEqual(errorWrapper.errors.first, TestValues.brandNotFoundError)
67 | }
68 |
69 | func test_deserialize_unexpectedError() throws {
70 | // IDEMPOTENCY_KEY_REUSED is an unexpected error, so deserialization should fail.
71 | let fixtureJSON = try fixtureDataForFilename(idempotencyKeyReusedFilename, in: .errors)
72 | XCTAssertThrowsError(try jsonDecoder.decode(IntegrationErrorWrapper.self, from: fixtureJSON)) { error in
73 | XCTAssert(error is Swift.DecodingError)
74 | }
75 | }
76 |
77 | let unauthorizedFilename = "unauthorized"
78 | let brandNotFoundFilename = "brandNotFound"
79 | let idempotencyKeyReusedFilename = "idempotencyKeyReused"
80 | }
81 |
--------------------------------------------------------------------------------
/Tests/PayKitUITests/BaseSnapshotTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseSnapshotTestCase.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import SnapshotTesting
18 | import SwiftUI
19 | import XCTest
20 |
21 | class BaseSnapshotTestCase: XCTestCase {
22 | override func setUp() {
23 | super.setUp()
24 | isRecording = false
25 | }
26 | }
27 |
28 | // MARK: - SnapshotTesting Extensions
29 |
30 | private let lowPrecision: Float = 0.95
31 | private let viewBackground: UIColor = .darkGray
32 |
33 | @available(iOS 13.0, *)
34 | extension Snapshotting where Value: UIView, Format == UIImage {
35 | public static func image(
36 | filling config: ViewImageConfig,
37 | userInterfaceStyle: UIUserInterfaceStyle = .light
38 | ) -> Snapshotting {
39 | Snapshotting.image(
40 | on: config,
41 | precision: lowPrecision,
42 | traits: UITraitCollection(userInterfaceStyle: userInterfaceStyle)
43 | ).pullback { view in
44 | let controller = UIViewController()
45 | controller.view.backgroundColor = .gray
46 | controller.view.addSubview(view)
47 | view.translatesAutoresizingMaskIntoConstraints = false
48 | NSLayoutConstraint.activate([
49 | view.leadingAnchor.constraint(equalTo: controller.view.leadingAnchor),
50 | view.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor),
51 | view.topAnchor.constraint(equalTo: controller.view.topAnchor),
52 | view.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor),
53 | ])
54 | controller.view.frame = UIScreen.main.bounds
55 | return controller
56 | }
57 | }
58 |
59 | public static func image(
60 | centeredIn config: ViewImageConfig,
61 | userInterfaceStyle: UIUserInterfaceStyle = .light
62 | ) -> Snapshotting {
63 | Snapshotting.image(
64 | on: config,
65 | precision: lowPrecision,
66 | traits: UITraitCollection(userInterfaceStyle: userInterfaceStyle)
67 | ).pullback { view in
68 | let controller = UIViewController()
69 | controller.view.backgroundColor = .gray
70 | controller.view.addSubview(view)
71 | view.translatesAutoresizingMaskIntoConstraints = false
72 | NSLayoutConstraint.activate([
73 | view.centerXAnchor.constraint(equalTo: controller.view.centerXAnchor),
74 | view.centerYAnchor.constraint(equalTo: controller.view.centerYAnchor),
75 | ])
76 | controller.view.frame = UIScreen.main.bounds
77 | return controller
78 | }
79 | }
80 | }
81 |
82 | @available(iOS 13.0, *)
83 | extension Snapshotting where Value: View, Format == UIImage {
84 | public static func image(
85 | on config: ViewImageConfig,
86 | userInterfaceStyle: UIUserInterfaceStyle = .light
87 | ) -> Snapshotting {
88 | Snapshotting.image(
89 | on: config,
90 | precision: lowPrecision,
91 | traits: UITraitCollection(userInterfaceStyle: userInterfaceStyle)
92 | ).pullback { view in
93 | let controller = UIHostingController.init(rootView: view)
94 | controller.view.backgroundColor = .darkGray
95 | controller.view.frame = UIScreen.main.bounds
96 | return controller
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Analytics/AnalyticsClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsClient.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | class AnalyticsClient {
20 |
21 | // MARK: - Properties
22 |
23 | private let restService: RESTService
24 |
25 | private let apiPath = "2.0/log/eventstream"
26 | private var baseURL: URL {
27 | URL(string: apiPath, relativeTo: endpoint.baseURL)!
28 | }
29 |
30 | private var requestHeaders: [String: String] {
31 | [
32 | "Accept": "application/json",
33 | "Content-Type": "application/json",
34 | ]
35 | }
36 |
37 | private let encoder: JSONEncoder = .eventStream2Encoder()
38 |
39 | // MARK: - Public Properties
40 |
41 | private let endpoint: Endpoint
42 |
43 | // MARK: - Lifecycle
44 |
45 | init(restService: RESTService, endpoint: Endpoint) {
46 | self.restService = restService
47 | self.endpoint = endpoint
48 | }
49 |
50 | // MARK: - Public
51 |
52 | func upload(
53 | appName: String,
54 | events: [AnalyticsEvent],
55 | completion: @escaping (Result) -> Void
56 | ) {
57 | let params = events.map { EventStream2Params(appName: appName, event: $0) }
58 | let eventsCollection = EventStream2CollectionParams(events: params)
59 |
60 | let jsonParams: Data
61 | do {
62 | jsonParams = try encoder.encode(eventsCollection)
63 | } catch {
64 | DispatchQueue.main.async {
65 | completion(.failure(DebugError.parseError(events)))
66 | }
67 | return
68 | }
69 |
70 | var request = URLRequest(url: baseURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 5.0)
71 | request.httpMethod = "POST"
72 | request.allHTTPHeaderFields = requestHeaders
73 | request.httpBody = jsonParams
74 |
75 | restService.execute(
76 | request: request,
77 | retryPolicy: .exponential(maximumNumberOfAttempts: 5)
78 | ) { _, _, error in
79 | if let error {
80 | Log.write("Failed to upload analytics events with error \(error)")
81 | }
82 | DispatchQueue.main.async {
83 | completion(.success(()))
84 | }
85 | }
86 | }
87 | }
88 |
89 | // MARK: - Endpoint
90 |
91 | extension AnalyticsClient {
92 | enum Endpoint {
93 | case production
94 | case staging
95 |
96 | var baseURL: URL {
97 | switch self {
98 | case .production:
99 | return URL(string: "https://api.squareup.com/")!
100 | case .staging:
101 | return URL(string: "https://api.squareupstaging.com/")!
102 | }
103 | }
104 | }
105 | }
106 |
107 | // MARK: - Request Models
108 |
109 | struct EventStream2CollectionParams: Encodable {
110 | let events: [EventStream2Params]
111 | }
112 |
113 | struct EventStream2Params: Encodable {
114 | let appName: String
115 | let event: AnalyticsEvent
116 |
117 | init(appName: String, event: AnalyticsEvent) {
118 | self.appName = appName
119 | self.event = event
120 | }
121 |
122 | enum CodingKeys: CodingKey {
123 | case appName
124 | case catalogName
125 | case recordedAtUsec
126 | case uuid
127 | case jsonData
128 | }
129 |
130 | func encode(to encoder: Encoder) throws {
131 | var container = encoder.container(keyedBy: CodingKeys.self)
132 | try container.encode(appName, forKey: .appName)
133 | try container.encode(event.catalog, forKey: .catalogName)
134 | try container.encode(event.timestamp, forKey: .recordedAtUsec)
135 | try container.encode(event.id.uuidString, forKey: .uuid)
136 | try container.encode(event.jsonString(), forKey: .jsonData)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/PayKit/Errors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | public struct APIError: Error, Codable, Equatable {
20 | let category: Category
21 | let code: ErrorCode
22 | let detail: String?
23 | let field: String?
24 |
25 | enum Category: String, Codable, Equatable {
26 | case API_ERROR
27 | }
28 |
29 | enum ErrorCode: String, Codable, Equatable {
30 | case INTERNAL_SERVER_ERROR
31 | case SERVICE_UNAVAILABLE
32 | case GATEWAY_TIMEOUT
33 | }
34 | }
35 |
36 | public struct IntegrationError: Error, Codable, Equatable {
37 | let category: Category
38 | let code: ErrorCode
39 | let detail: String?
40 | let field: String?
41 |
42 | enum Category: String, Codable, Equatable {
43 | case AUTHENTICATION_ERROR
44 | case BRAND_ERROR
45 | case MERCHANT_ERROR
46 | case INVALID_REQUEST_ERROR
47 | case RATE_LIMIT_ERROR
48 | }
49 |
50 | enum ErrorCode: String, Codable, Equatable {
51 |
52 | // Authentication Errors
53 | case UNAUTHORIZED
54 | case CLIENT_DISABLED
55 | case FORBIDDEN
56 |
57 | // Invalid Request Errors
58 | case VALUE_TOO_LONG
59 | case VALUE_TOO_SHORT
60 | case VALUE_EMPTY
61 | case VALUE_REGEX_MISMATCH
62 | case INVALID_URL
63 | case VALUE_TOO_HIGH
64 | case VALUE_TOO_LOW
65 | case ARRAY_LENGTH_TOO_LONG
66 | case ARRAY_LENGTH_TOO_SHORT
67 | case INVALID_ARRAY_TYPE
68 | case NOT_FOUND
69 | case CONFLICT
70 | case INVALID_STATE_TRANSITION
71 | case CLIENT_NOT_FOUND
72 |
73 | // Rate Limit Errors
74 | case RATE_LIMITED
75 |
76 | // Brand Errors
77 | case BRAND_NOT_FOUND
78 |
79 | // Merchant Errors
80 | case MERCHANT_MISSING_ADDRESS_OR_SITE
81 | }
82 |
83 | static var terminalStateError: IntegrationError {
84 | IntegrationError(
85 | category: .INVALID_REQUEST_ERROR,
86 | code: .INVALID_STATE_TRANSITION,
87 | detail: "The request provided was already in a terminal state.",
88 | field: nil
89 | )
90 | }
91 | }
92 |
93 | public struct UnexpectedError: Error, Codable, Equatable {
94 | let category: String
95 | let code: String
96 | let detail: String?
97 | let field: String?
98 |
99 | static var emptyErrorArray: UnexpectedError {
100 | return UnexpectedError(
101 | category: "API_ERROR",
102 | code: "EMPTY_ERROR_ARRAY",
103 | detail: "The API returned an error, but the `errors` array was empty. " +
104 | "Please report this bug to Cash App Developer Support.",
105 | field: "errors"
106 | )
107 | }
108 |
109 | static func noRedirectURLFor(_ customerRequest: CustomerRequest) -> UnexpectedError {
110 | return UnexpectedError(
111 | category: "API_ERROR",
112 | code: "NO_REDIRECT_URL",
113 | detail: "The API returned a customer request without a `mobileURL` field to use for redirecting. " +
114 | "Customer request ID: \(customerRequest.id). Please report this bug to Cash App Developer Support.",
115 | field: "auth_flow_triggers.mobile_url"
116 | )
117 | }
118 |
119 | static func unknownErrorFor(_ error: Error) -> UnexpectedError {
120 | return UnexpectedError(
121 | category: "UNKNOWN_ERROR",
122 | code: "UNKNOWN_ERROR",
123 | detail: "Received an Error in an unexpected form: \(String(describing: error))",
124 | field: nil
125 | )
126 | }
127 | }
128 |
129 | public enum NetworkError: Error, Equatable {
130 | case noResponse
131 | case nilData(HTTPURLResponse)
132 | case invalidJSON(Data)
133 | case systemError(NSError)
134 | }
135 |
136 | // MARK: - Wrappers for serialization
137 | struct APIErrorWrapper: Codable, Equatable {
138 | let errors: [APIError]
139 | }
140 |
141 | struct IntegrationErrorWrapper: Codable, Equatable {
142 | let errors: [IntegrationError]
143 | }
144 |
145 | struct UnexpectedErrorWrapper: Codable, Equatable {
146 | let errors: [UnexpectedError]
147 | }
148 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/ResilientRestServiceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResilientRESTServiceTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | final class ResilientRESTServiceTests: XCTestCase {
21 |
22 | private var url: URL!
23 |
24 | override func setUp() {
25 | super.setUp()
26 | url = URL(string: "https://api.cash.app/")!
27 | }
28 |
29 | override func tearDown() {
30 | url = nil
31 | super.tearDown()
32 | }
33 |
34 | func test_execute_performs_request_and_calls_handler() {
35 | let requestExpectation = expectation(description: "Executed request")
36 | let mock = MockURLSession { request in
37 | self.XCTAssertEqual(request.url, self.url)
38 | requestExpectation.fulfill()
39 | return (Data(), HTTPURLResponse(), nil)
40 | }
41 |
42 | let handlerExpectation = expectation(description: "Handler called")
43 | let service = ResilientRESTService(urlSession: mock.session)
44 | service.execute(request: URLRequest(url: url), retryPolicy: nil) { data, _, _ in
45 | XCTAssertNotNil(data)
46 | handlerExpectation.fulfill()
47 | }
48 | waitForExpectations(timeout: 0.2)
49 | }
50 |
51 | func test_failure_without_retry_calls_handler() {
52 | let requestExpectation = expectation(description: "Executed request")
53 | let mock = MockURLSession { request in
54 | self.XCTAssertEqual(request.url, self.url)
55 | requestExpectation.fulfill()
56 | return (nil, nil, NSError(domain: "", code: 5))
57 | }
58 |
59 | let handlerExpectation = expectation(description: "Handler called")
60 | let service = ResilientRESTService(urlSession: mock.session)
61 | service.execute(request: URLRequest(url: url), retryPolicy: nil) { _, _, error in
62 | self.XCTAssertEqual((error as? NSError)?.code, 5)
63 | handlerExpectation.fulfill()
64 | }
65 | waitForExpectations(timeout: 2)
66 | }
67 |
68 | func test_execute_with_retry_eventually_fails_and_calls_handler() {
69 | let requestExpectation = expectation(description: "Executed request")
70 | requestExpectation.expectedFulfillmentCount = 2
71 | let mock = MockURLSession { request in
72 | self.XCTAssertEqual(request.url, self.url)
73 | requestExpectation.fulfill()
74 | return (nil, nil, NSError(domain: "", code: 5))
75 | }
76 |
77 | let handlerExpectation = expectation(description: "Handler called")
78 | let service = ResilientRESTService(urlSession: mock.session)
79 | service.execute(
80 | request: URLRequest(url: url),
81 | retryPolicy: .exponential(
82 | delay: 1,
83 | maximumNumberOfAttempts: 1
84 | )
85 | ) { _, _, error in
86 | self.XCTAssertEqual((error as? NSError)?.code, 5)
87 | handlerExpectation.fulfill()
88 | }
89 | waitForExpectations(timeout: 1)
90 | }
91 |
92 | func test_execute_with_retry_stops_after_success() {
93 | let stubError = NSError(domain: "", code: 5)
94 | let stubResponse = "".data(using: .utf8)
95 | let requestExpectation = expectation(description: "Executed request")
96 | requestExpectation.expectedFulfillmentCount = 2
97 |
98 | var responses: [(Data?, URLResponse?, Error?)] = [(nil, nil, stubError), (stubResponse, HTTPURLResponse(), nil)]
99 | let mock = MockURLSession { request in
100 | self.XCTAssertEqual(request.url, self.url)
101 | requestExpectation.fulfill()
102 | return responses.removeFirst()
103 | }
104 |
105 | let handlerExpectation = expectation(description: "Handler called")
106 | let service = ResilientRESTService(urlSession: mock.session)
107 | service.execute(
108 | request: URLRequest(url: url),
109 | retryPolicy:
110 | .exponential(
111 | delay: 1,
112 | maximumNumberOfAttempts: 10
113 | )
114 | ) { data, _, _ in
115 | XCTAssertNotNil(data)
116 | handlerExpectation.fulfill()
117 | }
118 | waitForExpectations(timeout: 1)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/PayKit/Services/Analytics/AnalyticsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsService.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | import UIKit
19 |
20 | protocol AnalyticsService {
21 | func track(_ event: AnalyticsEvent)
22 | }
23 |
24 | class EventStream2: AnalyticsService {
25 |
26 | // MARK: - Properties
27 |
28 | private let appName: String
29 | private let commonParameters: [String: Loggable]
30 |
31 | private let store: AnalyticsDataSource
32 | private let client: AnalyticsClient
33 |
34 | private let workQueue = OperationQueue()
35 |
36 | private let eventBatchSize: Int
37 |
38 | private var isBatchSizeExceeded: Bool {
39 | store.count >= eventBatchSize
40 | }
41 |
42 | // MARK: - Lifecycle
43 |
44 | /**
45 | - Parameters:
46 | - appName: The appname in EventStream2.
47 | - commonParameters: Parameters to be combined with each event event sending.
48 | - store: The event store used to buffer events before sending.
49 | - client: The networking client used for analytics.
50 | - eventBatchSize: The batch size for events sent to ES2. Default is 5.
51 | - timeInterval: The maximum time interval before events are uploaded. Default is 5 seconds.
52 | - notificationCenter: The Notification Center.
53 | */
54 | init(
55 | appName: String,
56 | commonParameters: [String: Loggable],
57 | store: AnalyticsDataSource,
58 | client: AnalyticsClient,
59 | eventBatchSize: Int = 5,
60 | timeInterval: TimeInterval = 5,
61 | notificationCenter: NotificationCenter = .default) {
62 | self.appName = appName
63 | self.commonParameters = commonParameters
64 | self.store = store
65 | self.client = client
66 | self.eventBatchSize = eventBatchSize
67 |
68 | notificationCenter.addObserver(
69 | self,
70 | selector: #selector(enqueueUpload),
71 | name: UIApplication.willEnterForegroundNotification,
72 | object: nil
73 | )
74 |
75 | notificationCenter.addObserver(
76 | self,
77 | selector: #selector(enqueueUpload),
78 | name: UIApplication.didEnterBackgroundNotification,
79 | object: nil
80 | )
81 |
82 | notificationCenter.addObserver(
83 | self,
84 | selector: #selector(enqueueUpload),
85 | name: UIApplication.willTerminateNotification,
86 | object: nil
87 | )
88 |
89 | let timer = Timer(timeInterval: timeInterval, repeats: true) { [weak self] _ in
90 | self?.enqueueUpload()
91 | }
92 |
93 | RunLoop.main.add(timer, forMode: .common)
94 | }
95 |
96 | // MARK: - Public
97 |
98 | func track(_ event: AnalyticsEvent) {
99 | event.add(commonFields: commonParameters)
100 | workQueue.addOperation { [weak self] in
101 | guard let self else { return }
102 | self.store.insert(event: event)
103 | if self.isBatchSizeExceeded == true {
104 | self.uploadEvents()
105 | }
106 | }
107 | }
108 |
109 | // MARK: - Private
110 |
111 | @objc
112 | private func enqueueUpload() {
113 | workQueue.addOperation { [weak self] in
114 | self?.uploadEvents()
115 | }
116 | }
117 |
118 | private func uploadEvents() {
119 | precondition(OperationQueue.current == workQueue)
120 | guard store.count > 0 else { return }
121 | let events = store.fetch(limit: eventBatchSize)
122 | store.remove(events.map(\.id))
123 | client.upload(appName: appName, events: events) { [weak self] _ in
124 | guard let self else { return }
125 | if self.isBatchSizeExceeded == true {
126 | self.enqueueUpload()
127 | }
128 | }
129 | }
130 | }
131 |
132 | extension EventStream2 {
133 | static let appName = "paykitsdk-ios"
134 |
135 | enum CommonFields: String {
136 | case clientID = "client_id"
137 | case platform = "platform"
138 | case sdkVersion = "sdk_version"
139 | case clientUA = "client_ua"
140 | case environment = "environment"
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/PayKit/ObjcWrapper/ObjCWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObjCWrapper.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | @objc public protocol CAPCashAppPayObserver: NSObjectProtocol {
20 | func stateDidChange(to state: CAPCashAppPayState)
21 | }
22 |
23 | @objc(CAPCashAppPay)
24 | public final class ObjCWrapper: NSObject {
25 | @objc public static var sdkVersion: String {
26 | CashAppPay.version
27 | }
28 |
29 | @objc public static var RedirectNotification: Notification.Name {
30 | CashAppPay.RedirectNotification
31 | }
32 |
33 | private let cashAppPay: CashAppPay
34 | private var observations = [ObjectIdentifier: Observation]()
35 |
36 | @objc public var endpoint: CAPEndpoint {
37 | cashAppPay.endpoint.capEndpoint
38 | }
39 |
40 | @objc public convenience init(clientID: String, endpoint: CAPEndpoint = .production) {
41 | let cashAppPay = CashAppPay(
42 | clientID: clientID,
43 | endpoint: endpoint.endpoint
44 | )
45 | self.init(cashAppPay: cashAppPay)
46 | }
47 |
48 | init(cashAppPay: CashAppPay) {
49 | self.cashAppPay = cashAppPay
50 | super.init()
51 | cashAppPay.addObserver(self)
52 | }
53 |
54 | @objc public func retrieveCustomerRequest(
55 | id: String,
56 | completion: @escaping (CAPCustomerRequest?, NSError?) -> Void
57 | ) {
58 | cashAppPay.retrieveCustomerRequest(id: id) { result in
59 | switch result {
60 | case .success(let customerRequest):
61 | let capCustomerRequest = CAPCustomerRequest(customerRequest: customerRequest)
62 | completion(capCustomerRequest, nil)
63 | case .failure(let error):
64 | completion(nil, error.cashAppPayObjCError)
65 | }
66 | }
67 | }
68 |
69 | @objc public func createCustomerRequest(
70 | params: CAPCreateCustomerRequestParams
71 | ) {
72 | cashAppPay.createCustomerRequest(params: params.createCustomerRequestParams)
73 | }
74 |
75 | @objc public func updateCustomerRequest(
76 | _ request: CAPCustomerRequest,
77 | with params: CAPUpdateCustomerRequestParams
78 | ) {
79 | cashAppPay.updateCustomerRequest(
80 | request.customerRequest,
81 | with: params.updateCustomerRequestParams
82 | )
83 | }
84 |
85 | @objc public func authorizeCustomerRequest(
86 | _ request: CAPCustomerRequest
87 | ) {
88 | cashAppPay.authorizeCustomerRequest(request.customerRequest)
89 | }
90 | }
91 |
92 | // MARK: - Observations
93 |
94 | extension ObjCWrapper {
95 | private struct Observation {
96 | weak var observer: CAPCashAppPayObserver?
97 | }
98 |
99 | @objc public func addObserver(_ observer: CAPCashAppPayObserver) {
100 | let id = ObjectIdentifier(observer)
101 | observations[id] = Observation(observer: observer)
102 | }
103 |
104 | @objc func removeObserver(_ observer: CAPCashAppPayObserver) {
105 | let id = ObjectIdentifier(observer)
106 | observations.removeValue(forKey: id)
107 | }
108 | }
109 |
110 | // MARK: - CashAppPayObserver
111 |
112 | extension ObjCWrapper: CashAppPayObserver {
113 | public func stateDidChange(to state: CashAppPayState) {
114 | for (id, observation) in observations {
115 | // Clean up any observer that is no longer in memory
116 | guard let observer = observation.observer else {
117 | observations.removeValue(forKey: id)
118 | continue
119 | }
120 | observer.stateDidChange(to: state.asCAPCashAppPayState)
121 | }
122 | }
123 | }
124 |
125 | // MARK: - CAPEndpoint
126 |
127 | @objc public enum CAPEndpoint: Int {
128 | case production
129 | case sandbox
130 | case staging
131 |
132 | var endpoint: CashAppPay.Endpoint {
133 | switch self {
134 | case .production: return .production
135 | case .sandbox: return .sandbox
136 | case .staging: return .staging
137 | }
138 | }
139 | }
140 |
141 | private extension CashAppPay.Endpoint {
142 | var capEndpoint: CAPEndpoint {
143 | switch self {
144 | case .production: return .production
145 | case .sandbox: return .sandbox
146 | case .staging: return .staging
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/AccessModifierTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessModifierTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import PayKit
18 | import XCTest
19 |
20 | class AccessModifierTests: XCTestCase {
21 | func test_create_customer_request_params() throws {
22 | let url = try XCTUnwrap(URL(string: "https://block.xyz/"))
23 | let request = CreateCustomerRequestParams(
24 | actions: [
25 | .onFilePayment(
26 | scopeID: "scope",
27 | accountReferenceID: "reference"
28 | ),
29 | ],
30 | channel: .IN_APP,
31 | redirectURL: url,
32 | referenceID: "reference",
33 | metadata: ["meta": "data"]
34 | )
35 |
36 | XCTAssertEqual(request.actions.count, 1)
37 | XCTAssertEqual(request.channel, .IN_APP)
38 | XCTAssertEqual(request.redirectURL, url)
39 | XCTAssertEqual(request.referenceID, "reference")
40 | XCTAssertEqual(request.metadata, ["meta": "data"])
41 | }
42 |
43 | func test_update_customer_request_params() {
44 | let request = UpdateCustomerRequestParams(
45 | actions: [
46 | .onFilePayment(
47 | scopeID: "scope",
48 | accountReferenceID:
49 | "reference"
50 | ),
51 | ],
52 | referenceID: "reference",
53 | metadata: ["meta": "data"]
54 | )
55 |
56 | XCTAssertEqual(request.actions.count, 1)
57 | XCTAssertEqual(request.referenceID, "reference")
58 | XCTAssertEqual(request.metadata, ["meta": "data"])
59 | }
60 |
61 | func test_customer_request() {
62 | let request = TestValues.customerRequest
63 |
64 | XCTAssertEqual(request.id, "GRR_mg3saamyqdm29jj9pqjqkedm")
65 | XCTAssertEqual(request.status, .PENDING)
66 | XCTAssertEqual(request.actions.count, 1)
67 | XCTAssertNotNil(request.authFlowTriggers)
68 | XCTAssertEqual(request.redirectURL, URL(string: "paykitdemo://callback")!)
69 | XCTAssertNotNil(request.createdAt)
70 | XCTAssertNotNil(request.updatedAt)
71 | XCTAssertNotNil(request.expiresAt)
72 | XCTAssertEqual(request.channel, .IN_APP)
73 | XCTAssertNotNil(request.origin)
74 | XCTAssertNotNil(request.authFlowTriggers)
75 | XCTAssertEqual(request.referenceID, "refer_to_me")
76 | XCTAssertNotNil(request.requesterProfile)
77 | XCTAssertNil(request.customerProfile)
78 | XCTAssertNotNil(request.metadata)
79 | }
80 |
81 | func test_auth_flow_triggers() throws {
82 | let flowTriggers = try XCTUnwrap(TestValues.customerRequest.authFlowTriggers)
83 | XCTAssertNotNil(flowTriggers.qrCodeImageURL)
84 | XCTAssertNotNil(flowTriggers.qrCodeSVGURL)
85 | XCTAssertNotNil(flowTriggers.mobileURL)
86 | XCTAssertNotNil(flowTriggers.refreshesAt)
87 | }
88 |
89 | func test_origin() throws {
90 | let origin = try XCTUnwrap(TestValues.customerRequest.origin)
91 | XCTAssertNil(origin.id)
92 | XCTAssertEqual(origin.type, .DIRECT)
93 | }
94 |
95 | func test_grants() throws {
96 | let grant = try XCTUnwrap(TestValues.approvedRequestGrants.first)
97 |
98 | XCTAssertEqual(
99 | grant.id,
100 | "GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe"
101 | + "2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8D"
102 | + "Sf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g"
103 | )
104 | XCTAssertEqual(grant.customerID, "CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc")
105 | XCTAssertEqual(
106 | grant.action,
107 | .onFilePayment(scopeID: "BRAND_9kx6p0mkuo97jnl025q9ni94t", accountReferenceID: "account4")
108 | )
109 | }
110 |
111 | func test_payment_action() {
112 | let onfilePayment = PaymentAction.onFilePayment(scopeID: "scope", accountReferenceID: "reference")
113 | XCTAssertEqual(onfilePayment.scopeID, "scope")
114 | XCTAssertNil(onfilePayment.money)
115 | XCTAssertEqual(onfilePayment.type, .ON_FILE_PAYMENT)
116 | XCTAssertEqual(onfilePayment.accountReferenceID, "reference")
117 | }
118 |
119 | func test_money() {
120 | let money = Money(amount: 100, currency: .USD)
121 | XCTAssertEqual(money.amount, 100)
122 | XCTAssertEqual(money.currency, .USD)
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Demo/PayKitDemo/ComponentsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComponentsViewController.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | import PayKitUI
19 | import UIKit
20 |
21 | class ComponentsViewController: UITableViewController {
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | self.title = "Components"
25 | tableView.separatorStyle = .none
26 | tableView.backgroundColor = .lightGray
27 | }
28 |
29 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
30 | let cell: UITableViewCell
31 | switch indexPath.row {
32 | case 0:
33 | let smallButton = CashAppPayButton(size: .small, onClickHandler: {})
34 | smallButton.isEnabled = false
35 | cell = CashAppDemoCell(title: "Small Button", view: smallButton)
36 | case 1: cell = CashAppDemoCell(title: "Large Button", view: CashAppPayButton(size: .large, onClickHandler: {}))
37 | case 2:
38 | cell = CashAppDemoCell(
39 | title: "Small Button",
40 | view: CashAppPayButton(size: .small, onClickHandler: {}, usePolychromeAsset: true)
41 | )
42 | case 3:
43 | let largeButton = CashAppPayButton(size: .large, onClickHandler: {}, usePolychromeAsset: true)
44 | largeButton.isEnabled = false
45 | cell = CashAppDemoCell(title: "Large Button", view: largeButton)
46 | case 4: cell = CashAppDemoCell(
47 | title: "Small Payment Method",
48 | view: CashAppPaymentMethod(size: .small, cashTag: "$jack")
49 | )
50 | case 5: cell = CashAppDemoCell(
51 | title: "Large Payment Method",
52 | view: CashAppPaymentMethod(size: .large, cashTag: "$jack")
53 | )
54 | case 6: cell = CashAppDemoCell(
55 | title: "Small Payment Method",
56 | view: CashAppPaymentMethod(size: .small, cashTag: "$jack", usePolychromeAsset: true)
57 | )
58 | case 7: cell = CashAppDemoCell(
59 | title: "Large Payment Method",
60 | view: CashAppPaymentMethod(size: .large, cashTag: "$jack", usePolychromeAsset: true)
61 | )
62 | default:
63 | cell = UITableViewCell()
64 | }
65 | cell.overrideUserInterfaceStyle = (indexPath.section == 0) ? .light : .dark
66 | cell.backgroundColor = .lightGray
67 | cell.selectionStyle = .none
68 | return cell
69 | }
70 |
71 | // MARK: - Table View
72 |
73 | override func numberOfSections(in tableView: UITableView) -> Int {
74 | 2
75 | }
76 |
77 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
78 | 8
79 | }
80 |
81 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
82 | (section == 0) ? "Light Mode" : "Dark Mode"
83 | }
84 | }
85 |
86 | // MARK: - View Building
87 |
88 | extension ComponentsViewController {
89 | final class CashAppDemoCell: UITableViewCell {
90 | init(title: String, view: UIView) {
91 | super.init(style: .default, reuseIdentifier: nil)
92 | let titleLabel = UILabel()
93 | titleLabel.font = .boldSystemFont(ofSize: 18)
94 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
95 | titleLabel.text = title
96 |
97 | let stack = UIStackView(arrangedSubviews: [view])
98 | stack.axis = .vertical
99 | stack.distribution = .fillProportionally
100 | stack.alignment = .center
101 | stack.translatesAutoresizingMaskIntoConstraints = false
102 |
103 | contentView.addSubview(titleLabel)
104 | contentView.addSubview(stack)
105 | NSLayoutConstraint.activate([
106 | titleLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
107 | titleLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
108 | titleLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
109 | stack.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
110 | stack.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
111 | stack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
112 | stack.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
113 | ])
114 | }
115 |
116 | @available(*, unavailable)
117 | required init?(coder: NSCoder) {
118 | fatalError("init(coder:) has not been implemented")
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/RELEASE-NOTES.md:
--------------------------------------------------------------------------------
1 | ## Paykit 0.6.3 Release Notes
2 |
3 | Pay Kit 0.6.3 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
4 |
5 | Pay Kit 0.6.3 includes the following new features and enhancements.
6 |
7 | - **PayKitUI Update**
8 |
9 | This is a complete UI Update to button and payment method styles to coincide with Afterpay + Cash App brand convergence UI. An additional parameter called `usePolychromeAsset` will toggle between the two convergence UI styles and will default to false. Since this is not a breaking API change, it's recommended to view and select which asset type (`usePolychromeAsset`) to use. See `ComponentsViewController` to view all variations.
10 |
11 | ## PayKit 0.6.2 Release Notes
12 |
13 | Pay Kit 0.6.2 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
14 |
15 | Pay Kit 0.6.2 includes the following new features and enhancements.
16 |
17 | - **Privacy Manifest**
18 |
19 | PayKit and PayKitUI now each contain a Privacy Manifest.
20 |
21 | ## PayKit 0.6.1 Release Notes
22 |
23 | Pay Kit 0.6.1 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
24 |
25 | Pay Kit 0.6.1 includes the following new features and enhancements.
26 |
27 | - **Objective-C Improvements**
28 |
29 | Fixed a crash in the Objective-C wrapper where calls to `retrieveCustomerRequest` would crash if an error was encountered.
30 |
31 | ## PayKit 0.6.0 Release Notes
32 |
33 | Pay Kit 0.6.0 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
34 |
35 | Pay Kit 0.6.0 includes the following new features and enhancements.
36 |
37 | - **PayKit Supports Objective-C**
38 |
39 | PayKit now provides Objective-C bindings.
40 |
41 | ## PayKit 0.5.1 Release Notes
42 |
43 | Pay Kit 0.5.1 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
44 |
45 | Pay Kit 0.5.1 includes the following new features and enhancements.
46 |
47 | - **Dropped Support for iOS 11 in PayKitUI**
48 |
49 | Increased the minimum supported iOS version to 12.
50 |
51 | - **`PayKitDemo` Demo**
52 |
53 | The *`PayKitDemo` App* includes a button to dismiss the keyboard and better support for light and dark themes.
54 |
55 | ## PayKit 0.5.0 Release Notes
56 |
57 | Pay Kit 0.5.0 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0.
58 |
59 | Pay Kit 0.5.0 includes the following new features and enhancements.
60 |
61 | - **Dropped Support for iOS 11**
62 |
63 | Increased the minimum supported iOS version to 12.
64 |
65 | ## PayKit 0.4.1 Release Notes
66 |
67 | Pay Kit 0.4.1 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
68 |
69 | Pay Kit 0.4.1 includes the following new features and enhancements.
70 |
71 | - **Redacting PII**
72 |
73 | Added redactions for personally identifiable information.
74 |
75 | ## PayKit 0.4.0 Release Notes
76 |
77 | Pay Kit 0.4.0 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
78 |
79 | Pay Kit 0.4.0 includes the following new features and enhancements.
80 |
81 | - **Adds `refreshing` to `CashAppPayState`**
82 |
83 | When calling `authorizeCustomerRequest()` for a CustomerRequest with expired `AuthFlowTriggers` the state machine refreshes the CustomerRequest before redirecting.
84 |
85 | This is a breaking change and clients updating from an older version should show a loading state here.
86 |
87 | ## PayKit 0.3.3 Release Notes
88 |
89 | Pay Kit 0.3.3 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
90 |
91 | Pay Kit 0.3.3 includes the following new features and enhancements.
92 |
93 | - **`retrieveCustomerRequest`**
94 |
95 | Adds a method to retrieve an existing CustomerRequest by ID.
96 |
97 | ## PayKit 0.3.2 Release Notes
98 |
99 | Pay Kit 0.3.2 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
100 |
101 | Pay Kit 0.3.2 includes the following new features and enhancements.
102 |
103 | - **Cocoapods**
104 |
105 | `PayKit` and `PayKitUI` support iOS 11 when imported through Cocoapods.
106 |
107 | ## PayKit 0.3.1 Release Notes
108 |
109 | Pay Kit 0.3.1 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
110 |
111 | Pay Kit 0.3.1 includes the following new features and enhancements.
112 |
113 | - **`PayKitDemo` Demo**
114 |
115 | The *`PayKitDemo` App* now includes a toggle to test in the staging environment.
116 |
117 | ## Pay Kit 0.3.0 Release Notes
118 | Pay Kit 0.3.0 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
119 |
120 | Pay Kit 0.3.0 includes the following new features and enhancements.
121 |
122 | - **Name changes**
123 |
124 | `PayKit` class has been renamed to `CashAppPay`.
125 |
126 | ## Pay Kit 0.2.0 Release Notes
127 |
128 | Pay Kit 0.2.0 supports iOS and requires Xcode 9 or later. The minimum supported Base SDK is 11.0.
129 |
130 | Pay Kit 0.2.0 includes the following new features and enhancements.
131 |
132 | - **Pay Kit Compiles in iOS 11**
133 |
134 | Pay Kit now supports a minimum base SDK version of 11.0 however `PayKitUI` still requires SDK version 13.0.
135 |
136 | - **`CashAppPayButton` Disabled State**
137 |
138 | The `CashAppPayButton` now supports a disabled state when the button is not tappable.
139 |
140 | ## Pay Kit 0.1.0 Release Notes
141 |
142 | Pay Kit 0.1.0 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 13.0.
143 |
144 | Pay Kit 0.1.0 includes the following new features and enhancements.
145 |
146 | - **Pay Kit**
147 |
148 | Pay Kit allows you to accept Cash App Pay in your App.
149 |
150 | - **Pay Kit UI**
151 |
152 | `PayKitUI` contains views provided the user to launch a Pay Kit payment and to present the Cashtag.
153 |
154 | - **Test App**
155 |
156 | The `PayKitDemo` App (`PayKitDemo` project in the Pay Kit workspace) serves as an application to test different modules and features from
157 | the Pay Kit framework.
158 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/Errors+ObjCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors+ObjCTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | final class Errors_ObjCTests: XCTestCase {
21 |
22 | // MARK: - APIError
23 |
24 | func test_apiError() {
25 | let objcError = CAPApiError(apiError: TestValues.internalServerError)
26 | XCTAssertEqual(objcError.category, TestValues.internalServerError.category.capApiErrorCategory)
27 | XCTAssertEqual(objcError.code, TestValues.internalServerError.code.capApiErrorCode.rawValue)
28 | XCTAssertEqual(objcError.detail, TestValues.internalServerError.detail)
29 | XCTAssertEqual(objcError.field, TestValues.internalServerError.field)
30 | }
31 |
32 | func test_apiError_category() {
33 | XCTAssertEqual(APIError.Category.API_ERROR.capApiErrorCategory, .API_ERROR)
34 | }
35 |
36 | func test_apiError_code() {
37 | XCTAssertEqual(APIError.ErrorCode.INTERNAL_SERVER_ERROR.capApiErrorCode, .INTERNAL_SERVER_ERROR)
38 | XCTAssertEqual(APIError.ErrorCode.SERVICE_UNAVAILABLE.capApiErrorCode, .SERVICE_UNAVAILABLE)
39 | XCTAssertEqual(APIError.ErrorCode.GATEWAY_TIMEOUT.capApiErrorCode, .GATEWAY_TIMEOUT)
40 | }
41 |
42 | // MARK: - IntegrationError
43 |
44 | func test_integrationError() {
45 | let objcError = CAPIntegrationError(integrationError: TestValues.brandNotFoundError)
46 | XCTAssertEqual(objcError.category, TestValues.brandNotFoundError.category.capIntegrationErrorCategory)
47 | XCTAssertEqual(objcError.code, TestValues.brandNotFoundError.code.capIntegrationErrorCode.rawValue)
48 | XCTAssertEqual(objcError.detail, TestValues.brandNotFoundError.detail)
49 | XCTAssertEqual(objcError.field, TestValues.brandNotFoundError.field)
50 | }
51 |
52 | func test_integrationError_category() {
53 | XCTAssertEqual(
54 | IntegrationError.Category.AUTHENTICATION_ERROR.capIntegrationErrorCategory,
55 | .AUTHENTICATION_ERROR
56 | )
57 | XCTAssertEqual(IntegrationError.Category.BRAND_ERROR.capIntegrationErrorCategory, .BRAND_ERROR)
58 | XCTAssertEqual(IntegrationError.Category.MERCHANT_ERROR.capIntegrationErrorCategory, .MERCHANT_ERROR)
59 | XCTAssertEqual(
60 | IntegrationError.Category.INVALID_REQUEST_ERROR.capIntegrationErrorCategory,
61 | .INVALID_REQUEST_ERROR
62 | )
63 | XCTAssertEqual(IntegrationError.Category.RATE_LIMIT_ERROR.capIntegrationErrorCategory, .RATE_LIMIT_ERROR)
64 | }
65 |
66 | func test_integrationError_code() {
67 | XCTAssertEqual(IntegrationError.ErrorCode.UNAUTHORIZED.capIntegrationErrorCode, .UNAUTHORIZED)
68 | XCTAssertEqual(IntegrationError.ErrorCode.CLIENT_DISABLED.capIntegrationErrorCode, .CLIENT_DISABLED)
69 | XCTAssertEqual(IntegrationError.ErrorCode.FORBIDDEN.capIntegrationErrorCode, .FORBIDDEN)
70 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_LONG.capIntegrationErrorCode, .VALUE_TOO_LONG)
71 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_SHORT.capIntegrationErrorCode, .VALUE_TOO_SHORT)
72 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_EMPTY.capIntegrationErrorCode, .VALUE_EMPTY)
73 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_REGEX_MISMATCH.capIntegrationErrorCode, .VALUE_REGEX_MISMATCH)
74 | XCTAssertEqual(IntegrationError.ErrorCode.INVALID_URL.capIntegrationErrorCode, .INVALID_URL)
75 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_HIGH.capIntegrationErrorCode, .VALUE_TOO_HIGH)
76 | XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_LOW.capIntegrationErrorCode, .VALUE_TOO_LOW)
77 | XCTAssertEqual(IntegrationError.ErrorCode.ARRAY_LENGTH_TOO_LONG.capIntegrationErrorCode, .ARRAY_LENGTH_TOO_LONG)
78 | XCTAssertEqual(
79 | IntegrationError.ErrorCode.ARRAY_LENGTH_TOO_SHORT.capIntegrationErrorCode,
80 | .ARRAY_LENGTH_TOO_SHORT
81 | )
82 | XCTAssertEqual(IntegrationError.ErrorCode.INVALID_ARRAY_TYPE.capIntegrationErrorCode, .INVALID_ARRAY_TYPE)
83 | XCTAssertEqual(IntegrationError.ErrorCode.NOT_FOUND.capIntegrationErrorCode, .NOT_FOUND)
84 | XCTAssertEqual(IntegrationError.ErrorCode.CONFLICT.capIntegrationErrorCode, .CONFLICT)
85 | XCTAssertEqual(
86 | IntegrationError.ErrorCode.INVALID_STATE_TRANSITION.capIntegrationErrorCode,
87 | .INVALID_STATE_TRANSITION
88 | )
89 | XCTAssertEqual(IntegrationError.ErrorCode.CLIENT_NOT_FOUND.capIntegrationErrorCode, .CLIENT_NOT_FOUND)
90 | XCTAssertEqual(IntegrationError.ErrorCode.RATE_LIMITED.capIntegrationErrorCode, .RATE_LIMITED)
91 | XCTAssertEqual(IntegrationError.ErrorCode.BRAND_NOT_FOUND.capIntegrationErrorCode, .BRAND_NOT_FOUND)
92 | XCTAssertEqual(
93 | IntegrationError.ErrorCode.MERCHANT_MISSING_ADDRESS_OR_SITE.capIntegrationErrorCode,
94 | .MERCHANT_MISSING_ADDRESS_OR_SITE
95 | )
96 | }
97 |
98 | // MARK: - UnexpectedError
99 |
100 | func test_unexpectedError() {
101 | let objcError = CAPUnexpectedError(unexpectedError: TestValues.idempotencyKeyReusedError)
102 | XCTAssertEqual(objcError.category, TestValues.idempotencyKeyReusedError.category)
103 | XCTAssertEqual(objcError.codeMessage, TestValues.idempotencyKeyReusedError.code)
104 | XCTAssertEqual(objcError.detail, TestValues.idempotencyKeyReusedError.detail)
105 | XCTAssertEqual(objcError.field, TestValues.idempotencyKeyReusedError.field)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Tests/PayKitTests/LoggableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggableTests.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | @testable import PayKit
18 | import XCTest
19 |
20 | class LoggableTests: XCTestCase {
21 | func test_loggable_type_object() {
22 | XCTAssert(LoggableType.string("test").object is String)
23 | XCTAssert(LoggableType.int(5).object is Int)
24 | XCTAssert(LoggableType.uint(7).object is UInt)
25 | XCTAssert(LoggableType.bool(false).object is Bool)
26 | }
27 |
28 | func test_loggable_type_description() {
29 | XCTAssertEqual(LoggableType.string("test").description, "test")
30 | XCTAssertEqual(LoggableType.int(5).description, "5")
31 | XCTAssertEqual(LoggableType.uint(7).description, "7")
32 | XCTAssertEqual(LoggableType.bool(false).description, "false")
33 | }
34 |
35 | func test_loggable_int() {
36 | XCTAssertEqual(5.loggableDescription, .int(5))
37 | }
38 |
39 | func test_loggable_bool() {
40 | XCTAssertEqual(true.loggableDescription, .bool(true))
41 | }
42 |
43 | func test_loggable_string() {
44 | XCTAssertEqual("test".loggableDescription, .string("test"))
45 | }
46 |
47 | func test_loggable_url() throws {
48 | let url = try XCTUnwrap("https://block.xyz/")
49 | XCTAssertEqual(url.loggableDescription, .string("https://block.xyz/"))
50 | }
51 |
52 | func test_loggable_date() throws {
53 | let date = try XCTUnwrap(
54 | DateComponents(
55 | calendar: Calendar(identifier: .gregorian),
56 | timeZone: .init(secondsFromGMT: 0),
57 | year: 2022,
58 | month: 4,
59 | day: 20,
60 | hour: 8,
61 | minute: 30,
62 | second: 45
63 | ).date
64 | )
65 | XCTAssertEqual(date.loggableDescription, .uint(1650443445000000))
66 | }
67 |
68 | func test_loggable_dictionary() {
69 | let loggable = ["Hello": "world", "Any": "value"].loggableDescription
70 | XCTAssertEqual(loggable, .string("{\"Any\":\"value\",\"Hello\":\"world\"}"))
71 | }
72 |
73 | func test_loggable_array() {
74 | let loggable = ["Hello", "world", "Any", "value"].loggableDescription
75 | XCTAssertEqual(loggable, .string("[\"Hello\",\"world\",\"Any\",\"value\"]"))
76 | }
77 |
78 | func test_loggable_money() {
79 | let loggable = Money(amount: 100, currency: .USD).loggableDescription
80 | XCTAssertEqual(loggable, .string("{\"amount\":100,\"currency\":\"USD\"}"))
81 | }
82 |
83 | func test_loggable_payment_action_one_time_payment() {
84 | let paymentAction = PaymentAction.oneTimePayment(
85 | scopeID: "test",
86 | money: Money(amount: 100, currency: .USD)
87 | )
88 | let loggable = LoggablePaymentAction(paymentAction: paymentAction).loggableDescription
89 | // swiftlint:disable:next line_length
90 | XCTAssertEqual(loggable, .string("{\"amount\":100,\"currency\":\"USD\",\"scope_id\":\"test\",\"type\":\"ONE_TIME_PAYMENT\"}"))
91 | }
92 |
93 | func test_loggable_payment_action_on_file_payment() {
94 | let paymentAction = PaymentAction.onFilePayment(
95 | scopeID: "test",
96 | accountReferenceID: "account4"
97 | )
98 | let loggable = LoggablePaymentAction(paymentAction: paymentAction).loggableDescription
99 | // swiftlint:disable:next line_length
100 | XCTAssertEqual(loggable, .string(#"{"account_reference_id":"FILTERED","scope_id":"test","type":"ON_FILE_PAYMENT"}"#))
101 | }
102 |
103 | func test_loggable_grant() throws {
104 | let grant = try XCTUnwrap(TestValues.approvedRequestGrants.first)
105 | let loggable = LoggableGrant(grant: grant).loggableDescription
106 | // swiftlint:disable:next line_length
107 | XCTAssertEqual(loggable, .string("{\"action\":{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"},\"channel\":\"IN_APP\",\"created_at\":1666299823249000,\"customer_id\":\"CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc\",\"expires_at\":1823979823159000,\"id\":\"GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g\",\"status\":\"ACTIVE\",\"type\":\"EXTENDED\",\"updated_at\":1666299823249000}"))
108 | }
109 |
110 | func test_loggable_grant_init() throws {
111 | let grant = try XCTUnwrap(TestValues.approvedRequestGrants.first)
112 | let loggableGrant = LoggableGrant(grant: grant)
113 |
114 | XCTAssertEqual(grant.id, loggableGrant.id)
115 | XCTAssertEqual(grant.customerID, loggableGrant.customerID)
116 | XCTAssertEqual(grant.status, loggableGrant.status)
117 | XCTAssertEqual(grant.type, loggableGrant.type)
118 | XCTAssertEqual(grant.channel, loggableGrant.channel)
119 | XCTAssertEqual(grant.createdAt, loggableGrant.createdAt)
120 | XCTAssertEqual(grant.updatedAt, loggableGrant.updatedAt)
121 | XCTAssertEqual(grant.expiresAt, loggableGrant.expiresAt)
122 | }
123 |
124 | func test_loggable_payment_action_init() {
125 | let paymentAction = PaymentAction(
126 | type: .ONE_TIME_PAYMENT,
127 | scopeID: "scopeID",
128 | money: Money(amount: 100, currency: .USD),
129 | accountReferenceID: "account",
130 | clearing: true
131 | )
132 | let loggablePaymentAction = LoggablePaymentAction(paymentAction: paymentAction)
133 |
134 | XCTAssertEqual(loggablePaymentAction.accountReferenceID, "FILTERED")
135 | XCTAssertEqual(paymentAction.scopeID, loggablePaymentAction.scopeID)
136 | XCTAssertEqual(paymentAction.clearing, loggablePaymentAction.clearing)
137 | XCTAssertEqual(paymentAction.money, loggablePaymentAction.money)
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/Shared/Generated/Assets.swift:
--------------------------------------------------------------------------------
1 | // swiftlint:disable all
2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
3 |
4 | #if os(macOS)
5 | import AppKit
6 | #elseif os(iOS)
7 | import UIKit
8 | #elseif os(tvOS) || os(watchOS)
9 | import UIKit
10 | #endif
11 | #if canImport(SwiftUI)
12 | import SwiftUI
13 | #endif
14 |
15 | // Deprecated typealiases
16 | @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0")
17 | internal typealias AssetColorTypeAlias = ColorAsset.Color
18 | @available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0")
19 | internal typealias AssetImageTypeAlias = ImageAsset.Image
20 |
21 | // swiftlint:disable superfluous_disable_command file_length implicit_return
22 |
23 | // MARK: - Asset Catalogs
24 |
25 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name
26 | internal enum Asset {
27 | internal enum Colors {
28 | internal static let buttonTextPrimary = ColorAsset(name: "ButtonTextPrimary")
29 | internal static let polyChrome = ColorAsset(name: "PolyChrome")
30 | internal static let surfacePrimary = ColorAsset(name: "SurfacePrimary")
31 | internal static let surfacePrimaryDisabled = ColorAsset(name: "SurfacePrimaryDisabled")
32 | internal static let surfaceSecondary = ColorAsset(name: "SurfaceSecondary")
33 | internal static let textPrimary = ColorAsset(name: "TextPrimary")
34 | internal static let textSecondary = ColorAsset(name: "TextSecondary")
35 | internal static let textTernary = ColorAsset(name: "TextTernary")
36 | }
37 | internal enum Images {
38 | internal static let monoChromeLogo = ImageAsset(name: "MonoChromeLogo")
39 | internal static let monoChromeLogoReverse = ImageAsset(name: "MonoChromeLogoReverse")
40 | internal static let polyChromeLogo = ImageAsset(name: "PolyChromeLogo")
41 | }
42 | }
43 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name
44 |
45 | // MARK: - Implementation Details
46 |
47 | internal final class ColorAsset {
48 | internal fileprivate(set) var name: String
49 |
50 | #if os(macOS)
51 | internal typealias Color = NSColor
52 | #elseif os(iOS) || os(tvOS) || os(watchOS)
53 | internal typealias Color = UIColor
54 | #endif
55 |
56 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
57 | internal private(set) lazy var color: Color = {
58 | guard let color = Color(asset: self) else {
59 | fatalError("Unable to load color asset named \(name).")
60 | }
61 | return color
62 | }()
63 |
64 | #if os(iOS) || os(tvOS)
65 | @available(iOS 11.0, tvOS 11.0, *)
66 | internal func color(compatibleWith traitCollection: UITraitCollection) -> Color {
67 | let bundle = BundleToken.bundle
68 | guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
69 | fatalError("Unable to load color asset named \(name).")
70 | }
71 | return color
72 | }
73 | #endif
74 |
75 | #if canImport(SwiftUI)
76 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
77 | internal var swiftUIColor: SwiftUI.Color {
78 | SwiftUI.Color(asset: self)
79 | }
80 | #endif
81 |
82 | fileprivate init(name: String) {
83 | self.name = name
84 | }
85 | }
86 |
87 | internal extension ColorAsset.Color {
88 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
89 | convenience init?(asset: ColorAsset) {
90 | let bundle = BundleToken.bundle
91 | #if os(iOS) || os(tvOS)
92 | self.init(named: asset.name, in: bundle, compatibleWith: nil)
93 | #elseif os(macOS)
94 | self.init(named: NSColor.Name(asset.name), bundle: bundle)
95 | #elseif os(watchOS)
96 | self.init(named: asset.name)
97 | #endif
98 | }
99 | }
100 |
101 | #if canImport(SwiftUI)
102 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
103 | internal extension SwiftUI.Color {
104 | init(asset: ColorAsset) {
105 | let bundle = BundleToken.bundle
106 | self.init(asset.name, bundle: bundle)
107 | }
108 | }
109 | #endif
110 |
111 | internal struct ImageAsset {
112 | internal fileprivate(set) var name: String
113 |
114 | #if os(macOS)
115 | internal typealias Image = NSImage
116 | #elseif os(iOS) || os(tvOS) || os(watchOS)
117 | internal typealias Image = UIImage
118 | #endif
119 |
120 | @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
121 | internal var image: Image {
122 | let bundle = BundleToken.bundle
123 | #if os(iOS) || os(tvOS)
124 | let image = Image(named: name, in: bundle, compatibleWith: nil)
125 | #elseif os(macOS)
126 | let name = NSImage.Name(self.name)
127 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
128 | #elseif os(watchOS)
129 | let image = Image(named: name)
130 | #endif
131 | guard let result = image else {
132 | fatalError("Unable to load image asset named \(name).")
133 | }
134 | return result
135 | }
136 |
137 | #if os(iOS) || os(tvOS)
138 | @available(iOS 8.0, tvOS 9.0, *)
139 | internal func image(compatibleWith traitCollection: UITraitCollection) -> Image {
140 | let bundle = BundleToken.bundle
141 | guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
142 | fatalError("Unable to load image asset named \(name).")
143 | }
144 | return result
145 | }
146 | #endif
147 |
148 | #if canImport(SwiftUI)
149 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
150 | internal var swiftUIImage: SwiftUI.Image {
151 | SwiftUI.Image(asset: self)
152 | }
153 | #endif
154 | }
155 |
156 | internal extension ImageAsset.Image {
157 | @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
158 | @available(macOS, deprecated,
159 | message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
160 | convenience init?(asset: ImageAsset) {
161 | #if os(iOS) || os(tvOS)
162 | let bundle = BundleToken.bundle
163 | self.init(named: asset.name, in: bundle, compatibleWith: nil)
164 | #elseif os(macOS)
165 | self.init(named: NSImage.Name(asset.name))
166 | #elseif os(watchOS)
167 | self.init(named: asset.name)
168 | #endif
169 | }
170 | }
171 |
172 | #if canImport(SwiftUI)
173 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
174 | internal extension SwiftUI.Image {
175 | init(asset: ImageAsset) {
176 | let bundle = BundleToken.bundle
177 | self.init(asset.name, bundle: bundle)
178 | }
179 |
180 | init(asset: ImageAsset, label: Text) {
181 | let bundle = BundleToken.bundle
182 | self.init(asset.name, bundle: bundle, label: label)
183 | }
184 |
185 | init(decorative asset: ImageAsset) {
186 | let bundle = BundleToken.bundle
187 | self.init(decorative: asset.name, bundle: bundle)
188 | }
189 | }
190 | #endif
191 |
192 | // swiftlint:disable convenience_type
193 | private final class BundleToken {
194 | static let bundle: Bundle = {
195 | #if SWIFT_PACKAGE
196 | return Bundle.module
197 | #else
198 | return Bundle(for: BundleToken.self)
199 | #endif
200 | }()
201 | }
202 | // swiftlint:enable convenience_type
203 |
204 |
--------------------------------------------------------------------------------
/Sources/PayKit/CashAppPay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayKit.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | import UIKit
19 |
20 | public class CashAppPay {
21 |
22 | public static let version = "0.6.3"
23 |
24 | public static let RedirectNotification: Notification.Name = Notification.Name("CashAppPayRedirect")
25 |
26 | let stateMachine: StateMachine
27 |
28 | private let networkManager: NetworkManager
29 |
30 | public convenience init(clientID: String, endpoint: Endpoint = .production) {
31 | let networkManager = NetworkManager(clientID: clientID, endpoint: endpoint)
32 | let analytics = EventStream2(
33 | appName: EventStream2.appName,
34 | commonParameters: [
35 | EventStream2.CommonFields.clientID.rawValue: clientID,
36 | EventStream2.CommonFields.platform.rawValue: "iOS",
37 | EventStream2.CommonFields.sdkVersion.rawValue: CashAppPay.version,
38 | EventStream2.CommonFields.clientUA.rawValue: UserAgent.userAgent,
39 | EventStream2.CommonFields.environment.rawValue: endpoint.analyticsField,
40 | ],
41 | store: AnalyticsDataSource(),
42 | client: AnalyticsClient(restService: ResilientRESTService(), endpoint: endpoint.analyticsEndpoint)
43 | )
44 |
45 | let stateMachine = StateMachine(networkManager: networkManager, analyticsService: analytics)
46 | self.init(stateMachine: stateMachine, networkManager: networkManager, endpoint: endpoint)
47 | }
48 |
49 | init(stateMachine: StateMachine, networkManager: NetworkManager, endpoint: Endpoint) {
50 | self.stateMachine = stateMachine
51 | self.networkManager = networkManager
52 | self.endpoint = endpoint
53 | }
54 |
55 | public enum Endpoint {
56 | case production
57 | case sandbox
58 | case staging
59 | }
60 |
61 | /// The endpoint that the requests are routed to
62 | public let endpoint: Endpoint
63 |
64 | /// Fetch an existing CustomerRequest by ID.
65 | public func retrieveCustomerRequest(
66 | id: String,
67 | completion: @escaping (Result) -> Void
68 | ) {
69 | networkManager.retrieveCustomerRequest(id: id, completionHandler: completion)
70 | }
71 |
72 | /// Create a customer request. Registered observers are notified of state changes as the request proceeds.
73 | public func createCustomerRequest(
74 | params: CreateCustomerRequestParams
75 | ) {
76 | stateMachine.state = .creatingCustomerRequest(params)
77 | }
78 |
79 | /// Update an existing customer request. Registered observers are notified of state changes as the request proceeds.
80 | public func updateCustomerRequest(
81 | _ request: CustomerRequest,
82 | with params: UpdateCustomerRequestParams
83 | ) {
84 | switch request.status {
85 | case .APPROVED, .DECLINED:
86 | stateMachine.state = .integrationError(.terminalStateError)
87 | case .PENDING, .PROCESSING:
88 | stateMachine.state = .updatingCustomerRequest(request: request, params: params)
89 | }
90 | }
91 |
92 | public enum AuthorizationMethod {
93 | case DEEPLINK
94 | // case QR_CODE -- future
95 | }
96 |
97 | /// Authorize an existing customer request.
98 | /// Registered observers are notified of state changes as the request proceeds.
99 | public func authorizeCustomerRequest(
100 | _ request: CustomerRequest,
101 | method: AuthorizationMethod = .DEEPLINK
102 | ) {
103 | switch request.status {
104 | case .DECLINED:
105 | stateMachine.state = .integrationError(.terminalStateError)
106 | case .APPROVED:
107 | stateMachine.state = .redirecting(request)
108 | case .PENDING, .PROCESSING:
109 | if request.authFlowTriggers?.isExpired() == false {
110 | stateMachine.state = .redirecting(request)
111 | } else {
112 | stateMachine.state = .refreshing(request)
113 | }
114 | }
115 | }
116 | }
117 |
118 | public enum CashAppPayState: Equatable {
119 | /// Ready for a Create Customer Request to be initiated.
120 | case notStarted
121 | /// CustomerRequest is being created. For information only.
122 | case creatingCustomerRequest(CreateCustomerRequestParams)
123 | /// CustomerRequest is being updated. For information only.
124 | case updatingCustomerRequest(request: CustomerRequest, params: UpdateCustomerRequestParams)
125 | /// CustomerRequest has been created, waiting for customer to press "Pay with Cash App Pay" button.
126 | case readyToAuthorize(CustomerRequest)
127 | /// SDK is redirecting to Cash App for authorization. Show loading indicator if desired.
128 | case redirecting(CustomerRequest)
129 | /// SDK is retrieving authorized CustomerRequest. Show loading indicator if desired.
130 | case polling(CustomerRequest)
131 | /// CustomerRequest was declined. Update UI to tell customer to try again.
132 | case declined(CustomerRequest)
133 | /// CustomerRequest was approved. Update UI to show payment info or $cashtag.
134 | case approved(request: CustomerRequest, grants: [CustomerRequest.Grant])
135 | /// CustomerRequest is being refreshed as a result of the AuthFlowTriggers expiring.
136 | /// Show loading indicator if desired.
137 | case refreshing(CustomerRequest)
138 | /// An error with the Cash App Pay API that can manifest at runtime.
139 | /// If an `APIError` is received, the integration is degraded and Cash App Pay functionality
140 | /// should be temporarily removed from the app's UI.
141 | case apiError(APIError)
142 | /// An error in the integration that should be resolved before shipping to production.
143 | /// Examples include authorization issues, incorrect brand IDs, validation errors, etc.
144 | case integrationError(IntegrationError)
145 | /// A networking error, likely due to poor internet connectivity.
146 | case networkError(NetworkError)
147 | /// An unexpected error. Please report any errors of this kind (and what caused them) to Cash App Developer Support.
148 | case unexpectedError(UnexpectedError)
149 | }
150 |
151 | public protocol CashAppPayObserver: AnyObject {
152 | func stateDidChange(to state: CashAppPayState)
153 | }
154 |
155 | public extension CashAppPay {
156 | func addObserver(_ observer: CashAppPayObserver) {
157 | stateMachine.addObserver(observer)
158 | }
159 |
160 | func removeObserver(_ observer: CashAppPayObserver) {
161 | stateMachine.removeObserver(observer)
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/Sources/PayKitUI/SwiftUI/CashAppPaymentMethodView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CashAppPaymentMethodView.swift
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 | import SwiftUI
19 |
20 | @available(iOS 13.0, *)
21 | public struct CashAppPaymentMethodView: View {
22 |
23 | // MARK: - Public Properties
24 |
25 | @ObservedObject public var viewModel: ViewModel
26 |
27 | private var currentAsset: SwiftUI.Image {
28 | viewModel.usePolychromeAsset ?
29 | Asset.Images.polyChromeLogo.swiftUIImage :
30 | Asset.Images.monoChromeLogoReverse.swiftUIImage
31 | }
32 |
33 | private var currentColor: SwiftUI.Color {
34 | viewModel.usePolychromeAsset ?
35 | Asset.Colors.polyChrome.swiftUIColor :
36 | Asset.Colors.surfaceSecondary.swiftUIColor
37 | }
38 |
39 | // MARK: - Lifecycle
40 |
41 | /**
42 | Initializes a view with the Cash App logo on the left and the Cash Tag representing
43 | the customer on the right or the bottom.
44 |
45 | - Parameters:
46 | - size: The size of the view where the `small` is vertically stacked while `large` is horizontally stacked.
47 | Defaults to `large`.
48 | - cashTag: The Customer ID. Defaults to `nil`.
49 | - cashTagFont: Cash Tag text font.
50 | - cashTagTextColor: Cash Tag text color.
51 | - usePolychromeAsset: Toggle usage of polychrome UI
52 | */
53 | public init(
54 | size: SizingCategory = .large,
55 | cashTag: String,
56 | cashTagFont: Font = Constants.cashTagFont,
57 | cashTagTextColor: Color = Constants.cashTagTextColor,
58 | usePolychromeAsset: Bool = false
59 | ) {
60 | self.viewModel = ViewModel(
61 | size: size,
62 | cashTag: cashTag,
63 | cashTagFont: cashTagFont,
64 | cashTagTextColor: cashTagTextColor,
65 | usePolychromeAsset: usePolychromeAsset
66 | )
67 | }
68 |
69 | public var body: some View {
70 | switch viewModel.size {
71 | case .small:
72 | VStack(alignment: .leading, spacing: Constants.verticalTextSpacing) {
73 | currentAsset
74 | .resizable()
75 | .aspectRatio(contentMode: .fill)
76 | .frame(width: Constants.titleWidth, height: Constants.titleHeight)
77 | .offset(y: Constants.titleVerticalOffset)
78 | .padding(
79 | EdgeInsets(
80 | top: Constants.verticalPadding,
81 | leading: Constants.horizontalPadding,
82 | bottom: .zero,
83 | trailing: Constants.horizontalPadding
84 | )
85 | )
86 | cashTagText
87 | .padding(
88 | EdgeInsets(
89 | top: .zero,
90 | leading: Constants.cashTagInset,
91 | bottom: Constants.verticalPadding,
92 | trailing: .zero
93 | )
94 | )
95 | }.background(currentColor)
96 | .cornerRadius(Constants.cornerRadius)
97 | case .large:
98 | HStack(alignment: .center) {
99 | currentAsset
100 | .resizable()
101 | .aspectRatio(contentMode: .fill)
102 | .frame(width: Constants.titleWidth, height: Constants.titleHeight)
103 | .padding(
104 | EdgeInsets(
105 | top: Constants.verticalPadding,
106 | leading: Constants.horizontalPadding,
107 | bottom: Constants.verticalPadding,
108 | trailing: .zero
109 | )
110 | )
111 | Spacer()
112 | cashTagText
113 | .padding(.trailing, Constants.horizontalPadding)
114 | }.background(currentColor)
115 | .cornerRadius(Constants.cornerRadius)
116 | }
117 | }
118 |
119 | private var cashTagText: some View {
120 | Text(viewModel.cashTag)
121 | .foregroundColor(viewModel.cashTagTextColor)
122 | .font(viewModel.cashTagFont)
123 | }
124 |
125 | public enum Constants {
126 | public static let cashTagFont = Font.system(size: 14)
127 | public static let cashTagTextColor = Asset.Colors.textTernary.swiftUIColor
128 |
129 | static let titleWidth: CGFloat = 127
130 | static let titleHeight: CGFloat = 20
131 | static let cashTagInset: CGFloat = 38
132 | static let titleVerticalOffset: CGFloat = 2
133 | static let horizontalPadding: CGFloat = 8
134 | static let verticalPadding: CGFloat = 12
135 | static let cornerRadius: CGFloat = 8
136 | static let verticalTextSpacing: CGFloat = 2
137 | }
138 | }
139 |
140 | // MARK: - View Model
141 |
142 | @available(iOS 13.0, *)
143 | extension CashAppPaymentMethodView {
144 | public class ViewModel: ObservableObject {
145 | @Published var size: SizingCategory
146 | @Published var cashTag: String
147 | @Published var cashTagFont: Font
148 | @Published var cashTagTextColor: Color
149 | @Published var usePolychromeAsset: Bool
150 | init(
151 | size: SizingCategory,
152 | cashTag: String,
153 | cashTagFont: Font,
154 | cashTagTextColor: Color,
155 | usePolychromeAsset: Bool
156 | ) {
157 | self.size = size
158 | self.cashTag = cashTag
159 | self.cashTagFont = cashTagFont
160 | self.cashTagTextColor = cashTagTextColor
161 | self.usePolychromeAsset = usePolychromeAsset
162 | }
163 | }
164 | }
165 |
166 | // MARK: - Preview
167 |
168 | @available(iOS 13.0, *)
169 | struct CashAppPaymentMethodView_Previews: PreviewProvider {
170 | static var previews: some View {
171 | VStack(alignment: .leading, spacing: 10) {
172 | CashAppPaymentMethodView(size: .large, cashTag: "$jack", usePolychromeAsset: false)
173 | .padding()
174 | CashAppPaymentMethodView(size: .large, cashTag: "", usePolychromeAsset: false)
175 | .padding()
176 | CashAppPaymentMethodView(size: .small, cashTag: "$jack", usePolychromeAsset: false)
177 | .padding()
178 | CashAppPaymentMethodView(size: .small, cashTag: "", usePolychromeAsset: false)
179 | .padding()
180 | CashAppPaymentMethodView(size: .large, cashTag: "$jack", usePolychromeAsset: false)
181 | .padding()
182 | }.background(Color.blue)
183 | }
184 | }
185 |
--------------------------------------------------------------------------------