├── .idea
├── .name
├── .gitignore
├── codeStyles
│ └── codeStyleConfig.xml
├── compiler.xml
├── kotlinc.xml
├── misc.xml
├── copyright
│ ├── Apache_Cash.xml
│ └── profiles_settings.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── core
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ └── drawable
│ │ │ │ ├── cap_btn_background_green.xml
│ │ │ │ ├── cap_btn_background_dark.xml
│ │ │ │ └── cap_btn_background_light_outlined.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── paykit
│ │ │ └── core
│ │ │ ├── android
│ │ │ ├── LogTag.kt
│ │ │ ├── ThreadExtensions.kt
│ │ │ └── ApplicationContextHolder.kt
│ │ │ ├── models
│ │ │ ├── response
│ │ │ │ ├── GrantType.kt
│ │ │ │ ├── ApiErrorResponse.kt
│ │ │ │ ├── Origin.kt
│ │ │ │ ├── CustomerTopLevelResponse.kt
│ │ │ │ ├── RequesterProfile.kt
│ │ │ │ ├── CustomerProfile.kt
│ │ │ │ ├── ApiError.kt
│ │ │ │ ├── AuthFlowTriggers.kt
│ │ │ │ ├── Grant.kt
│ │ │ │ └── CustomerResponseData.kt
│ │ │ ├── sdk
│ │ │ │ ├── CashAppPayCurrency.kt
│ │ │ │ └── CashAppPayPaymentAction.kt
│ │ │ ├── pii
│ │ │ │ ├── PiiContent.kt
│ │ │ │ └── PiiString.kt
│ │ │ ├── request
│ │ │ │ ├── CreateCustomerRequest.kt
│ │ │ │ ├── CustomerRequestData.kt
│ │ │ │ └── CustomerRequestDataFactory.kt
│ │ │ ├── analytics
│ │ │ │ ├── EventStream2Response.kt
│ │ │ │ ├── payloads
│ │ │ │ │ ├── AnalyticsBasePayload.kt
│ │ │ │ │ ├── AnalyticsInitializationPayload.kt
│ │ │ │ │ └── AnalyticsEventListenerPayload.kt
│ │ │ │ └── EventStream2Event.kt
│ │ │ └── common
│ │ │ │ ├── NetworkResult.kt
│ │ │ │ └── Action.kt
│ │ │ ├── utils
│ │ │ ├── UUIDManager.kt
│ │ │ ├── Clock.kt
│ │ │ ├── UUIDManagerRealImpl.kt
│ │ │ ├── Extensions.kt
│ │ │ ├── ClockRealImpl.kt
│ │ │ ├── SingleThreadManager.kt
│ │ │ ├── UserAgentProvider.kt
│ │ │ └── SingleThreadManagerImpl.kt
│ │ │ ├── exceptions
│ │ │ ├── CashAppPayIntegrationException.kt
│ │ │ ├── CashAppPayConnectivityNetworkException.kt
│ │ │ ├── CashAppPayNetworkException.kt
│ │ │ └── CashAppPayApiNetworkException.kt
│ │ │ ├── CashAppPayLifecycleObserver.kt
│ │ │ ├── analytics
│ │ │ ├── AnalyticsEventStream2Event.kt
│ │ │ └── PayKitAnalyticsEventDispatcher.kt
│ │ │ ├── CashAppPayInitializer.kt
│ │ │ ├── network
│ │ │ ├── OkHttpProvider.kt
│ │ │ ├── MoshiProvider.kt
│ │ │ ├── adapters
│ │ │ │ ├── InstantAdapter.kt
│ │ │ │ ├── PiiStringRedactAdapter.kt
│ │ │ │ └── PiiStringClearTextAdapter.kt
│ │ │ └── RetryManager.kt
│ │ │ ├── ui
│ │ │ └── CashAppPayButton.kt
│ │ │ ├── NetworkManager.kt
│ │ │ ├── CashAppPayState.kt
│ │ │ └── impl
│ │ │ └── CashAppPayLifecycleObserverImpl.kt
│ ├── test
│ │ └── java
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── paykit
│ │ │ └── core
│ │ │ ├── fakes
│ │ │ ├── FakeClock.kt
│ │ │ ├── FakeUUIDManager.kt
│ │ │ └── FakeData.kt
│ │ │ ├── utils
│ │ │ ├── UUIDTests.kt
│ │ │ └── ClockTests.kt
│ │ │ ├── UserAgentProviderTests.kt
│ │ │ ├── PiiStringTests.kt
│ │ │ ├── CashAppPayExceptionsTests.kt
│ │ │ ├── CashAppPayAuthorizeTests.kt
│ │ │ ├── CashAppPayLifecycleObserverTests.kt
│ │ │ └── PayKitAnalyticsEventDispatcherImplTest.kt
│ └── testRelease
│ │ └── java
│ │ └── app
│ │ └── cash
│ │ └── paykit
│ │ └── core
│ │ └── CashAppPayProdExceptionsTests.kt
├── proguard-rules.pro
├── consumer-rules.pro
├── build.gradle
└── lint-baseline.xml
├── logging
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── paykit
│ │ │ └── logging
│ │ │ ├── CashAppLogEntry.kt
│ │ │ ├── CashAppLoggerHistory.kt
│ │ │ ├── CashAppLogger.kt
│ │ │ └── CashAppLoggerImpl.kt
│ └── test
│ │ └── java
│ │ └── app
│ │ └── cash
│ │ └── paykit
│ │ └── logging
│ │ ├── CashAppLoggerHistoryTests.kt
│ │ └── CashAppLoggerImplTests.kt
├── lint-baseline.xml
├── proguard-rules.pro
└── build.gradle.kts
├── analytics-core
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── paykit
│ │ │ └── analytics
│ │ │ ├── core
│ │ │ ├── DeliveryListener.kt
│ │ │ ├── Deliverable.kt
│ │ │ ├── DeliveryHandler.kt
│ │ │ └── DeliveryWorker.kt
│ │ │ ├── AnalyticsLogger.kt
│ │ │ ├── persistence
│ │ │ ├── AnalyticEntry.kt
│ │ │ ├── sqlite
│ │ │ │ └── AnalyticsSqLiteHelper.kt
│ │ │ └── EntriesDataSource.kt
│ │ │ └── AnalyticsOptions.kt
│ └── test
│ │ └── java
│ │ └── app
│ │ └── cash
│ │ └── paykit
│ │ └── analytics
│ │ ├── Utils.kt
│ │ └── DeliveryWorkerTest.kt
├── proguard-rules.pro
├── build.gradle
└── lint-baseline.xml
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── license-header.txt
├── .github
├── PULL_REQUEST_TEMPLATE
│ └── codegood.md
├── pull_request_template.md
├── CODEOWNERS
└── workflows
│ ├── build.yml
│ ├── release.yaml
│ └── codeql.yml
├── settings.gradle
├── gradle.properties.kochiku
├── gradle.properties
├── RELEASING.md
├── .gitignore
├── README.md
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | PayKit SDK
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/logging/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/logging/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/analytics-core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/analytics-core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/cash-app-pay-android-sdk/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/core/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cash App Pay
4 |
--------------------------------------------------------------------------------
/logging/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/analytics-core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Feb 09 13:50:05 PST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/codegood.md:
--------------------------------------------------------------------------------
1 | ## Jira Ticket
2 | [Jira Ticket]()
3 |
4 | ## What are you trying to accomplish?
5 |
6 | ## How did you accomplish this?
7 |
8 | ## Steps to manually test this change:
9 |
12 |
13 | ## Visual:
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Jira Ticket
2 | [Jira Ticket]()
3 |
4 | ## What are you trying to accomplish?
5 |
6 | ## How did you accomplish this?
7 |
8 | ## Steps to manually test this change:
9 |
12 |
13 | ## Visual:
14 |
15 | | Before | After |
16 | |--------|-------|
17 | | | |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Pay Kit SDK"
16 | include ':core'
17 | include ':analytics-core'
18 | include ':logging'
19 |
--------------------------------------------------------------------------------
/.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-android
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/cap_btn_background_green.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/cap_btn_background_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/logging/lint-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/cap_btn_background_light_outlined.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/gradle/license-header.txt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) $YEAR Cash App
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 | */
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/android/LogTag.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.android
17 |
18 | const val CAP_TAG = "CashAppPay"
19 |
--------------------------------------------------------------------------------
/.idea/copyright/Apache_Cash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/logging/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/GrantType.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | enum class GrantType {
19 | ONE_TIME,
20 | EXTENDED,
21 | UNKNOWN,
22 | }
23 |
--------------------------------------------------------------------------------
/analytics-core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -dontobfuscate
2 | # Add project specific ProGuard rules here.
3 | # You can control the set of applied configuration files using the
4 | # proguardFiles setting in build.gradle.
5 | #
6 | # For more details, see
7 | # http://developer.android.com/guide/developing/tools/proguard.html
8 |
9 | # If your project uses WebView with JS, uncomment the following
10 | # and specify the fully qualified class name to the JavaScript interface
11 | # class:
12 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
13 | # public *;
14 | #}
15 |
16 | # Uncomment this to preserve the line number information for
17 | # debugging stack traces.
18 | #-keepattributes SourceFile,LineNumberTable
19 |
20 | # If you keep the line number information, uncomment this to
21 | # hide the original source file name.
22 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/UUIDManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | internal interface UUIDManager {
19 |
20 | /**
21 | * Returns a UUID as a String.
22 | */
23 | fun generateUUID(): String
24 | }
25 |
--------------------------------------------------------------------------------
/logging/src/main/java/app/cash/paykit/logging/CashAppLogEntry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | data class CashAppLogEntry(
19 | val level: Int,
20 | val tag: String,
21 | val msg: String,
22 | val throwable: Throwable? = null,
23 | )
24 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayCurrency.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.sdk
17 |
18 | /**
19 | * Supported Cash App Pay Currencies.
20 | */
21 | enum class CashAppPayCurrency(val backendValue: String) {
22 | USD("USD"),
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/Clock.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | internal interface Clock {
19 | /**
20 | * Returns the current Epoch time in microseconds (AKA usec).
21 | */
22 | fun currentTimeInMicroseconds(): Long
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/UUIDManagerRealImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import java.util.UUID
19 |
20 | internal class UUIDManagerRealImpl : UUIDManager {
21 | override fun generateUUID(): String {
22 | return UUID.randomUUID().toString()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | workflow_call:
5 | push:
6 | branches:
7 | - main
8 | tags-ignore:
9 | - '**'
10 | pull_request:
11 |
12 | env:
13 | GRADLE_OPTS: "-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false"
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 |
19 | strategy:
20 | fail-fast: false
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 |
26 | - name: Gradle Setup
27 | uses: gradle/gradle-build-action@v2
28 |
29 | - name: Configure JDK
30 | uses: actions/setup-java@v3
31 | with:
32 | distribution: 'zulu'
33 | java-version: 17
34 |
35 | - name: Static Analysis
36 | run: ./gradlew lint spotlessCheck
37 |
38 | - name: Unit Tests
39 | run: ./gradlew testRelease testDebug --stacktrace
40 |
41 | - name: Build
42 | run: ./gradlew core:assemble
43 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryListener.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.core
17 |
18 | import app.cash.paykit.analytics.persistence.AnalyticEntry
19 |
20 | interface DeliveryListener {
21 | fun onSuccess(entries: List)
22 | fun onError(entries: List)
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | /**
19 | * Executes a lambda if the predicate [R] is null.
20 | *
21 | * Eg.: ```gotData?.(display).orElse { logError() }```
22 | */
23 | internal inline fun R?.orElse(block: () -> R): R {
24 | return this ?: block()
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayIntegrationException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.exceptions
17 |
18 | /**
19 | * This exception gets throw when an illegal operation is performed against the Cash App Pay SDK.
20 | */
21 | class CashAppPayIntegrationException(val description: String) : Exception(description)
22 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/ClockRealImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import java.util.concurrent.TimeUnit
19 |
20 | internal class ClockRealImpl : Clock {
21 | override fun currentTimeInMicroseconds(): Long {
22 | return TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/fakes/FakeClock.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.fakes
17 |
18 | import app.cash.paykit.core.utils.Clock
19 |
20 | class FakeClock : Clock {
21 | companion object {
22 | const val NOW = 123L
23 | }
24 |
25 | override fun currentTimeInMicroseconds(): Long {
26 | return NOW
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/CashAppPayLifecycleObserver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.impl.CashAppPayLifecycleListener
19 |
20 | interface CashAppPayLifecycleObserver {
21 | fun register(newInstance: CashAppPayLifecycleListener)
22 | fun unregister(instanceToRemove: CashAppPayLifecycleListener)
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/ApiErrorResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class ApiErrorResponse(
23 | @Json(name = "errors")
24 | val apiErrors: List,
25 | )
26 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/fakes/FakeUUIDManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.fakes
17 |
18 | import app.cash.paykit.core.utils.UUIDManager
19 |
20 | class FakeUUIDManager : UUIDManager {
21 |
22 | companion object {
23 | const val UUID = "abc"
24 | }
25 | override fun generateUUID(): String {
26 | return UUID
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/pii/PiiContent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.pii
17 |
18 | /**
19 | * This is a marker interface for PII content (Personal Identifiable Information). It is meant to signal to the developer that a object
20 | * of this class contains PII and should be treated as such.
21 | */
22 | interface PiiContent
23 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/pii/PiiString.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.pii
17 |
18 | /**
19 | * A string that has been classified as Personal Identifiable Information (PII).
20 | *
21 | */
22 | class PiiString(private var value: String) : PiiContent {
23 |
24 | override fun toString(): String {
25 | return value
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/Origin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class Origin(
23 | @Json(name = "id")
24 | val id: String?,
25 |
26 | @Json(name = "type")
27 | val type: String,
28 | )
29 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/CustomerTopLevelResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class CustomerTopLevelResponse(
23 | @Json(name = "request")
24 | val customerResponseData: CustomerResponseData,
25 | )
26 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/RequesterProfile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class RequesterProfile(
23 | @Json(name = "logo_url")
24 | val logoUrl: String,
25 | @Json(name = "name")
26 | val name: String,
27 | )
28 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import com.squareup.moshi.Json
20 | import com.squareup.moshi.JsonClass
21 |
22 | @JsonClass(generateAdapter = true)
23 | data class CustomerProfile(
24 | @Json(name = "id")
25 | val id: String,
26 | @Json(name = "cashtag")
27 | val cashTag: PiiString,
28 | )
29 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayConnectivityNetworkException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.exceptions
17 |
18 | import app.cash.paykit.core.exceptions.CashAppPayNetworkErrorType.CONNECTIVITY
19 |
20 | /**
21 | * This exception represents Network connectivity issues, such as network timeout errors.
22 | */
23 | data class CashAppPayConnectivityNetworkException(val e: Exception) :
24 | CashAppPayNetworkException(CONNECTIVITY)
25 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/request/CreateCustomerRequest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.request
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class CreateCustomerRequest(
23 | @Json(name = "idempotency_key")
24 | val idempotencyKey: String? = null,
25 | @Json(name = "request")
26 | val customerRequestData: CustomerRequestData,
27 | )
28 |
--------------------------------------------------------------------------------
/gradle.properties.kochiku:
--------------------------------------------------------------------------------
1 | # This file is for CI BUILDS ONLY!
2 | #------------------ copied from cash-android ---------------------
3 |
4 | org.gradle.jvmargs=-Xmx8192M -XX:+HeapDumpOnOutOfMemoryError
5 | org.gradle.caching=true
6 |
7 | com.squareup.cash.remote.cache.enabled=true
8 |
9 | #------------------ from our repo ---------------------
10 |
11 |
12 | android.useAndroidX=true
13 | android.enableJetifier=false
14 |
15 |
16 | # AndroidX package structure to make it clearer which packages are bundled with the
17 | # Android operating system, and which are packaged with your app's APK
18 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
19 | android.useAndroidX=true
20 |
21 | # Automatically convert third-party libraries to use AndroidX
22 | android.enableJetifier=true
23 |
24 | # Use this property to enable support to the new architecture.
25 | # This will allow you to use TurboModules and the Fabric render in
26 | # your application. You should enable this flag either if you want
27 | # to write custom TurboModules/Fabric components OR use libraries that
28 | # are providing them.
29 | newArchEnabled=false
30 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Response.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.analytics
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class EventStream2Response(
23 | @Json(name = "failure_count")
24 | val failureCount: Int,
25 | @Json(name = "invalid_count")
26 | val invalidCount: Int,
27 | @Json(name = "success_count")
28 | val successCount: Int,
29 | )
30 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayNetworkException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.exceptions
17 |
18 | /**
19 | * This exception represents a network related issue. Subclasses of this will be used to represent higher granularity.
20 | * See [CashAppPayNetworkErrorType] for more.
21 | */
22 | open class CashAppPayNetworkException(val errorType: CashAppPayNetworkErrorType) : Exception()
23 |
24 | enum class CashAppPayNetworkErrorType {
25 | API,
26 | CONNECTIVITY,
27 | }
28 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/ApiError.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class ApiError(
23 | @Json(name = "category")
24 | val category: String,
25 | @Json(name = "code")
26 | val code: String,
27 | @Json(name = "detail")
28 | val detail: String?,
29 | @Json(name = "field")
30 | val field_value: String?,
31 | )
32 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayApiNetworkException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.exceptions
17 |
18 | import app.cash.paykit.core.exceptions.CashAppPayNetworkErrorType.API
19 |
20 | /**
21 | * This exception encapsulates all of the metadata provided by an API error.
22 | */
23 | data class CashAppPayApiNetworkException(
24 | val category: String,
25 | val code: String,
26 | val detail: String?,
27 | val field_value: String?,
28 | ) :
29 | CashAppPayNetworkException(API)
30 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsBasePayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.analytics.payloads
17 |
18 | open class AnalyticsBasePayload(
19 | // Version of the SDK.
20 | open val sdkVersion: String,
21 |
22 | // User Agent of the app.
23 | open val clientUserAgent: String,
24 |
25 | open val requestPlatform: String,
26 |
27 | open val clientId: String,
28 |
29 | // Environment the SDK is running against. E.: production, sandbox, staging, etc.
30 | open val environment: String,
31 | )
32 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/analytics/AnalyticsEventStream2Event.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.analytics
17 |
18 | import app.cash.paykit.analytics.core.Deliverable
19 |
20 | /**
21 | * Class that represents the payload to be delivered to the ES2 API.
22 | */
23 | internal data class AnalyticsEventStream2Event constructor(
24 | override val content: String,
25 | ) : Deliverable {
26 | override val type = ESEventType
27 | override val metaData = null
28 |
29 | companion object {
30 | const val ESEventType = "AnalyticsEventStream2Event"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/utils/UUIDTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import com.google.common.truth.Truth.assertThat
19 | import org.junit.Test
20 |
21 | class UUIDTests {
22 | @Test
23 | fun `generateUUID should return a different valid UUID each time`() {
24 | val uuidManager = UUIDManagerRealImpl()
25 | val uuid = uuidManager.generateUUID()
26 | assertThat(uuid).isNotEmpty()
27 | assertThat(uuid).hasLength(36)
28 | val secondUuid = uuidManager.generateUUID()
29 | assertThat(secondUuid).isNotEqualTo(uuid)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Event.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.analytics
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class EventStream2Event(
23 | @Json(name = "app_name")
24 | val appName: String,
25 | @Json(name = "catalog_name")
26 | val catalogName: String,
27 | @Json(name = "json_data")
28 | val jsonData: String,
29 | @Json(name = "recorded_at_usec")
30 | val recordedAt: Long,
31 | @Json(name = "uuid")
32 | val uuid: String,
33 | )
34 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/AuthFlowTriggers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 | import kotlinx.datetime.Instant
21 |
22 | @JsonClass(generateAdapter = true)
23 | data class AuthFlowTriggers(
24 | @Json(name = "mobile_url")
25 | val mobileUrl: String,
26 | @Json(name = "qr_code_image_url")
27 | val qrCodeImageUrl: String,
28 | @Json(name = "qr_code_svg_url")
29 | val qrCodeSvgUrl: String,
30 | @Json(name = "refreshes_at")
31 | val refreshesAt: Instant,
32 | )
33 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/core/Deliverable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.core
17 |
18 | /**
19 | * Represents data that needs to be delivered
20 | */
21 | interface Deliverable {
22 | /** A descriptive name of the deliverable. We will use this value to match the `Deliverable` with the appropriate `DeliveryHandler` implementation. */
23 | val type: String
24 |
25 | /** A String representing the content to be delivered. */
26 | val content: String
27 |
28 | /** Meta data that you want to associate with the deliverable (optional) */
29 | val metaData: String?
30 | }
31 |
--------------------------------------------------------------------------------
/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | import java.util.LinkedList
19 |
20 | internal class CashAppLoggerHistory {
21 | companion object {
22 | private const val HISTORY_MAX_SIZE = 5000
23 | }
24 |
25 | private val history = LinkedList()
26 |
27 | fun log(entry: CashAppLogEntry) {
28 | history.add(entry)
29 | if (history.size > HISTORY_MAX_SIZE) {
30 | history.removeFirst()
31 | }
32 | }
33 |
34 | fun retrieveLogs(): List {
35 | return history.toList()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/core/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
18 |
19 |
23 |
24 |
28 |
29 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/common/NetworkResult.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.common
17 |
18 | /**
19 | * This class wraps all I/O-related requests in one of 2 states: [Success] or [Failure].
20 | */
21 | internal sealed interface NetworkResult {
22 |
23 | class Failure(val exception: Exception) : NetworkResult
24 |
25 | class Success(val data: T) : NetworkResult
26 |
27 | companion object {
28 | fun success(data: T): NetworkResult = Success(data)
29 |
30 | fun failure(exception: Exception): NetworkResult =
31 | Failure(exception)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | enum class ThreadPurpose {
19 | REFRESH_AUTH_TOKEN,
20 | CHECK_APPROVAL_STATUS,
21 | DEFERRED_REFRESH,
22 | }
23 |
24 | /**
25 | * A manager class that is responsible for creating and managing threads, and guarantee that
26 | * each [ThreadPurpose] has only one thread at any given time.
27 | */
28 | internal interface SingleThreadManager {
29 | fun createThread(purpose: ThreadPurpose, runnable: Runnable): Thread
30 |
31 | fun interruptThread(purpose: ThreadPurpose)
32 |
33 | fun interruptAllThreads()
34 | }
35 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import com.google.common.truth.Truth.assertThat
19 | import org.junit.Test
20 |
21 | class ClockTests {
22 |
23 | @Test
24 | fun `currentTimeInMicroseconds should return current time in microseconds`() {
25 | val clock = ClockRealImpl()
26 | val currentTimeInMicroseconds = clock.currentTimeInMicroseconds()
27 | assertThat(currentTimeInMicroseconds).isGreaterThan(0L)
28 |
29 | // Microseconds of when the test was written.
30 | assertThat(currentTimeInMicroseconds).isAtLeast(1686318558468000)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/common/Action.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.common
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import com.squareup.moshi.Json
20 | import com.squareup.moshi.JsonClass
21 |
22 | @JsonClass(generateAdapter = true)
23 | data class Action(
24 | @Json(name = "amount")
25 | val amount_cents: Int? = null,
26 | @Json(name = "currency")
27 | val currency: String? = null,
28 | @Json(name = "scope_id")
29 | val scopeId: String,
30 | @Json(name = "type")
31 | val type: String,
32 | @Json(name = "account_reference_id")
33 | val accountReferenceId: PiiString? = null,
34 | )
35 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.request
17 |
18 | import app.cash.paykit.core.models.common.Action
19 | import app.cash.paykit.core.models.pii.PiiString
20 | import com.squareup.moshi.Json
21 | import com.squareup.moshi.JsonClass
22 |
23 | @JsonClass(generateAdapter = true)
24 | data class CustomerRequestData(
25 | @Json(name = "actions")
26 | val actions: List,
27 | @Json(name = "channel")
28 | val channel: String?,
29 | @Json(name = "redirect_url")
30 | val redirectUri: PiiString?,
31 | @Json(name = "reference_id")
32 | val referenceId: PiiString?,
33 | )
34 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/CashAppPayInitializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import android.content.Context
19 | import androidx.annotation.Keep
20 | import androidx.startup.Initializer
21 | import app.cash.paykit.core.android.ApplicationContextHolder
22 |
23 | interface CashAppPayInitializerStub
24 |
25 | @Keep
26 | class CashAppPayInitializer : Initializer {
27 | override fun create(context: Context): CashAppPayInitializerStub {
28 | ApplicationContextHolder.init(context.applicationContext)
29 | return object : CashAppPayInitializerStub {}
30 | }
31 |
32 | override fun dependencies(): List>> {
33 | return emptyList()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | # TODO we could consider creating a github release
4 | # https://www.notion.so/cashappcash/Change-github-triggers-to-build-on-new-Github-Release-8c3da4ce779e440299908e7ec734626a
5 | # from zipline - https://github.com/cashapp/zipline/blob/trunk/.github/workflows/release.yaml#LL111C7-L130C22
6 |
7 | on:
8 | workflow_dispatch:
9 | push:
10 | tags:
11 | - 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z]*'
12 |
13 | jobs:
14 | call-build-workflow:
15 | uses: ./.github/workflows/build.yml
16 |
17 | publish:
18 | runs-on: ubuntu-latest
19 | needs: call-build-workflow
20 |
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 |
25 | - name: Validate Gradle Wrapper
26 | uses: gradle/wrapper-validation-action@v1
27 |
28 | - name: Configure JDK
29 | uses: actions/setup-java@v3
30 | with:
31 | distribution: 'zulu'
32 | java-version: 17
33 |
34 | - name: Publish Artifacts
35 | run: |
36 | ./gradlew clean publish --stacktrace
37 |
38 | env:
39 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
40 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
41 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }}
42 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/OkHttpProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network
17 |
18 | import okhttp3.OkHttpClient
19 | import java.util.concurrent.TimeUnit.MILLISECONDS
20 |
21 | internal object OkHttpProvider {
22 |
23 | fun provideOkHttpClient(): OkHttpClient {
24 | return OkHttpClient.Builder()
25 | .connectTimeout(DEFAULT_NETWORK_TIMEOUT_MILLISECONDS, MILLISECONDS)
26 | .callTimeout(DEFAULT_NETWORK_TIMEOUT_MILLISECONDS, MILLISECONDS)
27 | .readTimeout(DEFAULT_NETWORK_TIMEOUT_MILLISECONDS, MILLISECONDS)
28 | .writeTimeout(DEFAULT_NETWORK_TIMEOUT_MILLISECONDS, MILLISECONDS)
29 | .build()
30 | }
31 |
32 | private const val DEFAULT_NETWORK_TIMEOUT_MILLISECONDS = 60_000L
33 | }
34 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.android
17 |
18 | import app.cash.paykit.logging.CashAppLogger
19 |
20 | /**
21 | * This class is used to wrap a thread start operation in a way that allows for smooth degradation on exception, as well as convenient and consistent error handling.
22 | */
23 | fun Thread.safeStart(errorMessage: String?, logger: CashAppLogger, onError: () -> Unit? = {}) {
24 | try {
25 | start()
26 | } catch (e: IllegalThreadStateException) {
27 | // This can happen if the thread is already started.
28 | logger.logError(CAP_TAG, errorMessage ?: "", e)
29 | onError()
30 | } catch (e: InterruptedException) {
31 | logger.logError(CAP_TAG, errorMessage ?: "", e)
32 | onError()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsLogger.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics
17 |
18 | import android.util.Log
19 | import app.cash.paykit.logging.CashAppLogger
20 |
21 | class AnalyticsLogger(
22 | private val options: AnalyticsOptions,
23 | private val cashAppLogger: CashAppLogger,
24 | ) {
25 | fun v(tag: String, msg: String) {
26 | if (options.logLevel <= Log.VERBOSE) {
27 | cashAppLogger.logVerbose(tag, msg)
28 | }
29 | }
30 |
31 | fun w(tag: String, msg: String) {
32 | if (options.logLevel <= Log.WARN) {
33 | cashAppLogger.logWarning(tag, msg)
34 | }
35 | }
36 |
37 | fun e(tag: String, msg: String, throwable: Throwable? = null) {
38 | if (options.logLevel <= Log.ERROR) {
39 | cashAppLogger.logError(tag, msg, throwable)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import app.cash.paykit.core.network.adapters.InstantAdapter
20 | import app.cash.paykit.core.network.adapters.PiiStringClearTextAdapter
21 | import app.cash.paykit.core.network.adapters.PiiStringRedactAdapter
22 | import com.squareup.moshi.Moshi
23 | import kotlinx.datetime.Instant
24 |
25 | internal object MoshiProvider {
26 | fun provideDefault(redactPii: Boolean = false): Moshi {
27 | val builder = Moshi.Builder()
28 | .add(Instant::class.java, InstantAdapter())
29 |
30 | if (redactPii) {
31 | builder.add(PiiString::class.java, PiiStringRedactAdapter())
32 | } else {
33 | builder.add(PiiString::class.java, PiiStringClearTextAdapter())
34 | }
35 |
36 | return builder.build()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network.adapters
17 |
18 | import com.squareup.moshi.JsonAdapter
19 | import com.squareup.moshi.JsonReader
20 | import com.squareup.moshi.JsonReader.Token.NULL
21 | import com.squareup.moshi.JsonWriter
22 | import kotlinx.datetime.Instant
23 | import kotlinx.datetime.toInstant
24 |
25 | internal class InstantAdapter : JsonAdapter() {
26 |
27 | override fun fromJson(reader: JsonReader): Instant? {
28 | if (reader.peek() == NULL) {
29 | return reader.nextNull()
30 | }
31 | val timeRFC3339 = reader.nextString()
32 | return timeRFC3339.toInstant()
33 | }
34 |
35 | override fun toJson(writer: JsonWriter, value: Instant?) {
36 | if (value == null) {
37 | writer.nullValue()
38 | } else {
39 | writer.value(value.toString())
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/Grant.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import app.cash.paykit.core.models.common.Action
19 | import app.cash.paykit.core.models.response.GrantType.UNKNOWN
20 | import com.squareup.moshi.Json
21 | import com.squareup.moshi.JsonClass
22 |
23 | @JsonClass(generateAdapter = true)
24 | data class Grant(
25 | @Json(name = "id")
26 | val id: String,
27 | @Json(name = "status")
28 | val status: String,
29 | @Json(name = "type")
30 | val type: GrantType = UNKNOWN,
31 | @Json(name = "action")
32 | val action: Action,
33 | @Json(name = "channel")
34 | val channel: String,
35 | @Json(name = "customer_id")
36 | val customerId: String,
37 | @Json(name = "updated_at")
38 | val updatedAt: String,
39 | @Json(name = "created_at")
40 | val createdAt: String,
41 | @Json(name = "expires_at")
42 | val expiresAt: String,
43 | )
44 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/android/ApplicationContextHolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.android
17 |
18 | import android.content.Context
19 | import androidx.annotation.VisibleForTesting
20 | import java.lang.ref.WeakReference
21 |
22 | /**
23 | * Singleton that holds onto our application [Context] as a [WeakReference]
24 | */
25 | internal object ApplicationContextHolder {
26 | private var isInitialized: Boolean = false
27 |
28 | private lateinit var applicationContextReference: WeakReference
29 |
30 | fun init(applicationContext: Context) {
31 | if (isInitialized) {
32 | return
33 | }
34 | isInitialized = true
35 | applicationContextReference = WeakReference(applicationContext.applicationContext)
36 | }
37 |
38 | val applicationContext: Context
39 | get() = applicationContextReference.get()!!
40 |
41 | @VisibleForTesting
42 | fun clearApplicationRef() {
43 | isInitialized = false
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringRedactAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network.adapters
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import com.squareup.moshi.JsonAdapter
20 | import com.squareup.moshi.JsonReader
21 | import com.squareup.moshi.JsonReader.Token.NULL
22 | import com.squareup.moshi.JsonWriter
23 |
24 | /**
25 | * This adapter will redact the value of a [PiiString] when serializing to JSON.
26 | */
27 | internal class PiiStringRedactAdapter : JsonAdapter() {
28 |
29 | override fun fromJson(reader: JsonReader): PiiString? {
30 | if (reader.peek() == NULL) {
31 | return reader.nextNull()
32 | }
33 | val plainString = reader.nextString()
34 | return PiiString(plainString)
35 | }
36 |
37 | override fun toJson(writer: JsonWriter, value: PiiString?) {
38 | if (value == null) {
39 | writer.nullValue()
40 | } else {
41 | writer.value("FILTERED")
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringClearTextAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network.adapters
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import com.squareup.moshi.JsonAdapter
20 | import com.squareup.moshi.JsonReader
21 | import com.squareup.moshi.JsonReader.Token.NULL
22 | import com.squareup.moshi.JsonWriter
23 |
24 | /**
25 | * This adapter will NOT redact the value of a [PiiString] when serializing to JSON.
26 | */
27 | internal class PiiStringClearTextAdapter : JsonAdapter() {
28 |
29 | override fun fromJson(reader: JsonReader): PiiString? {
30 | if (reader.peek() == NULL) {
31 | return reader.nextNull()
32 | }
33 | val plainString = reader.nextString()
34 | return PiiString(plainString)
35 | }
36 |
37 | override fun toJson(writer: JsonWriter, value: PiiString?) {
38 | if (value == null) {
39 | writer.nullValue()
40 | } else {
41 | writer.value(value.toString())
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # RELEASING
2 |
3 | The SDK artifact will be uploaded to Maven Central (SonaType). Snapshots will be uploaded to the
4 | snapshots repository.
5 |
6 | The Github Actions build configuration determines which repository is used. If the version name
7 | contains "SNAPSHOT", it will be uploaded to the snapshots repository. If it contains a normal
8 | SEMVER, then it will upload to Maven Central.
9 |
10 | ## Maven Publishing
11 |
12 | Create a new tag with the format `v{SEMVER}` and publish the tag to git.
13 |
14 | Github actions should build and upload the AAR artifacts using the version declared in the
15 | root [build.gradle](./build.gradle)
16 |
17 | ### Releasing new SNAPSHOT build
18 |
19 | To release a new snapshot build do the following steps:
20 |
21 | - Open a PR where you increment build version properties `NEXT_VERSION` and `allprojects.version`,
22 | which can be found in the root `build.gradle` file
23 | - After merging, switch back to `main` and create a tag for the new release and suffix it
24 | with `-SNAPSHOT`. Eg.: `git tag v0.0.1-SNAPSHOT`
25 | - Push the tag (`git push origin tag_name`)
26 |
27 | GitHub Actions will start a release job, and if everything goes well the SNAPSHOT will automatically
28 | uploaded to
29 | the [snapshots repository](https://oss.sonatype.org/index.html#view-repositories;snapshots~browsestorage~/app/cash/paykit/core/maven-metadata.xml)
30 | .
31 |
32 | # Development tasks
33 |
34 | ## Run Android lint on the project
35 |
36 | ```bash
37 | ./gradlew core:lint
38 | ```
39 |
40 | ## Apply Ktlint formatting via Spotless
41 |
42 | ```bash
43 | ./gradlew :core:spotlessApply
44 | ```
45 |
46 | ## Run all Unit Tests
47 |
48 | ```bash
49 | ./gradlew test
50 | ```
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/AnalyticEntry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.persistence
17 |
18 | import android.database.Cursor
19 |
20 | /**
21 | * Object model of the sync_entries database table.
22 | */
23 | data class AnalyticEntry constructor(
24 | val id: Long = 0,
25 | val type: String? = null,
26 | val content: String,
27 | val state: Int = 0,
28 | val metaData: String? = null,
29 | val processId: String? = null,
30 | val version: String? = null,
31 | ) {
32 |
33 | companion object {
34 | const val STATE_NEW = 0
35 | const val STATE_DELIVERY_PENDING = 1
36 | const val STATE_DELIVERY_IN_PROGRESS = 2
37 | const val STATE_DELIVERY_FAILED = 3
38 |
39 | fun from(cursor: Cursor) = AnalyticEntry(
40 | id = cursor.getLong(0),
41 | type = cursor.getString(1),
42 | content = cursor.getString(2),
43 | state = cursor.getInt(3),
44 | metaData = cursor.getString(4),
45 | processId = cursor.getString(5),
46 | version = cursor.getString(6),
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/ui/CashAppPayButton.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.ui
17 |
18 | import android.content.Context
19 | import android.util.AttributeSet
20 | import android.widget.ImageButton
21 |
22 | /**
23 | * Cash App Pay button. Should be used in conjunction with either `CAPButtonStyle.Default`,
24 | * `CAPButtonStyle.Alt`, `CAPButtonStyle.MonochromeDark`, or `CAPButtonStyle.MonochromeLight` styles.
25 | *
26 | * **Note**: Due to its unmanaged nature, the button is merely a stylized button, it's up to developers
27 | * to trigger the correct action on button press, as well as manage any visibility states of the
28 | * button accordingly.
29 | */
30 | class CashAppPayButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, style: Int = 0) :
31 | ImageButton(
32 | context,
33 | attrs,
34 | 0,
35 | style,
36 | ) {
37 |
38 | override fun setEnabled(enabled: Boolean) {
39 | super.setEnabled(enabled)
40 | alpha = if (enabled) {
41 | 1f
42 | } else {
43 | .3f
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/UserAgentProviderTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.utils.UserAgentProvider
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 | import org.junit.runner.RunWith
22 | import org.robolectric.RobolectricTestRunner
23 | import org.robolectric.RuntimeEnvironment
24 |
25 | @RunWith(RobolectricTestRunner::class)
26 | class UserAgentProviderTests {
27 |
28 | @Test
29 | fun testUserAgentFormat() {
30 | val userAgent = UserAgentProvider.provideUserAgent(RuntimeEnvironment.getApplication())
31 | // Example of what Robolectric env. will produce: "app.cash.paykit.core.test (Android 12; robolectric; robolectric; robolectric; en_US) PayKitVersion/0.0.6-SNAPSHOT".
32 | assertThat(userAgent).contains("app.cash.paykit.core.test")
33 | assertThat(userAgent).containsMatch("Android ..")
34 | assertThat(userAgent).containsMatch("PayKitVersion/.")
35 | assertThat(userAgent).contains("robolectric")
36 | assertThat(userAgent).contains("en_US")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/analytics-core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id "com.vanniktech.maven.publish.base"
5 | }
6 |
7 | android {
8 | namespace 'app.cash.paykit.analytics'
9 | compileSdk versions.compileSdk
10 |
11 | defaultConfig {
12 | minSdk versions.minSdk
13 | targetSdk versions.targetSdk
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | resourcePrefix 'paykit_analytics_'
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_17
29 | targetCompatibility JavaVersion.VERSION_17
30 | }
31 | kotlinOptions {
32 | jvmTarget = "17"
33 | }
34 |
35 | kotlin {
36 | jvmToolchain(17)
37 | }
38 |
39 | lintOptions {
40 | abortOnError true
41 | htmlReport true
42 | checkAllWarnings true
43 | warningsAsErrors true
44 | baseline file("lint-baseline.xml")
45 | }
46 |
47 | buildFeatures {
48 | buildConfig = true
49 | }
50 | }
51 |
52 | dependencies {
53 |
54 | testImplementation "junit:junit:$junit_version"
55 | testImplementation "io.mockk:mockk:$mockk_version"
56 |
57 | implementation project(':logging')
58 |
59 | // Robolectric environment.
60 | testImplementation "org.robolectric:robolectric:$robolectric_version"
61 | }
62 |
63 | mavenPublishing {
64 | // AndroidMultiVariantLibrary(publish a sources jar, publish a javadoc jar)
65 | configure(new com.vanniktech.maven.publish.AndroidSingleVariantLibrary("release", true, true))
66 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/UserAgentProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import android.content.Context
19 | import android.os.Build
20 | import app.cash.paykit.core.R
21 | import java.util.*
22 |
23 | internal object UserAgentProvider {
24 |
25 | fun provideUserAgent(context: Context): String {
26 | /**
27 | * User Agent:
28 | * BuildConfig.APPLICATION_ID
29 | * (Android XY
30 | * Build.VERSION.RELEASE
31 | * Build.MANUFACTURER
32 | * Build.BRAND
33 | * Build.MODEL
34 | * Locale.getDefault() )
35 | * PayKitVersion/[PayKit Version]
36 | */
37 | val stb = StringBuilder(context.packageName)
38 | stb.append(" (")
39 | stb.append("Android ")
40 | stb.append(Build.VERSION.RELEASE)
41 | stb.append("; ")
42 | stb.append(Build.MANUFACTURER)
43 | stb.append("; ")
44 | stb.append(Build.BRAND)
45 | stb.append("; ")
46 | stb.append(Build.MODEL)
47 | stb.append("; ")
48 | stb.append(Locale.getDefault())
49 | stb.append(") PayKitVersion/")
50 | stb.append(context.getString(R.string.cap_version))
51 |
52 | return stb.toString()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.utils
17 |
18 | import app.cash.paykit.logging.CashAppLogger
19 |
20 | internal class SingleThreadManagerImpl(private val logger: CashAppLogger) : SingleThreadManager {
21 |
22 | private val threads: MutableMap = mutableMapOf()
23 |
24 | override fun createThread(purpose: ThreadPurpose, runnable: Runnable): Thread {
25 | // Before creating a new thread of a given type, make sure the last one is interrupted.
26 | interruptThread(purpose)
27 |
28 | val thread = Thread(runnable, purpose.name)
29 | threads[purpose] = thread
30 | return thread
31 | }
32 |
33 | override fun interruptThread(purpose: ThreadPurpose) {
34 | try {
35 | threads[purpose]?.interrupt()
36 | } catch (e: Exception) {
37 | logger.logError(TAG, "Failed to interrupt thread: ${purpose.name}", e)
38 | } finally {
39 | threads[purpose] = null
40 | }
41 | }
42 |
43 | override fun interruptAllThreads() {
44 | threads.keys.forEach { interruptThread(it) }
45 | }
46 |
47 | companion object {
48 | private const val TAG = "SingleThreadManager"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/logging/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
2 |
3 | plugins {
4 | id("com.android.library")
5 | id("org.jetbrains.kotlin.android")
6 | id("com.vanniktech.maven.publish.base")
7 | }
8 |
9 | //https://issuetracker.google.com/issues/226095015
10 | com.android.tools.analytics.AnalyticsSettings.optedIn = false
11 |
12 | android {
13 | namespace = "app.cash.paykit.logging"
14 | compileSdk = 33
15 |
16 | defaultConfig {
17 | minSdk = 21
18 |
19 | consumerProguardFiles("consumer-rules.pro")
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = false
25 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility = JavaVersion.VERSION_17
30 | targetCompatibility = JavaVersion.VERSION_17
31 | }
32 |
33 | kotlinOptions {
34 | jvmTarget = "17"
35 | }
36 |
37 | kotlin {
38 | jvmToolchain(17)
39 | }
40 |
41 | lint {
42 | abortOnError = true
43 | htmlReport = true
44 | warningsAsErrors = true
45 | checkAllWarnings = true
46 | baseline = file("lint-baseline.xml")
47 | }
48 | }
49 |
50 | val junit_version = rootProject.extra["junit_version"] as String
51 | val google_truth_version = rootProject.extra["google_truth_version"] as String
52 | val robolectric_version = rootProject.extra["robolectric_version"] as String
53 |
54 | dependencies {
55 | testImplementation("junit:junit:$junit_version")
56 | testImplementation("com.google.truth:truth:$google_truth_version")
57 | testImplementation("org.robolectric:robolectric:$robolectric_version")
58 | }
59 |
60 | mavenPublishing {
61 | // AndroidMultiVariantLibrary(publish a sources jar, publish a javadoc jar)
62 | configure(AndroidSingleVariantLibrary("release", true, true))
63 | }
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/NetworkManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.models.analytics.EventStream2Response
19 | import app.cash.paykit.core.models.common.NetworkResult
20 | import app.cash.paykit.core.models.response.CustomerTopLevelResponse
21 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction
22 | import java.io.IOException
23 |
24 | internal interface NetworkManager {
25 |
26 | @Throws(IOException::class)
27 | fun createCustomerRequest(
28 | clientId: String,
29 | paymentActions: List,
30 | redirectUri: String?,
31 | referenceId: String?,
32 | ): NetworkResult
33 |
34 | @Throws(IOException::class)
35 | fun updateCustomerRequest(
36 | clientId: String,
37 | requestId: String,
38 | referenceId: String?,
39 | paymentActions: List,
40 | ): NetworkResult
41 |
42 | fun retrieveUpdatedRequestData(
43 | clientId: String,
44 | requestId: String,
45 | ): NetworkResult
46 |
47 | fun uploadAnalyticsEvents(eventsAsJson: List): NetworkResult
48 | }
49 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsInitializationPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.analytics.payloads
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | /**
22 | * This payload corresponds to the (mobile_cap_pk_initialization)[https://es-manager.stage.sqprod.co/schema-manager/catalogs/1339] Catalog.
23 | */
24 | @JsonClass(generateAdapter = true)
25 | class AnalyticsInitializationPayload(
26 | /*
27 | * Common fields.
28 | */
29 | @Json(name = "mobile_cap_pk_initialization_sdk_version")
30 | sdkVersion: String,
31 |
32 | @Json(name = "mobile_cap_pk_initialization_client_ua")
33 | clientUserAgent: String,
34 |
35 | @Json(name = "mobile_cap_pk_initialization_platform")
36 | requestPlatform: String,
37 |
38 | @Json(name = "mobile_cap_pk_initialization_client_id")
39 | clientId: String,
40 |
41 | @Json(name = "mobile_cap_pk_initialization_environment")
42 | override val environment: String,
43 |
44 | ) : AnalyticsBasePayload(sdkVersion, clientUserAgent, requestPlatform, clientId, environment) {
45 |
46 | companion object {
47 | const val CATALOG = "mobile_cap_pk_initialization"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/network/RetryManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.network
17 |
18 | import kotlin.time.Duration
19 | import kotlin.time.DurationUnit
20 | import kotlin.time.toDuration
21 |
22 | internal interface RetryManager {
23 | fun shouldRetry(): Boolean
24 |
25 | fun timeUntilNextRetry(): Duration
26 |
27 | fun networkAttemptFailed()
28 |
29 | fun getRetryCount(): Int
30 | }
31 |
32 | internal class RetryManagerOptions(
33 | val maxRetries: Int = 4,
34 | val initialDuration: Duration = 1.5.toDuration(DurationUnit.SECONDS),
35 | )
36 |
37 | /**
38 | * A [RetryManager] implementation with max number of retries and back-off strategy.
39 | */
40 | internal class RetryManagerImpl(
41 | private val retryManagerOptions: RetryManagerOptions,
42 | ) : RetryManager {
43 |
44 | private var durationTillNextRetry = retryManagerOptions.initialDuration
45 | private var retryCount = 0
46 |
47 | override fun shouldRetry(): Boolean {
48 | return retryCount <= retryManagerOptions.maxRetries
49 | }
50 |
51 | override fun timeUntilNextRetry(): Duration {
52 | return durationTillNextRetry
53 | }
54 |
55 | override fun networkAttemptFailed() {
56 | retryCount++
57 | durationTillNextRetry = durationTillNextRetry.times(2)
58 | }
59 |
60 | override fun getRetryCount(): Int = retryCount
61 | }
62 |
--------------------------------------------------------------------------------
/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.fakes.FakeData
19 | import app.cash.paykit.core.impl.CashAppPayImpl
20 | import io.mockk.MockKAnnotations
21 | import io.mockk.impl.annotations.MockK
22 | import io.mockk.mockk
23 | import org.junit.Before
24 | import org.junit.Test
25 |
26 | class CashAppPayProdExceptionsTests {
27 |
28 | @MockK(relaxed = true)
29 | private lateinit var networkManager: NetworkManager
30 |
31 | @Before
32 | fun setup() {
33 | MockKAnnotations.init(this)
34 | }
35 |
36 | @Test
37 | fun `softCrashOrStateException should NOT crash in prod`() {
38 | val payKit = createPayKit(useSandboxEnvironment = false)
39 | val listener = mockk(relaxed = true)
40 | payKit.registerForStateUpdates(listener)
41 | payKit.createCustomerRequest(emptyList(), FakeData.REDIRECT_URI)
42 | }
43 |
44 | private fun createPayKit(useSandboxEnvironment: Boolean) =
45 | CashAppPayImpl(
46 | clientId = FakeData.CLIENT_ID,
47 | networkManager = networkManager,
48 | payKitLifecycleListener = mockk(relaxed = true),
49 | useSandboxEnvironment = useSandboxEnvironment,
50 | analyticsEventDispatcher = mockk(relaxed = true),
51 | logger = mockk(relaxed = true),
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | interface CashAppLogger {
19 |
20 | /**
21 | * Log a message with level VERBOSE.
22 | */
23 | fun logVerbose(tag: String, msg: String)
24 |
25 | /**
26 | * Log a message with level WARNING.
27 | */
28 | fun logWarning(tag: String, msg: String)
29 |
30 | /**
31 | * Log a message with level ERROR. Optionally include a [Throwable] to log along the error message.
32 | */
33 | fun logError(tag: String, msg: String, throwable: Throwable? = null)
34 |
35 | /**
36 | * Retrieve all logs.
37 | */
38 | fun retrieveLogs(): List
39 |
40 | /**
41 | * Retrieves all logs, compiled as a single string.
42 | * Each log entry is separated by two newline characters.
43 | * The format of each log entry is: "LEVEL: MESSAGE".
44 | *
45 | * If you need more control over the format, use [retrieveLogs] instead.
46 | */
47 | fun logsAsString(): String
48 |
49 | /**
50 | * Set a listener to be notified when a new log is added.
51 | */
52 | fun setListener(listener: CashAppLoggerListener)
53 |
54 | /**
55 | * Remove the currently registered listener, if any.
56 | */
57 | fun removeListener()
58 | }
59 |
60 | interface CashAppLoggerListener {
61 | fun onNewLog(log: CashAppLogEntry)
62 | }
63 |
--------------------------------------------------------------------------------
/analytics-core/lint-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
13 |
14 |
15 |
20 |
24 |
25 |
26 |
31 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsEventListenerPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.analytics.payloads
17 |
18 | import com.squareup.moshi.Json
19 | import com.squareup.moshi.JsonClass
20 |
21 | /**
22 | * This payload corresponds to the (mobile_cap_pk_event_listener)[https://es-manager.stage.sqprod.co/schema-manager/catalogs/1340] Catalog.
23 | */
24 | @JsonClass(generateAdapter = true)
25 | class AnalyticsEventListenerPayload(
26 | /*
27 | * Common fields.
28 | */
29 | @Json(name = "mobile_cap_pk_event_listener_sdk_version")
30 | sdkVersion: String,
31 |
32 | @Json(name = "mobile_cap_pk_event_listener_client_ua")
33 | clientUserAgent: String,
34 |
35 | @Json(name = "mobile_cap_pk_event_listener_platform")
36 | requestPlatform: String,
37 |
38 | @Json(name = "mobile_cap_pk_event_listener_client_id")
39 | clientId: String,
40 |
41 | @Json(name = "mobile_cap_pk_event_listener_environment")
42 | override val environment: String,
43 |
44 | /*
45 | * Event Specific fields.
46 | */
47 |
48 | /**
49 | * True if the listener is being added.
50 | */
51 | @Json(name = "mobile_cap_pk_event_listener_is_added")
52 | val isAdded: Boolean,
53 |
54 | ) : AnalyticsBasePayload(sdkVersion, clientUserAgent, requestPlatform, clientId, environment) {
55 |
56 | companion object {
57 | const val CATALOG = "mobile_cap_pk_event_listener"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/cashapp/android-cash-paykit-sdk/actions/workflows/release.yaml) 
2 |
3 | # About
4 |
5 | Cash App Pay Android SDK is a wrapper library around our public APIs that allows merchants to easily
6 | integrate payments with Cash on their native Android checkout flows. Similar SDK projects existing
7 | for Web and iOS.
8 |
9 | # Integrate the SDK
10 |
11 | For detailed documentation on how to integrate this SDK please visit
12 | our [Cash App Developers webpage](https://developers.cash.app/docs/api/technical-documentation/sdks/pay-kit/android-getting-started)
13 | .
14 |
15 | ## Sample App
16 |
17 | A Sample App that showcases our demo merchant can be found
18 | in [here](https://github.com/cashapp/cash-pay-pay-sdk-android-sample-app).
19 |
20 | ## Sandbox App
21 |
22 | For convenience, we recommend developers to leverage
23 | our [Sandbox App](https://developers.cash.app/docs/api/technical-documentation/sandbox/sandbox-app)
24 | to simulate the necessary interactions with Cash App during development stages.
25 | The Sandbox App can be particularly helpful for those who do not possess a Cash App account or live
26 |
27 | # Maven
28 |
29 | The latest version of the SDK can be found here: https://search.maven.org/search?q=g:app.cash.paykit
30 |
31 | License
32 | =======
33 |
34 | Copyright 2023 Cash App
35 |
36 | Licensed under the Apache License, Version 2.0 (the "License");
37 | you may not use this file except in compliance with the License.
38 | You may obtain a copy of the License at
39 |
40 | http://www.apache.org/licenses/LICENSE-2.0
41 |
42 | Unless required by applicable law or agreed to in writing, software
43 | distributed under the License is distributed on an "AS IS" BASIS,
44 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
45 | See the License for the specific language governing permissions and
46 | limitations under the License.
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.analytics
17 |
18 | import app.cash.paykit.core.CashAppPayState
19 | import app.cash.paykit.core.CashAppPayState.Approved
20 | import app.cash.paykit.core.CashAppPayState.CashAppPayExceptionState
21 | import app.cash.paykit.core.models.common.Action
22 | import app.cash.paykit.core.models.response.CustomerResponseData
23 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction
24 |
25 | /**
26 | * Definition of analytics events that we want to capture.
27 | */
28 | internal interface PayKitAnalyticsEventDispatcher {
29 |
30 | fun sdkInitialized()
31 |
32 | fun eventListenerAdded()
33 |
34 | fun eventListenerRemoved()
35 |
36 | fun createdCustomerRequest(
37 | paymentKitActions: List,
38 | apiActions: List,
39 | redirectUri: String?,
40 | )
41 |
42 | fun updatedCustomerRequest(
43 | requestId: String,
44 | paymentKitActions: List,
45 | apiActions: List,
46 | )
47 |
48 | fun genericStateChanged(cashAppPayState: CashAppPayState, customerResponseData: CustomerResponseData?)
49 |
50 | fun stateApproved(approved: Approved)
51 |
52 | fun exceptionOccurred(
53 | payKitExceptionState: CashAppPayExceptionState,
54 | customerResponseData: CustomerResponseData?,
55 | )
56 |
57 | /**
58 | * Command this [PayKitAnalyticsEventDispatcher] to stop executing and discard.
59 | */
60 | fun shutdown()
61 | }
62 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsOptions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics
17 |
18 | import android.util.Log
19 | import kotlin.time.Duration
20 | import kotlin.time.Duration.Companion.seconds
21 |
22 | data class AnalyticsOptions constructor(
23 |
24 | /** Delay in seconds to wait until we begin to deliver events. */
25 | val delay: Duration = 0.seconds,
26 |
27 | /** Interval of time between uploading batches. */
28 | val interval: Duration = 30.seconds,
29 |
30 | /** Number of entries to include per process. */
31 | val maxEntryCountPerProcess: Int = 30,
32 |
33 | /** Number of events to include in a given batch process. */
34 | val batchSize: Int = 10,
35 |
36 | /** The name of the database file on disk. */
37 | val databaseName: String,
38 |
39 | /** The log level. */
40 | val logLevel: Int = Log.ERROR,
41 |
42 | /** whether or not logging is disabled */
43 | val isLoggerDisabled: Boolean = false,
44 |
45 | /** The version code of the application. */
46 | val applicationVersionCode: Int = 0,
47 | ) {
48 | init {
49 | if (!interval.isPositive()) {
50 | Log.e("PayKit", "Options interval must be > 0")
51 | if (BuildConfig.DEBUG) {
52 | throw IllegalArgumentException("Options interval must be > 0")
53 | }
54 | }
55 | if (delay.isNegative()) {
56 | Log.e("PayKit", "Options delay must be >= 0")
57 | if (BuildConfig.DEBUG) {
58 | throw IllegalArgumentException("Options interval must be > 0")
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | import com.google.common.truth.Truth.assertThat
19 | import org.junit.Before
20 | import org.junit.Test
21 |
22 | class CashAppLoggerHistoryTests {
23 | private lateinit var loggerHistory: CashAppLoggerHistory
24 |
25 | @Before
26 | fun setUp() {
27 | loggerHistory = CashAppLoggerHistory()
28 | }
29 |
30 | @Test
31 | fun `test log adds entry to history`() {
32 | val entry = CashAppLogEntry(1, "tag1", "message1")
33 | loggerHistory.log(entry)
34 | assertThat(loggerHistory.retrieveLogs()).contains(entry)
35 | }
36 |
37 | @Test
38 | fun `test log removes first entry when history exceeds max size`() {
39 | val oldEntry = CashAppLogEntry(1, "tag1", "message1")
40 | val newEntry = CashAppLogEntry(2, "tag2", "message2")
41 |
42 | loggerHistory.log(oldEntry)
43 | repeat(5000) {
44 | // this should remove the first "oldEntry" added above.
45 | loggerHistory.log(newEntry)
46 | }
47 |
48 | assertThat(loggerHistory.retrieveLogs()).doesNotContain(oldEntry)
49 | assertThat(loggerHistory.retrieveLogs()).contains(newEntry)
50 | }
51 |
52 | @Test
53 | fun `test retrieveLogs returns a list containing all entries in history`() {
54 | val entries = listOf(
55 | CashAppLogEntry(1, "tag1", "message1"),
56 | CashAppLogEntry(2, "tag2", "message2"),
57 | CashAppLogEntry(3, "tag3", "message3"),
58 | )
59 |
60 | entries.forEach {
61 | loggerHistory.log(it)
62 | }
63 |
64 | assertThat(loggerHistory.retrieveLogs()).containsExactlyElementsIn(entries)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.core
17 |
18 | import app.cash.paykit.analytics.AnalyticsLogger
19 | import app.cash.paykit.analytics.persistence.AnalyticEntry
20 | import app.cash.paykit.analytics.persistence.EntriesDataSource
21 | import app.cash.paykit.analytics.persistence.toCommaSeparatedListIds
22 |
23 | abstract class DeliveryHandler {
24 | abstract val deliverableType: String
25 |
26 | var logger: AnalyticsLogger? = null
27 |
28 | val deliveryListener: DeliveryListener
29 | get() = listener
30 |
31 | private var dataSource: EntriesDataSource? = null
32 |
33 | private val listener = object : DeliveryListener {
34 | override fun onSuccess(entries: List) {
35 | logger?.v(
36 | TAG,
37 | "successful delivery, deleting $deliverableType[" + entries.toCommaSeparatedListIds() + "]",
38 | )
39 | dataSource?.deleteEntry(entries)
40 | }
41 |
42 | override fun onError(entries: List) {
43 | logger?.v(
44 | TAG,
45 | "DELIVERY_FAILED for $deliverableType[" + entries.toCommaSeparatedListIds() + "]",
46 | )
47 | dataSource?.updateStatuses(entries, AnalyticEntry.STATE_DELIVERY_FAILED)
48 | }
49 | }
50 |
51 | internal fun setDependencies(dataSource: EntriesDataSource, logger: AnalyticsLogger) {
52 | this.dataSource = dataSource
53 | this.logger = logger
54 | }
55 |
56 | abstract fun deliver(
57 | entries: List,
58 | deliveryListener: DeliveryListener,
59 | )
60 |
61 | companion object {
62 | private const val TAG = "DeliveryHandler"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/response/CustomerResponseData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.response
17 |
18 | import app.cash.paykit.core.models.common.Action
19 | import app.cash.paykit.core.models.pii.PiiString
20 | import com.squareup.moshi.Json
21 | import com.squareup.moshi.JsonClass
22 | import kotlinx.datetime.Clock.System
23 | import kotlinx.datetime.Instant
24 |
25 | const val STATUS_PENDING = "PENDING"
26 | const val STATUS_PROCESSING = "PROCESSING"
27 | const val STATUS_APPROVED = "APPROVED"
28 | const val STATUS_DECLINED = "DECLINED"
29 |
30 | @JsonClass(generateAdapter = true)
31 | data class CustomerResponseData(
32 | @Json(name = "actions")
33 | val actions: List,
34 | @Json(name = "auth_flow_triggers")
35 | val authFlowTriggers: AuthFlowTriggers?,
36 | @Json(name = "channel")
37 | val channel: String,
38 | @Json(name = "id")
39 | val id: String,
40 | @Json(name = "origin")
41 | val origin: Origin,
42 | @Json(name = "requester_profile")
43 | val requesterProfile: RequesterProfile?,
44 | @Json(name = "status")
45 | val status: String,
46 | @Json(name = "updated_at")
47 | val updatedAt: Instant,
48 | @Json(name = "created_at")
49 | val createdAt: Instant,
50 | @Json(name = "expires_at")
51 | val expiresAt: Instant,
52 | @Json(name = "customer_profile")
53 | val customerProfile: CustomerProfile?,
54 | @Json(name = "grants")
55 | val grants: List?,
56 | @Json(name = "reference_id")
57 | val referenceId: PiiString?,
58 | ) {
59 | fun isAuthTokenExpired(): Boolean {
60 | val now = System.now()
61 | val isExpired = now > (authFlowTriggers?.refreshesAt ?: return false)
62 | return isExpired
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/sqlite/AnalyticsSqLiteHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.persistence.sqlite
17 |
18 | import android.content.Context
19 | import android.database.sqlite.SQLiteDatabase
20 | import android.database.sqlite.SQLiteOpenHelper
21 | import android.util.Log
22 | import app.cash.paykit.analytics.AnalyticsOptions
23 |
24 | class AnalyticsSqLiteHelper(context: Context, options: AnalyticsOptions) :
25 | SQLiteOpenHelper(context, options.databaseName, null, DATABASE_VERSION) {
26 |
27 | private var _database: SQLiteDatabase? = null
28 |
29 | init {
30 | ensureDatabaseIsInitialized()
31 | }
32 |
33 | override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
34 | sqLiteDatabase.execSQL(AnalyticsSQLiteDataSource.SQL_CREATE_TABLE)
35 | sqLiteDatabase.execSQL(AnalyticsSQLiteDataSource.SQL_CREATE_INDEX_FOR_COLUMN_STATE)
36 | sqLiteDatabase.execSQL(AnalyticsSQLiteDataSource.SQL_CREATE_INDEX_FOR_COLUMN_TYPE)
37 | sqLiteDatabase.execSQL(AnalyticsSQLiteDataSource.SQL_CREATE_INDEX_FOR_COLUMN_PROCESS_ID)
38 | }
39 |
40 | override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
41 |
42 | @get:Synchronized val database: SQLiteDatabase
43 | get() {
44 | ensureDatabaseIsInitialized()
45 | return _database!!
46 | }
47 |
48 | private fun ensureDatabaseIsInitialized() {
49 | if (!isDatabaseOpened) {
50 | _database = writableDatabase
51 | Log.d(TAG, "database opened.")
52 | }
53 | }
54 |
55 | private val isDatabaseOpened: Boolean
56 | get() = _database?.isOpen == true
57 |
58 | companion object {
59 | private const val TAG = "AnalyticsSQLiteHelper"
60 | const val DATABASE_VERSION = 1
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.models.pii.PiiString
19 | import app.cash.paykit.core.network.adapters.InstantAdapter
20 | import app.cash.paykit.core.network.adapters.PiiStringClearTextAdapter
21 | import app.cash.paykit.core.network.adapters.PiiStringRedactAdapter
22 | import com.google.common.truth.Truth.assertThat
23 | import com.squareup.moshi.JsonAdapter
24 | import com.squareup.moshi.Moshi
25 | import com.squareup.moshi.adapter
26 | import kotlinx.datetime.Instant
27 | import org.junit.Test
28 |
29 | class PiiStringTests {
30 |
31 | @OptIn(ExperimentalStdlibApi::class)
32 | @Test
33 | fun `test Redact Adapter will redact contents of PiiString`() {
34 | val piiString = PiiString("1234567890")
35 | val moshi = Moshi.Builder()
36 | .add(Instant::class.java, InstantAdapter())
37 | .add(PiiString::class.java, PiiStringRedactAdapter())
38 | .build()
39 |
40 | val serialized: JsonAdapter = moshi.adapter()
41 | assertThat(serialized.toJson(piiString)).isEqualTo("\"FILTERED\"")
42 | }
43 |
44 | @OptIn(ExperimentalStdlibApi::class)
45 | @Test
46 | fun `test Clear Text PiiString Adapter will NOT redact contents of PiiString`() {
47 | val piiString = PiiString("1234567890")
48 | val moshi = Moshi.Builder()
49 | .add(Instant::class.java, InstantAdapter())
50 | .add(PiiString::class.java, PiiStringClearTextAdapter())
51 | .build()
52 |
53 | val serialized: JsonAdapter = moshi.adapter()
54 | assertThat(serialized.toJson(piiString)).isEqualTo("\"$piiString\"")
55 | }
56 |
57 | @Test
58 | fun `test PiiString can be obtained as plain text`() {
59 | val value = "1234567890"
60 | val piiString = PiiString(value)
61 | assertThat(piiString.toString()).isEqualTo(value)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryWorker.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.core
17 |
18 | import app.cash.paykit.analytics.AnalyticsLogger
19 | import app.cash.paykit.analytics.persistence.AnalyticEntry
20 | import app.cash.paykit.analytics.persistence.EntriesDataSource
21 | import app.cash.paykit.analytics.persistence.toCommaSeparatedListIds
22 | import java.util.*
23 | import java.util.concurrent.Callable
24 |
25 | internal class DeliveryWorker(
26 | private val dataSource: EntriesDataSource,
27 | private val handlers: List = emptyList(),
28 | private val logger: AnalyticsLogger,
29 | ) : Callable {
30 | init {
31 | logger.v(TAG, "DeliveryWorker initialized.")
32 | }
33 |
34 | @Throws(Exception::class)
35 | override fun call() {
36 | logger.v(TAG, "Starting delivery [$this]")
37 | for (deliveryHandler in handlers) {
38 | val entryType = deliveryHandler.deliverableType
39 | val processId: String = dataSource.generateProcessId(entryType)
40 | var entries: List =
41 | dataSource.getEntriesForDelivery(processId, entryType)
42 | if (entries.isNotEmpty()) {
43 | logger.v(
44 | TAG,
45 | "Processing %s[%d] | processId=%s".format(Locale.US, entries, entries.size, processId),
46 | )
47 | }
48 | while (entries.isNotEmpty()) {
49 | logger.v(TAG, "DELIVERY_IN_PROGRESS for ids[" + entries.toCommaSeparatedListIds() + "]")
50 | dataSource.updateStatuses(entries, AnalyticEntry.STATE_DELIVERY_IN_PROGRESS)
51 | deliveryHandler.deliver(entries, deliveryHandler.deliveryListener)
52 |
53 | // get the next batch of events to send
54 | entries = dataSource.getEntriesForDelivery(processId, entryType)
55 | if (entries.isNotEmpty()) {
56 | logger.v(
57 | TAG,
58 | "Processing %s[%d] | processId=%s".format(Locale.US, entries, entries.size, processId),
59 | )
60 | }
61 | }
62 | }
63 | logger.v(TAG, "Delivery finished. [$this]")
64 | }
65 |
66 | companion object {
67 | private const val TAG = "DeliveryWorker"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | import android.util.Log
19 |
20 | class CashAppLoggerImpl : CashAppLogger {
21 |
22 | private val history = CashAppLoggerHistory()
23 | private var listener: CashAppLoggerListener? = null
24 |
25 | override fun logVerbose(tag: String, msg: String) {
26 | history.log(CashAppLogEntry(Log.VERBOSE, tag, msg))
27 |
28 | // We purposely don't reuse the same CashAppLogEntry instance here to avoid leaking.
29 | listener?.onNewLog(CashAppLogEntry(Log.VERBOSE, tag, msg))
30 | Log.v(tag, msg)
31 | }
32 |
33 | override fun logWarning(tag: String, msg: String) {
34 | history.log(CashAppLogEntry(Log.WARN, tag, msg))
35 | listener?.onNewLog(CashAppLogEntry(Log.WARN, tag, msg))
36 | Log.w(tag, msg)
37 | }
38 |
39 | override fun logError(tag: String, msg: String, throwable: Throwable?) {
40 | history.log(CashAppLogEntry(Log.ERROR, tag, msg, throwable))
41 | listener?.onNewLog(CashAppLogEntry(Log.ERROR, tag, msg, throwable))
42 | Log.e(tag, msg, throwable)
43 | }
44 |
45 | override fun retrieveLogs(): List {
46 | return history.retrieveLogs()
47 | }
48 |
49 | override fun logsAsString(): String {
50 | return buildString {
51 | for (log in history.retrieveLogs()) {
52 | append(logLevelToString(log.level)).append(": ").append(log.msg)
53 | if (log.throwable != null) {
54 | append("\n").append(" Exception: ").append(log.throwable.cause).append(": ").append(log.throwable.message)
55 | }
56 | append("\n\n")
57 | }
58 | }
59 | }
60 |
61 | private fun logLevelToString(level: Int): String {
62 | return when (level) {
63 | Log.VERBOSE -> "VERBOSE"
64 | Log.DEBUG -> "DEBUG"
65 | Log.INFO -> "INFO"
66 | Log.WARN -> "WARN"
67 | Log.ERROR -> "ERROR"
68 | Log.ASSERT -> "ASSERT"
69 | else -> "UNKNOWN"
70 | }
71 | }
72 |
73 | override fun setListener(listener: CashAppLoggerListener) {
74 | this.listener = listener
75 | }
76 |
77 | override fun removeListener() {
78 | this.listener = null
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayPaymentAction.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.sdk
17 |
18 | import app.cash.paykit.core.CashAppPay
19 |
20 | /**
21 | * This class holds the information necessary for [CashAppPay.createCustomerRequest] to be executed.
22 | */
23 | sealed class CashAppPayPaymentAction(open val scopeId: String?, open val referenceId: String?) {
24 |
25 | /**
26 | * Describes an intent for a client to charge a customer a given amount.
27 | *
28 | * Note the following restrictions when using this action:
29 | *
30 | * - If an amount is provided to the action, the payment charged must exactly equal that amount.
31 | * - If no amount is provided to the action, the payment charged may be any amount (use this for tipping support).
32 | * - If amount is provided, currency must be provided too (and vice versa).
33 | *
34 | * @param currency The type of currency to use for this payment.
35 | * @param amount Amount for this payment (typically in cents or equivalent monetary unit).
36 | * @param scopeId This is analogous with the reference ID, an optional field required for brands and merchants support. If null, client ID will be used instead.
37 | */
38 | data class OneTimeAction(
39 | val currency: CashAppPayCurrency?,
40 | val amount: Int?,
41 | override val scopeId: String? = null,
42 | override val referenceId: String? = null,
43 | ) : CashAppPayPaymentAction(scopeId, referenceId)
44 |
45 | /**
46 | * Describes an intent for a client to store a customer's account, allowing a client to create payments
47 | * or issue refunds for it on a recurring basis.
48 | *
49 | * @param scopeId This is analogous with the reference ID, an optional field required for brands and merchants support. If null, client ID will be used instead.
50 | * @param accountReferenceId Identifier of the account or customer associated to the on file action.
51 | */
52 | data class OnFileAction(
53 | override val scopeId: String? = null,
54 | val accountReferenceId: String? = null,
55 | override val referenceId: String? = null,
56 | ) :
57 | CashAppPayPaymentAction(scopeId, referenceId)
58 | }
59 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.fakes
17 |
18 | import app.cash.paykit.core.models.sdk.CashAppPayCurrency.USD
19 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction
20 |
21 | object FakeData {
22 | const val CLIENT_ID = "fake_client_id"
23 | const val BRAND_ID = "fake_brand_id"
24 | const val REDIRECT_URI = "fake_redirect_uri"
25 | const val FAKE_AMOUNT = 500
26 | const val REQUEST_ID = "Request-id-fake-123"
27 |
28 | const val SDK_VERSION = "1.0.0"
29 | const val USER_AGENT = "Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)"
30 | const val SDK_ENVIRONMENT_SANDBOX = "SANDBOX"
31 |
32 | val oneTimePayment = OneTimeAction(USD, FAKE_AMOUNT, BRAND_ID)
33 |
34 | val validCreateCustomerJSONresponse = """{
35 | "request":{
36 | "id":"GRR_dvm2v6b6wkdrwhcaqefx6tnp",
37 | "status":"PENDING",
38 | "actions":[
39 | {
40 | "type":"ONE_TIME_PAYMENT",
41 | "amount":500,
42 | "currency":"USD",
43 | "scope_id":"BRAND_9kx6p0mkuo97jnl025q9ni94t"
44 | }
45 | ],
46 | "origin":{
47 | "type":"DIRECT"
48 | },
49 | "auth_flow_triggers":{
50 | "qr_code_image_url":"https://sandbox.api.cash.app/qr/sandbox/v1/GRR_dvm2v6b6wkdrwhcaqefx6tnp-n3nf7z?rounded=0&logoColor=0000ff&format=png",
51 | "qr_code_svg_url":"https://sandbox.api.cash.app/qr/sandbox/v1/GRR_dvm2v6b6wkdrwhcaqefx6tnp-n3nf7z?rounded=0&logoColor=0000ff&format=svg",
52 | "mobile_url":"https://sandbox.api.cash.app/sandbox/v1/GRR_dvm2v6b6wkdrwhcaqefx6tnp-n3nf7z?method=mobile_url&type=cap",
53 | "refreshes_at":"2023-02-08T21:01:09.077Z"
54 | },
55 | "created_at":"2023-02-08T21:00:39.105Z",
56 | "updated_at":"2023-02-08T21:00:39.105Z",
57 | "expires_at":"2023-02-08T22:00:39.077Z",
58 | "requester_profile":{
59 | "name":"SDK Hacking: The Brand",
60 | "logo_url":"defaultlogo.jpg"
61 | },
62 | "channel":"IN_APP"
63 | }
64 | }"""
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [ "main" ]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [ "main" ]
14 |
15 | jobs:
16 | analyze:
17 | name: Analyze
18 | runs-on: 'ubuntu-latest'
19 | permissions:
20 | actions: read
21 | contents: read
22 | security-events: write
23 |
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | language: [ 'java' ]
28 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
29 | # Use only 'java' to analyze code written in Java, Kotlin or both
30 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
31 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
32 |
33 | steps:
34 | - name: Checkout repository
35 | uses: actions/checkout@v3
36 |
37 | # Initializes the CodeQL tools for scanning.
38 | - name: Initialize CodeQL
39 | uses: github/codeql-action/init@v2
40 | with:
41 | languages: ${{ matrix.language }}
42 | # If you wish to specify custom queries, you can do so here or in a config file.
43 | # By default, queries listed here will override any specified in a config file.
44 | # Prefix the list here with "+" to use these queries and those in the config file.
45 |
46 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
47 | # queries: security-extended,security-and-quality
48 |
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v2
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
57 |
58 | # If the Autobuild fails above, remove it and uncomment the following three lines.
59 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
60 |
61 | # - run: |
62 | # echo "Run, Build Application using script"
63 | # ./location_of_script_within_repo/buildscript.sh
64 |
65 | - name: Perform CodeQL Analysis
66 | uses: github/codeql-action/analyze@v2
67 | with:
68 | category: "/language:${{matrix.language}}"
69 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.exceptions.CashAppPayIntegrationException
19 | import app.cash.paykit.core.fakes.FakeData
20 | import app.cash.paykit.core.impl.CashAppPayImpl
21 | import app.cash.paykit.core.models.common.NetworkResult
22 | import io.mockk.MockKAnnotations
23 | import io.mockk.every
24 | import io.mockk.impl.annotations.MockK
25 | import io.mockk.mockk
26 | import org.junit.Before
27 | import org.junit.Test
28 |
29 | class CashAppPayExceptionsTests {
30 |
31 | @MockK(relaxed = true)
32 | private lateinit var networkManager: NetworkManager
33 |
34 | @Before
35 | fun setup() {
36 | MockKAnnotations.init(this)
37 | }
38 |
39 | @Test(expected = CashAppPayIntegrationException::class)
40 | fun `should throw on createCustomerRequest if has NOT registered for state updates`() {
41 | val payKit = createPayKit(useSandboxEnvironment = true)
42 | payKit.createCustomerRequest(FakeData.oneTimePayment, FakeData.REDIRECT_URI)
43 | }
44 |
45 | @Test(expected = CashAppPayIntegrationException::class)
46 | fun `should throw during Dev when paymentActions is an empty list`() {
47 | val payKit = createPayKit(useSandboxEnvironment = true)
48 | val listener = mockk(relaxed = true)
49 | payKit.registerForStateUpdates(listener)
50 | payKit.createCustomerRequest(emptyList(), FakeData.REDIRECT_URI)
51 | }
52 |
53 | @Test
54 | fun `logAndSoftCrash should NOT crash in prod`() {
55 | val payKit = createPayKit(useSandboxEnvironment = false)
56 | val listener = mockk(relaxed = true)
57 | payKit.registerForStateUpdates(listener)
58 |
59 | every { networkManager.createCustomerRequest(any(), any(), any(), any()) } returns NetworkResult.failure(
60 | Exception("bad"),
61 | )
62 | payKit.createCustomerRequest(FakeData.oneTimePayment, FakeData.REDIRECT_URI)
63 | }
64 |
65 | private fun createPayKit(useSandboxEnvironment: Boolean) =
66 | CashAppPayImpl(
67 | clientId = FakeData.CLIENT_ID,
68 | networkManager = networkManager,
69 | payKitLifecycleListener = mockk(relaxed = true),
70 | useSandboxEnvironment = useSandboxEnvironment,
71 | analyticsEventDispatcher = mockk(relaxed = true),
72 | logger = mockk(relaxed = true),
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.exceptions.CashAppPayIntegrationException
19 | import app.cash.paykit.core.fakes.FakeData
20 | import app.cash.paykit.core.impl.CashAppPayImpl
21 | import app.cash.paykit.core.models.response.CustomerResponseData
22 | import io.mockk.every
23 | import io.mockk.mockk
24 | import org.junit.Test
25 |
26 | class CashAppPayAuthorizeTests {
27 |
28 | @Test(expected = CashAppPayIntegrationException::class)
29 | fun `should throw if calling authorize before createCustomer`() {
30 | val payKit = createPayKit()
31 | payKit.registerForStateUpdates(mockk())
32 | payKit.authorizeCustomerRequest()
33 | }
34 |
35 | @Test(expected = CashAppPayIntegrationException::class)
36 | fun `should throw on authorizeCustomerRequest if has NOT registered for state updates`() {
37 | val payKit = createPayKit()
38 | val customerResponseData = mockk(relaxed = true)
39 | payKit.authorizeCustomerRequest(customerResponseData)
40 | }
41 |
42 | @Test(expected = IllegalArgumentException::class)
43 | fun `should throw if missing mobileUrl from customer data`() {
44 | val payKit = createPayKit()
45 | val customerResponseData = mockk(relaxed = true)
46 | payKit.registerForStateUpdates(mockk())
47 |
48 | payKit.authorizeCustomerRequest(customerResponseData)
49 | }
50 |
51 | @Test(expected = IllegalArgumentException::class)
52 | fun `should throw if unable to parse mobile url in customer data`() {
53 | val payKit = createPayKit()
54 | val customerResponseData = mockk(relaxed = true) {
55 | every { authFlowTriggers } returns null
56 | }
57 | payKit.registerForStateUpdates(mockk())
58 |
59 | payKit.authorizeCustomerRequest(customerResponseData)
60 | }
61 |
62 | @Test(expected = RuntimeException::class)
63 | fun `should throw on if unable to start mobileUrl activity`() {
64 | val payKit = createPayKit()
65 | val customerResponseData = mockk(relaxed = true) {
66 | every { authFlowTriggers } returns mockk {
67 | every { mobileUrl } returns "http://url"
68 | }
69 | }
70 | payKit.registerForStateUpdates(mockk())
71 |
72 | payKit.authorizeCustomerRequest(customerResponseData)
73 | }
74 |
75 | private fun createPayKit() =
76 | CashAppPayImpl(
77 | clientId = FakeData.CLIENT_ID,
78 | networkManager = mockk(),
79 | payKitLifecycleListener = mockk(relaxed = true),
80 | useSandboxEnvironment = true,
81 | analyticsEventDispatcher = mockk(relaxed = true),
82 | logger = mockk(relaxed = true),
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.models.request
17 |
18 | import app.cash.paykit.core.models.common.Action
19 | import app.cash.paykit.core.models.pii.PiiString
20 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction
21 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction
22 | import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction
23 |
24 | /**
25 | * Factory that will create a [CreateCustomerRequest] from a [CashAppPayPaymentAction].
26 | */
27 | object CustomerRequestDataFactory {
28 |
29 | internal const val CHANNEL_IN_APP = "IN_APP"
30 | private const val PAYMENT_TYPE_ONE_TIME = "ONE_TIME_PAYMENT"
31 | private const val PAYMENT_TYPE_ON_FILE = "ON_FILE_PAYMENT"
32 |
33 | fun build(
34 | clientId: String,
35 | redirectUri: String?,
36 | referenceId: String?,
37 | paymentActions: List,
38 | isRequestUpdate: Boolean = false,
39 | ): CustomerRequestData {
40 | val actions = ArrayList(paymentActions.size)
41 |
42 | for (paymentAction in paymentActions) {
43 | when (paymentAction) {
44 | is OnFileAction -> actions.add(buildFromOnFileAction(clientId, paymentAction))
45 | is OneTimeAction -> actions.add(buildFromOneTimeAction(clientId, paymentAction))
46 | }
47 | }
48 |
49 | return if (isRequestUpdate) {
50 | CustomerRequestData(
51 | actions = actions,
52 | channel = null,
53 | redirectUri = null,
54 | referenceId = referenceId?.let { PiiString(it) },
55 | )
56 | } else {
57 | CustomerRequestData(
58 | actions = actions,
59 | channel = CHANNEL_IN_APP,
60 | redirectUri = redirectUri?.let { PiiString(it) },
61 | referenceId = referenceId?.let { PiiString(it) },
62 | )
63 | }
64 | }
65 |
66 | private fun buildFromOnFileAction(
67 | clientId: String,
68 | onFileAction: OnFileAction,
69 | ): Action {
70 | // Create request data.
71 | val scopeIdOrClientId = onFileAction.scopeId ?: clientId
72 |
73 | return Action(
74 | scopeId = scopeIdOrClientId,
75 | type = PAYMENT_TYPE_ON_FILE,
76 | accountReferenceId = onFileAction.accountReferenceId?.let { PiiString(it) },
77 | )
78 | }
79 |
80 | private fun buildFromOneTimeAction(
81 | clientId: String,
82 | oneTimeAction: OneTimeAction,
83 | ): Action {
84 | // Create request data.
85 | val scopeIdOrClientId = oneTimeAction.scopeId ?: clientId
86 | return Action(
87 | amount_cents = oneTimeAction.amount,
88 | currency = oneTimeAction.currency?.backendValue,
89 | scopeId = scopeIdOrClientId,
90 | type = PAYMENT_TYPE_ONE_TIME,
91 | )
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | # Keep enums within the project.
2 | -keep enum app.cash.paykit.core.models.response.GrantType { *; }
3 | -keep enum app.cash.paykit.core.models.sdk.CashAppPayCurrency { *; }
4 | -keep enum app.cash.paykit.core.impl.RequestType { *; }
5 | -keep enum app.cash.paykit.core.utils.ThreadPurpose { *; }
6 |
7 |
8 | # Rules for Kotlin Serializer - a transitive dependency of KotlinX Datetime.
9 | # Can probably be removed after datetime is updated.
10 |
11 | # Keep `Companion` object fields of serializable classes.
12 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
13 | -if @kotlinx.serialization.Serializable class **
14 | -keepclassmembers class <1> {
15 | static <1>$Companion Companion;
16 | }
17 |
18 | # Keep `serializer()` on companion objects (both default and named) of serializable classes.
19 | -if @kotlinx.serialization.Serializable class ** {
20 | static **$* *;
21 | }
22 | -keepclassmembers class <2>$<3> {
23 | kotlinx.serialization.KSerializer serializer(...);
24 | }
25 |
26 | # Keep `INSTANCE.serializer()` of serializable objects.
27 | -if @kotlinx.serialization.Serializable class ** {
28 | public static ** INSTANCE;
29 | }
30 | -keepclassmembers class <1> {
31 | public static <1> INSTANCE;
32 | kotlinx.serialization.KSerializer serializer(...);
33 | }
34 |
35 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
36 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
37 |
38 | # Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
39 | # See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
40 | -dontnote kotlinx.serialization.**
41 |
42 | # Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
43 | # If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
44 | # However, since in this case they will not be used, we can disable these warnings
45 | -dontwarn kotlinx.serialization.internal.ClassValueReferences
46 |
47 | # Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
48 | # If you have any, replace classes with those containing named companion objects.
49 | -keepattributes InnerClasses # Needed for `getDeclaredClasses`.
50 |
51 | -if @kotlinx.serialization.Serializable class
52 | kotlinx.datetime.Instant$Companion, # <-- List serializable classes with named companions.
53 | kotlinx.datetime.Instant$Companion$serializer
54 | {
55 | static **$* *;
56 | }
57 | -keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
58 | static <1>$$serializer INSTANCE;
59 | }
60 |
61 | # Keep both serializer and serializable classes to save the attribute InnerClasses
62 | -keepclasseswithmembers, allowshrinking, allowobfuscation, allowaccessmodification class
63 | kotlinx.datetime.Instant$Companion, # <-- List serializable classes with named companions.
64 | kotlinx.datetime.Instant$Companion$serializer
65 | {
66 | *;
67 | }
68 |
69 | -dontwarn kotlinx.serialization.KSerializer
70 | -dontwarn kotlinx.serialization.Serializable
71 | -dontwarn kotlinx.serialization.descriptors.PrimitiveKind$STRING
72 | -dontwarn kotlinx.serialization.descriptors.PrimitiveKind
73 | -dontwarn kotlinx.serialization.descriptors.SerialDescriptor
74 | -dontwarn kotlinx.serialization.descriptors.SerialDescriptorsKt
75 | -dontwarn kotlinx.serialization.internal.AbstractPolymorphicSerializer
--------------------------------------------------------------------------------
/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerImplTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.logging
17 |
18 | import android.util.Log
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Before
21 | import org.junit.Test
22 | import org.junit.runner.RunWith
23 | import org.robolectric.RobolectricTestRunner
24 | import org.robolectric.annotation.Config
25 | import org.robolectric.shadows.ShadowLog
26 |
27 | @RunWith(RobolectricTestRunner::class)
28 | @Config(shadows = [ShadowLog::class])
29 | class CashAppLoggerImplTests {
30 |
31 | private lateinit var logger: CashAppLoggerImpl
32 | private lateinit var fakeListener: CashAppLoggerListener
33 | private lateinit var logEntries: MutableList
34 |
35 | @Before
36 | fun setUp() {
37 | // ShadowLog.stream = System.out redirects all logs to standard output.
38 | // On the below tests ShadowLog.getLogs() gets all the logs sent to LogCat.
39 | ShadowLog.stream = System.out
40 | logger = CashAppLoggerImpl()
41 | logEntries = mutableListOf()
42 |
43 | fakeListener = object : CashAppLoggerListener {
44 | override fun onNewLog(log: CashAppLogEntry) {
45 | logEntries.add(log)
46 | }
47 | }
48 | logger.setListener(fakeListener)
49 | }
50 |
51 | @Test
52 | fun `test logVerbose logs correctly`() {
53 | val tag = "tag"
54 | val msg = "verbose log"
55 |
56 | logger.logVerbose(tag, msg)
57 |
58 | val expectedLogEntry = CashAppLogEntry(Log.VERBOSE, tag, msg)
59 | assertThat(logger.retrieveLogs()).contains(expectedLogEntry)
60 | assertThat(logEntries).contains(expectedLogEntry)
61 | assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.type == Log.VERBOSE }).isTrue()
62 | }
63 |
64 | @Test
65 | fun `test logWarning logs correctly`() {
66 | val tag = "tag"
67 | val msg = "warning log"
68 |
69 | logger.logWarning(tag, msg)
70 |
71 | val expectedLogEntry = CashAppLogEntry(Log.WARN, tag, msg)
72 | assertThat(logger.retrieveLogs()).contains(expectedLogEntry)
73 | assertThat(logEntries).contains(expectedLogEntry)
74 | assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.type == Log.WARN }).isTrue()
75 | }
76 |
77 | @Test
78 | fun `test logError logs correctly`() {
79 | val tag = "tag"
80 | val msg = "error log"
81 | val throwable = Throwable("stuff happens")
82 |
83 | logger.logError(tag, msg, throwable)
84 |
85 | val expectedLogEntry = CashAppLogEntry(Log.ERROR, tag, msg, throwable)
86 | assertThat(logger.retrieveLogs()).contains(expectedLogEntry)
87 | assertThat(logEntries).contains(expectedLogEntry)
88 | assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.throwable == throwable && it.type == Log.ERROR }).isTrue()
89 | }
90 |
91 | @Test
92 | fun `test removeListener removes the listener`() {
93 | logger.removeListener()
94 | logger.logVerbose("tag", "message")
95 |
96 | assertThat(logEntries).isEmpty()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/CashAppPayState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.core.models.response.CustomerResponseData
19 |
20 | /**
21 | * Various states Cash App Pay SDK might be in depending the stage of the transaction process.
22 | *
23 | */
24 | sealed interface CashAppPayState {
25 |
26 | /**
27 | * This is the initial Cash App Pay State.
28 | */
29 | object NotStarted : CashAppPayState
30 |
31 | /**
32 | * This state denotes that a the Create Customer Request has been started. If successful, this state
33 | * will transition to [ReadyToAuthorize].
34 | */
35 | object CreatingCustomerRequest : CashAppPayState
36 |
37 | /**
38 | * This state denotes that a the Update Customer Request has been started. If successful, this state
39 | * will transition to [ReadyToAuthorize].
40 | */
41 | object UpdatingCustomerRequest : CashAppPayState
42 |
43 | /**
44 | * This state denotes that the SDK is in the process of retrieving an existing Customer Request.
45 | * If successful, this state will transition into the corresponding state of the request that was
46 | * retrieved, which can be one of the following: [ReadyToAuthorize], [Approved], [Declined].
47 | */
48 | object RetrievingExistingCustomerRequest : CashAppPayState
49 |
50 | /**
51 | * This state denotes that a valid Customer Request exists, and we're ready to authorize upon
52 | * user action.
53 | */
54 | class ReadyToAuthorize(val responseData: CustomerResponseData) : CashAppPayState
55 |
56 | /**
57 | * This state denotes that we've entered the process of authorizing an existing customer request.
58 | * This state will transition to [PollingTransactionStatus].
59 | */
60 | object Authorizing : CashAppPayState
61 |
62 | /**
63 | * This state denotes that we're in the process of refreshing the existing customer request,
64 | * in case the authorization has expired. This is typically an in-between between [Authorizing]
65 | * and [PollingTransactionStatus].
66 | */
67 | object Refreshing : CashAppPayState
68 |
69 | /**
70 | * This state denotes that we're actively polling for an authorization update. This state will
71 | * typically transition to either [Approved] or [Declined].
72 | */
73 | object PollingTransactionStatus : CashAppPayState
74 |
75 | /**
76 | * Terminal state denoting that the request authorization was approved.
77 | */
78 | class Approved(val responseData: CustomerResponseData) : CashAppPayState
79 |
80 | /**
81 | * Terminal state denoting that the request authorization was declined.
82 | */
83 | object Declined : CashAppPayState
84 |
85 | /**
86 | * Terminal state that can happen as a result of most states, indicates that an exception has
87 | * occurred.
88 | * This state is typically unrecoverable, and it is advised to restart the process from scratch in
89 | * case it is met.
90 | */
91 | class CashAppPayExceptionState(val exception: Exception) : CashAppPayState
92 | }
93 |
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id("com.google.devtools.ksp").version("1.8.22-1.0.11")
5 | id 'project-report' // run ./gradlew htmlDependencyReport
6 | id "com.vanniktech.maven.publish.base"
7 | }
8 |
9 | task sourceJar(type: Jar) {
10 | from android.sourceSets.main.java.srcDirs
11 | archiveClassifier.set('sources')
12 | }
13 |
14 | android {
15 | namespace 'app.cash.paykit.core'
16 | compileSdk versions.compileSdk
17 |
18 | defaultConfig {
19 | minSdk versions.minSdk
20 | // We target the minimum API that meets Google Play's target level, for higher compatibility: https://developer.android.com/google/play/requirements/target-sdk
21 | //noinspection OldTargetApi
22 | targetSdk versions.targetSdk
23 |
24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
25 | consumerProguardFiles "consumer-rules.pro"
26 | resValue("string", "cap_version", "$version")
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | compileOptions {
37 | sourceCompatibility JavaVersion.VERSION_17
38 | targetCompatibility JavaVersion.VERSION_17
39 | }
40 | kotlinOptions {
41 | jvmTarget = "17"
42 | }
43 |
44 | kotlin {
45 | jvmToolchain(17)
46 | }
47 |
48 | resourcePrefix 'cap_'
49 |
50 | lintOptions {
51 | abortOnError true
52 | htmlReport true
53 | checkAllWarnings true
54 | warningsAsErrors true
55 | baseline file("lint-baseline.xml")
56 | }
57 |
58 | testOptions {
59 | unitTests {
60 | returnDefaultValues = true
61 | includeAndroidResources = true
62 | }
63 | }
64 |
65 | buildFeatures {
66 | buildConfig = true
67 | }
68 | }
69 |
70 | dependencies {
71 |
72 | // We want to lock this dependency at a lower than latest version to not force transitive updates onto merchants.
73 | //noinspection GradleDependency
74 | ksp("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version")
75 | //noinspection GradleDependency
76 | implementation("com.squareup.moshi:moshi-kotlin:$moshi_version")
77 |
78 | // KotlinX Dates.
79 | implementation "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_date_version"
80 |
81 | // Provides a lifecycle for the whole application process.
82 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
83 |
84 | // AndroidX Startup.
85 | implementation "androidx.startup:startup-runtime:$startup_version"
86 |
87 | //noinspection GradleDependency
88 | implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
89 |
90 | implementation project(':logging')
91 | implementation project(':analytics-core')
92 |
93 | // TEST RELATED.
94 |
95 | testImplementation "junit:junit:$junit_version"
96 | testImplementation "io.mockk:mockk:$mockk_version"
97 | testImplementation "com.google.truth:truth:$google_truth_version"
98 | androidTestImplementation "androidx.test.ext:junit-ktx:$junit_androidx_version"
99 | testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
100 | // Robolectric environment.
101 | testImplementation "org.robolectric:robolectric:$robolectric_version"
102 | // Coroutines test support.
103 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
104 | //Test helpers for Lifecycle runtime
105 | testImplementation "androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version"
106 | }
107 |
108 | mavenPublishing {
109 | // AndroidMultiVariantLibrary(publish a sources jar, publish a javadoc jar)
110 | configure(new com.vanniktech.maven.publish.AndroidSingleVariantLibrary("release", true, true))
111 | }
--------------------------------------------------------------------------------
/core/src/main/java/app/cash/paykit/core/impl/CashAppPayLifecycleObserverImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core.impl
17 |
18 | import android.os.Handler
19 | import android.os.Looper
20 | import androidx.lifecycle.DefaultLifecycleObserver
21 | import androidx.lifecycle.LifecycleOwner
22 | import androidx.lifecycle.ProcessLifecycleOwner
23 | import app.cash.paykit.core.CashAppPayFactory
24 | import app.cash.paykit.core.CashAppPayLifecycleObserver
25 | import java.lang.ref.WeakReference
26 |
27 | /**
28 | * This class is intended to be a singleton.
29 | * The [CashAppPayFactory] static object creates and holds onto this single instance.
30 | */
31 | internal class CashAppPayLifecycleObserverImpl(
32 | private val processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
33 | ) : DefaultLifecycleObserver,
34 | CashAppPayLifecycleObserver {
35 |
36 | private val payKitInstances = arrayListOf>()
37 |
38 | private var mainHandler: Handler = Handler(Looper.getMainLooper())
39 |
40 | /*
41 | * Functions to register & unregister instances of [PayKitLifecycleListener].
42 | */
43 |
44 | override fun register(newInstance: CashAppPayLifecycleListener) {
45 | // Register for ProcessLifecycle changes if this is the first PayKitLifecycleListener.
46 | if (payKitInstances.isEmpty()) {
47 | runOnUiThread {
48 | processLifecycleOwner
49 | .lifecycle
50 | .addObserver(this)
51 | }
52 | }
53 |
54 | payKitInstances.add(WeakReference(newInstance))
55 | }
56 |
57 | override fun unregister(instanceToRemove: CashAppPayLifecycleListener) {
58 | val internalInstance = payKitInstances.firstOrNull { it.get() == instanceToRemove }
59 | payKitInstances.remove(internalInstance)
60 |
61 | // Stop listening for ProcessLifecycle changes if no more PayKitLifecycleListeners are available.
62 | if (payKitInstances.isEmpty()) {
63 | runOnUiThread {
64 | processLifecycleOwner
65 | .lifecycle
66 | .removeObserver(this)
67 | }
68 | }
69 | }
70 |
71 | private fun runOnUiThread(action: Runnable) {
72 | val isOnMainThread = Looper.getMainLooper().thread == Thread.currentThread()
73 | if (isOnMainThread) {
74 | action.run()
75 | } else {
76 | mainHandler.post(action)
77 | }
78 | }
79 |
80 | /*
81 | * Callback functions from [DefaultLifecycleObserver].
82 | */
83 |
84 | override fun onStart(owner: LifecycleOwner) {
85 | super.onStart(owner)
86 | payKitInstances.forEach { it.get()?.onApplicationForegrounded() }
87 | }
88 |
89 | override fun onStop(owner: LifecycleOwner) {
90 | super.onStop(owner)
91 | payKitInstances.forEach { it.get()?.onApplicationBackgrounded() }
92 | }
93 | }
94 |
95 | /**
96 | * Interface that exposes process foreground/background callback functions.
97 | */
98 | interface CashAppPayLifecycleListener {
99 | /**
100 | * Triggered when the application process was foregrounded.
101 | */
102 | fun onApplicationForegrounded()
103 |
104 | /**
105 | * Triggered when the application process was backgrounded.
106 | */
107 | fun onApplicationBackgrounded()
108 | }
109 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/CashAppPayLifecycleObserverTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import androidx.lifecycle.Lifecycle
19 | import androidx.lifecycle.LifecycleOwner
20 | import androidx.lifecycle.testing.TestLifecycleOwner
21 | import app.cash.paykit.core.impl.CashAppPayLifecycleListener
22 | import app.cash.paykit.core.impl.CashAppPayLifecycleObserverImpl
23 | import io.mockk.every
24 | import io.mockk.mockk
25 | import io.mockk.verify
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.ExperimentalCoroutinesApi
28 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
29 | import kotlinx.coroutines.test.runTest
30 | import kotlinx.coroutines.test.setMain
31 | import org.junit.Before
32 | import org.junit.Test
33 | import org.junit.runner.RunWith
34 | import org.robolectric.RobolectricTestRunner
35 |
36 | @OptIn(ExperimentalCoroutinesApi::class)
37 | @RunWith(RobolectricTestRunner::class)
38 | class CashAppPayLifecycleObserverTests {
39 |
40 | private val testDispatcher = UnconfinedTestDispatcher()
41 |
42 | @Before
43 | fun setup() {
44 | Dispatchers.setMain(testDispatcher)
45 | }
46 |
47 | @Test
48 | fun `registered PayKitLifecycleListener will receive updates`() = runTest {
49 | val testLifecycleOwner = TestLifecycleOwner()
50 | val payKitLifecycleObserver = CashAppPayLifecycleObserverImpl(testLifecycleOwner)
51 |
52 | // Create and register listener.
53 | val listenerMock = mockk(relaxed = true)
54 | payKitLifecycleObserver.register(listenerMock)
55 |
56 | // Simulate Application Lifecycle events.
57 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
58 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
59 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
60 |
61 | verify(exactly = 1) { listenerMock.onApplicationForegrounded() }
62 | }
63 |
64 | @Test
65 | fun `after unRegister PayKitLifecycleListener will NOT receive updates`() = runTest {
66 | val testLifecycleOwner = TestLifecycleOwner()
67 | val payKitLifecycleObserver = CashAppPayLifecycleObserverImpl(testLifecycleOwner)
68 | val listenerMock = mockk(relaxed = true)
69 |
70 | // Register and unregister listener.
71 | payKitLifecycleObserver.register(listenerMock)
72 | payKitLifecycleObserver.unregister(listenerMock)
73 |
74 | // Simulate Application Lifecycle events.
75 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
76 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
77 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
78 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
79 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
80 | testLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
81 |
82 | verify(exactly = 0) { listenerMock.onApplicationForegrounded() }
83 | verify(exactly = 0) { listenerMock.onApplicationBackgrounded() }
84 | }
85 |
86 | @Test
87 | fun `removeObserver should be called when all payKitInstances are gone`() {
88 | val mockLifecycleOwner = mockk(relaxed = true)
89 | val mockLifecycle = mockk(relaxed = true)
90 | every { mockLifecycleOwner.lifecycle } returns mockLifecycle
91 | val payKitLifecycleObserver = CashAppPayLifecycleObserverImpl(mockLifecycleOwner)
92 |
93 | // Register and unregister a mock listener.
94 | val listenerMock = mockk(relaxed = true)
95 | payKitLifecycleObserver.register(listenerMock)
96 | verify(exactly = 0) { mockLifecycle.removeObserver(any()) }
97 |
98 | payKitLifecycleObserver.unregister(listenerMock)
99 | verify(atLeast = 1) { mockLifecycle.removeObserver(any()) }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/core/src/test/java/app/cash/paykit/core/PayKitAnalyticsEventDispatcherImplTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.core
17 |
18 | import app.cash.paykit.analytics.PayKitAnalytics
19 | import app.cash.paykit.core.analytics.AnalyticsEventStream2Event
20 | import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcherImpl
21 | import app.cash.paykit.core.fakes.FakeClock
22 | import app.cash.paykit.core.fakes.FakeData
23 | import app.cash.paykit.core.fakes.FakeUUIDManager
24 | import io.mockk.MockKAnnotations
25 | import io.mockk.impl.annotations.MockK
26 | import io.mockk.verify
27 | import org.junit.Before
28 | import org.junit.Test
29 |
30 | class PayKitAnalyticsEventDispatcherImplTest {
31 |
32 | @MockK(relaxed = true)
33 | private lateinit var networkManager: NetworkManager
34 |
35 | @MockK(relaxed = true)
36 | private lateinit var paykitAnalytics: PayKitAnalytics
37 |
38 | @Before
39 | fun setup() {
40 | MockKAnnotations.init(this)
41 | }
42 |
43 | @Test
44 | fun `sdkInitialized records valid analytics event`() {
45 | val analyticsDispatcher = buildDispatcher()
46 | analyticsDispatcher.sdkInitialized()
47 | val contents = """{"app_name":"paykitsdk-android","catalog_name":"mobile_cap_pk_initialization","json_data":"{\"mobile_cap_pk_initialization_sdk_version\":\"1.0.0\",\"mobile_cap_pk_initialization_client_ua\":\"Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)\",\"mobile_cap_pk_initialization_platform\":\"android\",\"mobile_cap_pk_initialization_client_id\":\"fake_client_id\",\"mobile_cap_pk_initialization_environment\":\"SANDBOX\"}","recorded_at_usec":123,"uuid":"abc"}"""
48 |
49 | verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) }
50 | }
51 |
52 | @Test
53 | fun `eventListenerAdded records valid analytics event`() {
54 | val analyticsDispatcher = buildDispatcher()
55 | analyticsDispatcher.eventListenerAdded()
56 | val contents = """{"app_name":"paykitsdk-android","catalog_name":"mobile_cap_pk_event_listener","json_data":"{\"mobile_cap_pk_event_listener_sdk_version\":\"1.0.0\",\"mobile_cap_pk_event_listener_client_ua\":\"Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)\",\"mobile_cap_pk_event_listener_platform\":\"android\",\"mobile_cap_pk_event_listener_client_id\":\"fake_client_id\",\"mobile_cap_pk_event_listener_environment\":\"SANDBOX\",\"mobile_cap_pk_event_listener_is_added\":true}","recorded_at_usec":123,"uuid":"abc"}"""
57 |
58 | verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) }
59 | }
60 |
61 | @Test
62 | fun `eventListenerRemoved records valid analytics event`() {
63 | val analyticsDispatcher = buildDispatcher()
64 | analyticsDispatcher.eventListenerRemoved()
65 | val contents = """{"app_name":"paykitsdk-android","catalog_name":"mobile_cap_pk_event_listener","json_data":"{\"mobile_cap_pk_event_listener_sdk_version\":\"1.0.0\",\"mobile_cap_pk_event_listener_client_ua\":\"Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)\",\"mobile_cap_pk_event_listener_platform\":\"android\",\"mobile_cap_pk_event_listener_client_id\":\"fake_client_id\",\"mobile_cap_pk_event_listener_environment\":\"SANDBOX\",\"mobile_cap_pk_event_listener_is_added\":false}","recorded_at_usec":123,"uuid":"abc"}"""
66 |
67 | verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) }
68 | }
69 |
70 | @Test
71 | fun `SDK state update records valid analytics event`() {
72 | val analyticsDispatcher = buildDispatcher()
73 | analyticsDispatcher.genericStateChanged(CashAppPayState.Authorizing, null)
74 | val contents = """{"app_name":"paykitsdk-android","catalog_name":"mobile_cap_pk_customer_request","json_data":"{\"mobile_cap_pk_customer_request_sdk_version\":\"1.0.0\",\"mobile_cap_pk_customer_request_client_ua\":\"Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)\",\"mobile_cap_pk_customer_request_platform\":\"android\",\"mobile_cap_pk_customer_request_client_id\":\"fake_client_id\",\"mobile_cap_pk_customer_request_environment\":\"SANDBOX\",\"mobile_cap_pk_customer_request_action\":\"redirect\",\"mobile_cap_pk_customer_request_channel\":\"IN_APP\"}","recorded_at_usec":123,"uuid":"abc"}"""
75 |
76 | verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) }
77 | }
78 |
79 | private fun buildDispatcher() = PayKitAnalyticsEventDispatcherImpl(
80 | FakeData.SDK_VERSION,
81 | FakeData.CLIENT_ID,
82 | FakeData.USER_AGENT,
83 | FakeData.SDK_ENVIRONMENT_SANDBOX,
84 | paykitAnalytics,
85 | networkManager,
86 | uuidManager = FakeUUIDManager(),
87 | clock = FakeClock(),
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/analytics-core/src/test/java/app/cash/paykit/analytics/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics
17 |
18 | import android.content.ContentValues
19 | import android.database.Cursor
20 | import android.database.sqlite.SQLiteDatabase
21 | import android.util.Log
22 | import app.cash.paykit.analytics.persistence.AnalyticEntry
23 | import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSQLiteDataSource
24 | import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSqLiteHelper
25 | import java.lang.reflect.Field
26 |
27 | internal object Utils {
28 |
29 | fun getPrivateField(obj: Any, fieldName: String): Any? {
30 | try {
31 | val field: Field = obj.javaClass.getDeclaredField(fieldName)
32 | field.isAccessible = true
33 | return field.get(obj)
34 | } catch (e: NoSuchFieldException) {
35 | e.printStackTrace()
36 | } catch (e: IllegalAccessException) {
37 | e.printStackTrace()
38 | }
39 | return null
40 | }
41 |
42 | fun createEntry(entryType: String?, entryState: Int): AnalyticEntry {
43 | return createEntry(
44 | "entry.process.id",
45 | entryType,
46 | entryState,
47 | "entry.load",
48 | "entry.metadata",
49 | "v1",
50 | )
51 | }
52 |
53 | fun createEntry(
54 | processId: String?,
55 | entryType: String?,
56 | entryState: Int,
57 | load: String,
58 | metaData: String?,
59 | version: String?,
60 | ) = AnalyticEntry(
61 | id = System.currentTimeMillis(),
62 | processId = processId,
63 | type = entryType,
64 | state = entryState,
65 | content = load,
66 | metaData = metaData,
67 | version = version,
68 | )
69 |
70 | fun getEntriesToSync(count: Int): List {
71 | return mutableListOf().apply {
72 | for (i in 0 until count) {
73 | add(createEntry("TYPE_1", AnalyticEntry.STATE_NEW))
74 | }
75 | }
76 | }
77 |
78 | /**
79 | * Returns all sync entries from the database.
80 | *
81 | * @return list of sync entries
82 | */
83 | fun getAllSyncEntries(sqLiteHelper: AnalyticsSqLiteHelper): List {
84 | val entries = mutableListOf()
85 | val database: SQLiteDatabase = sqLiteHelper.database
86 | var cursor: Cursor? = null
87 | try {
88 | // @formatter:off
89 | cursor = database.query(
90 | true,
91 | AnalyticsSQLiteDataSource.TABLE_SYNC_ENTRIES,
92 | null,
93 | null,
94 | null,
95 | null,
96 | null,
97 | "id ASC",
98 | null,
99 | )
100 | // @formatter:on
101 | cursor.moveToFirst()
102 | while (!cursor.isAfterLast) {
103 | entries.add(AnalyticEntry.from(cursor))
104 | cursor.moveToNext()
105 | }
106 | } catch (e: Exception) {
107 | e.printStackTrace()
108 | Log.e("", "", e)
109 | } finally {
110 | cursor?.close()
111 | }
112 | return entries
113 | }
114 |
115 | /**
116 | * Deletes all entries from the database.
117 | */
118 | @Synchronized
119 | fun deleteAllEntries(sqLiteHelper: AnalyticsSqLiteHelper) {
120 | val database: SQLiteDatabase = sqLiteHelper.database
121 | try {
122 | database.delete(AnalyticsSQLiteDataSource.TABLE_SYNC_ENTRIES, null, null)
123 | } catch (e: Exception) {
124 | e.printStackTrace()
125 | Log.e("", "", e)
126 | }
127 | }
128 |
129 | fun insertSyncEntry(
130 | sqLiteHelper: AnalyticsSqLiteHelper,
131 | processId: String?,
132 | entryType: String?,
133 | entryState: Int,
134 | load: String?,
135 | metaData: String?,
136 | version: String?,
137 | ): Long {
138 | var insertId: Long = -1
139 | try {
140 | val database: SQLiteDatabase = sqLiteHelper.database
141 | val values = ContentValues()
142 | values.put(AnalyticsSQLiteDataSource.COLUMN_TYPE, entryType)
143 | values.put(AnalyticsSQLiteDataSource.COLUMN_PROCESS_ID, processId)
144 | values.put(AnalyticsSQLiteDataSource.COLUMN_CONTENT, load)
145 | values.put(AnalyticsSQLiteDataSource.COLUMN_STATE, entryState)
146 | values.put(AnalyticsSQLiteDataSource.COLUMN_META_DATA, metaData)
147 | values.put(AnalyticsSQLiteDataSource.COLUMN_VERSION, version)
148 | insertId = database.insert(AnalyticsSQLiteDataSource.TABLE_SYNC_ENTRIES, null, values)
149 | if (insertId < 0) {
150 | Log.e(
151 | "Utils",
152 | "Unable to insert record into the " + AnalyticsSQLiteDataSource.TABLE_SYNC_ENTRIES + ", values: " +
153 | values,
154 | )
155 | }
156 | } catch (e: Exception) {
157 | e.printStackTrace()
158 | Log.e("", "", e)
159 | }
160 | return insertId
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/EntriesDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics.persistence
17 |
18 | import android.os.SystemClock
19 | import app.cash.paykit.analytics.AnalyticsOptions
20 |
21 | abstract class EntriesDataSource(val options: AnalyticsOptions) {
22 |
23 | abstract fun insertEntry(type: String, content: String, metaData: String?): Long
24 |
25 | /**
26 | * Deletes the entries from the database.
27 | *
28 | * @param entries - entries to delete
29 | */
30 | abstract fun deleteEntry(entries: List)
31 |
32 | /**
33 | * Updates existing entries to SYNC_PENDING state and sets provided processId where entries
34 | * are in states NEW or SYNC_FAILED. The entries that are in state SYNC_IN_PROGRESS are skipped
35 | * in the update. entries in state SYNC_PENDING will be updated only if process id is NULL.
36 | * This is to ensure that in case that we have two sync processes overlapping we do not send
37 | * entries multiple times. The method will update only first MAX_SYNC_ENTRY_COUNT_PER_PROCESS
38 | * number of rows.
39 | *
40 | * @param processId id of the process that is going to perform the sync operation
41 | * @param entryType type of the entry to mark
42 | */
43 | abstract fun markEntriesForDelivery(processId: String, entryType: String)
44 |
45 | /**
46 | * Generates process id to be associated with the synchronization. This should be called at the
47 | * beginning of the synchronization task to obtain the process_id which should be used until
48 | * task ends its processing.
49 | *
50 | * @return process id associated with sync entries
51 | * @param entryType type of the entries
52 | */
53 | @Synchronized
54 | fun generateProcessId(entryType: String): String {
55 | val procId = "proc-" + SystemClock.elapsedRealtime()
56 | markEntriesForDelivery(procId, entryType)
57 | return procId
58 | }
59 |
60 | /**
61 | * Returns the list of entries to send to the server that are in state SYNC_PENDING and belong
62 | * to the provided PROCESS_ID. The result is paginated based on the 0 for offset and AnalyticEntry.BATCH_SIZE
63 | * for limit
64 | *
65 | * @param processId id of the sync process
66 | * @param entryType type of the entries
67 | * @return List of sync entries to send to the server
68 | */
69 | @Synchronized
70 | fun getEntriesForDelivery(
71 | processId: String,
72 | entryType: String,
73 | ): List {
74 | return getEntriesForDelivery(processId, entryType, 0, options.batchSize)
75 | }
76 |
77 | /**
78 | * Returns the list of entries to send to the server that are in state SYNC_PENDING and belong
79 | * to the provided PROCESS_ID. The result is paginated based on the provided offset and limit
80 | * parameters.
81 | *
82 | * @param processId id of the sync process
83 | * @param entryType type of the entry
84 | * @param offset results offset
85 | * @param limit results limit
86 | * @return List of sync entries to send to the server
87 | */
88 | @Synchronized
89 | fun getEntriesForDelivery(
90 | processId: String,
91 | entryType: String,
92 | offset: Int,
93 | limit: Int,
94 | ): List {
95 | return getEntriesByProcessIdAndState(
96 | processId,
97 | entryType,
98 | AnalyticEntry.STATE_DELIVERY_PENDING,
99 | offset,
100 | limit,
101 | )
102 | }
103 |
104 | /**
105 | * Returns the list of entries to send to the server. It retrieves entries that are in a
106 | * provided state and matching the provided process id. The result is paginated based on the
107 | * provided offset and limit parameters.
108 | *
109 | * @param processId id of the working process
110 | * @param entryType type of the entry
111 | * @param state state of the entries to retrieve
112 | * @param offset results offset
113 | * @param limit results limit
114 | * @return List of sync entries to send to the server
115 | */
116 | abstract fun getEntriesByProcessIdAndState(
117 | processId: String,
118 | entryType: String,
119 | state: Int,
120 | offset: Int,
121 | limit: Int,
122 | ): List
123 |
124 | /**
125 | * Updates all entries in the database to the status NEW and process_id to NULL
126 | */
127 | abstract fun resetEntries()
128 |
129 | /**
130 | * Updates the status of the sync entry.
131 | *
132 | * @param entries list of the sync entries to update
133 | * @param status new status for the sync entry
134 | */
135 | abstract fun updateStatuses(entries: List, status: Int)
136 | }
137 |
138 | fun List.toCommaSeparatedListIds() = joinToString(transform = {
139 | it.id.toString()
140 | })
141 |
--------------------------------------------------------------------------------
/core/lint-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
13 |
14 |
15 |
20 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/analytics-core/src/test/java/app/cash/paykit/analytics/DeliveryWorkerTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Cash App
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 | package app.cash.paykit.analytics
17 |
18 | import app.cash.paykit.analytics.core.DeliveryHandler
19 | import app.cash.paykit.analytics.core.DeliveryListener
20 | import app.cash.paykit.analytics.core.DeliveryWorker
21 | import app.cash.paykit.analytics.persistence.AnalyticEntry
22 | import app.cash.paykit.analytics.persistence.AnalyticEntry.Companion.STATE_DELIVERY_FAILED
23 | import app.cash.paykit.analytics.persistence.AnalyticEntry.Companion.STATE_DELIVERY_IN_PROGRESS
24 | import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSQLiteDataSource
25 | import app.cash.paykit.logging.CashAppLogger
26 | import io.mockk.every
27 | import io.mockk.mockk
28 | import io.mockk.verify
29 | import io.mockk.verifyOrder
30 | import org.junit.Test
31 | import org.junit.runner.RunWith
32 | import org.robolectric.RobolectricTestRunner
33 |
34 | @Suppress("UNCHECKED_CAST")
35 | @RunWith(RobolectricTestRunner::class)
36 | class DeliveryWorkerTest {
37 |
38 | private val cashAppLogger: CashAppLogger = mockk(relaxed = true)
39 |
40 | @Test
41 | fun testNoDeliveryHandlers() {
42 | val dataSource: AnalyticsSQLiteDataSource = mockk(relaxed = true)
43 | val analyticsOptions: AnalyticsOptions = mockk(relaxed = true)
44 | val handlers = ArrayList()
45 | val worker = DeliveryWorker(dataSource, handlers, AnalyticsLogger(analyticsOptions, cashAppLogger))
46 | worker.call()
47 |
48 | verify(inverse = true) { dataSource.generateProcessId(any()) }
49 | verify(inverse = true) { dataSource.getEntriesForDelivery(any(), any()) }
50 | verify(inverse = true) { dataSource.updateStatuses(any(), any()) }
51 | verify(inverse = true) { dataSource.deleteEntry(any()) }
52 | }
53 |
54 | @Test
55 | fun testNoDeliverablesToSync() {
56 | val dataSource: AnalyticsSQLiteDataSource = mockk(relaxed = true)
57 | val deliveryHandler: DeliveryHandler = mockk(relaxed = true)
58 |
59 | val deliveryType = "TYPE_1"
60 |
61 | every { deliveryHandler.deliverableType } answers { deliveryType }
62 | every { dataSource.getEntriesForDelivery(any(), any()) } answers {
63 | Utils.getEntriesToSync(0)
64 | }
65 |
66 | val handlers = listOf(deliveryHandler)
67 | val analyticsLogger: AnalyticsLogger = mockk(relaxed = true)
68 | val worker = DeliveryWorker(dataSource, handlers, analyticsLogger)
69 | worker.call()
70 |
71 | verify(exactly = 1) { dataSource.generateProcessId(eq(deliveryType)) }
72 | verify(exactly = 1) { dataSource.getEntriesForDelivery(any(), any()) }
73 | verify(inverse = true) { dataSource.updateStatuses(any(), any()) }
74 | verify(inverse = true) { dataSource.deleteEntry(any()) }
75 | }
76 |
77 | @Test
78 | fun testWorker() {
79 | val dataSource: AnalyticsSQLiteDataSource = mockk(relaxed = true)
80 |
81 | // Handler for TYPE_1 entries (sync successful)
82 | val handler1: DeliveryHandler = mockk(relaxed = true)
83 | val deliveryType1 = "TYPE_1"
84 |
85 | val deliveryListener1 = object : DeliveryListener {
86 | override fun onSuccess(entries: List) {
87 | dataSource.deleteEntry(entries)
88 | }
89 |
90 | override fun onError(entries: List) = Unit
91 | }
92 | every { handler1.deliveryListener } answers { deliveryListener1 }
93 | every { handler1.deliverableType } answers { deliveryType1 }
94 | every { handler1.deliver(any(), eq(deliveryListener1)) } answers {
95 | val entries = it.invocation.args[0]
96 | val listener = it.invocation.args[1] as DeliveryListener
97 | listener.onSuccess(entries as List)
98 | }
99 |
100 | // Handler for TYPE_2 entries (sync failed)
101 | val handler2: DeliveryHandler = mockk(relaxed = true)
102 | val deliveryType2 = "TYPE_2"
103 |
104 | val deliveryListener2 = object : DeliveryListener {
105 | override fun onSuccess(entries: List) = Unit
106 |
107 | override fun onError(entries: List) {
108 | dataSource.updateStatuses(entries, STATE_DELIVERY_FAILED)
109 | }
110 | }
111 | every { handler2.deliveryListener } answers { deliveryListener2 }
112 | every { handler2.deliverableType } answers { deliveryType2 }
113 | every { handler2.deliver(any(), eq(deliveryListener2)) } answers {
114 | val listener = it.invocation.args[1] as DeliveryListener
115 | listener.onError(args[0] as List)
116 | }
117 |
118 | val handlers = listOf(handler1, handler2)
119 |
120 | every { dataSource.getEntriesForDelivery(any(), eq(deliveryType1)) } returns
121 | Utils.getEntriesToSync(10) andThen
122 | Utils.getEntriesToSync(5) andThen
123 | Utils.getEntriesToSync(0)
124 |
125 | every { dataSource.getEntriesForDelivery(any(), eq(deliveryType2)) } returns
126 | Utils.getEntriesToSync(3) andThen
127 | Utils.getEntriesToSync(0)
128 |
129 | // start processing
130 | val analyticsLogger: AnalyticsLogger = mockk(relaxed = true)
131 | DeliveryWorker(dataSource, handlers, analyticsLogger).call()
132 |
133 | // Processing 1st handler
134 | verifyOrder {
135 | dataSource.generateProcessId(eq(deliveryType1))
136 | dataSource.getEntriesForDelivery(any(), eq(deliveryType1))
137 |
138 | dataSource.updateStatuses(any(), eq(STATE_DELIVERY_IN_PROGRESS))
139 | dataSource.deleteEntry(any())
140 | dataSource.getEntriesForDelivery(any(), eq(deliveryType1))
141 |
142 | dataSource.updateStatuses(any(), eq(STATE_DELIVERY_IN_PROGRESS))
143 | dataSource.deleteEntry(any())
144 | dataSource.getEntriesForDelivery(any(), eq(deliveryType1))
145 |
146 | // Processing 2nd handler
147 | dataSource.generateProcessId(eq(deliveryType2))
148 | dataSource.getEntriesForDelivery(any(), eq(deliveryType2))
149 |
150 | dataSource.updateStatuses(any(), eq(STATE_DELIVERY_IN_PROGRESS))
151 | dataSource.updateStatuses(any(), eq(STATE_DELIVERY_FAILED))
152 | dataSource.getEntriesForDelivery(any(), eq(deliveryType2))
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------