├── .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 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 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 | 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 | 7 | 8 | 10 | 11 | 14 | 15 | 17 | 18 | 20 | 21 | 23 | 24 | 27 | 28 | 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 | [![release](https://github.com/cashapp/android-cash-paykit-sdk/actions/workflows/release.yaml/badge.svg)](https://github.com/cashapp/android-cash-paykit-sdk/actions/workflows/release.yaml) ![License](https://img.shields.io/github/license/cashapp/cash-pay-kit-sdk-android-sample-app?style=plastic) 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 | 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 | --------------------------------------------------------------------------------