From 1c6054b158ba419f14da4a092f95a65127e36381 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 28 Jan 2026 14:39:44 +0100 Subject: [PATCH 1/5] Add Create Request Payout action --- .../request/CustomerRequestDataFactory.kt | 27 +++++++++++++------ .../models/sdk/CashAppPayPaymentAction.kt | 13 +++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt index 24c5981..746619d 100644 --- a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt +++ b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt @@ -19,6 +19,7 @@ import app.cash.paykit.core.models.common.Action import app.cash.paykit.core.models.pii.PiiString import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFilePayoutAction import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction /** @@ -29,6 +30,7 @@ object CustomerRequestDataFactory { internal const val CHANNEL_IN_APP = "IN_APP" private const val PAYMENT_TYPE_ONE_TIME = "ONE_TIME_PAYMENT" private const val PAYMENT_TYPE_ON_FILE = "ON_FILE_PAYMENT" + private const val PAYMENT_TYPE_ON_FILE_PAYOUT = "ON_FILE_PAYOUT" fun build( clientId: String, @@ -43,6 +45,7 @@ object CustomerRequestDataFactory { when (paymentAction) { is OnFileAction -> actions.add(buildFromOnFileAction(clientId, paymentAction)) is OneTimeAction -> actions.add(buildFromOneTimeAction(clientId, paymentAction)) + is OnFilePayoutAction -> actions.add(buildFromOnFilePayoutAction(clientId, paymentAction)) } } @@ -63,10 +66,7 @@ object CustomerRequestDataFactory { } } - private fun buildFromOnFileAction( - clientId: String, - onFileAction: OnFileAction, - ): Action { + private fun buildFromOnFileAction(clientId: String, onFileAction: OnFileAction): Action { // Create request data. val scopeIdOrClientId = onFileAction.scopeId ?: clientId @@ -77,10 +77,7 @@ object CustomerRequestDataFactory { ) } - private fun buildFromOneTimeAction( - clientId: String, - oneTimeAction: OneTimeAction, - ): Action { + private fun buildFromOneTimeAction(clientId: String, oneTimeAction: OneTimeAction): Action { // Create request data. val scopeIdOrClientId = oneTimeAction.scopeId ?: clientId return Action( @@ -90,4 +87,18 @@ object CustomerRequestDataFactory { type = PAYMENT_TYPE_ONE_TIME, ) } + + private fun buildFromOnFilePayoutAction( + clientId: String, + onFilePayoutAction: OnFilePayoutAction, + ): Action { + // Create request data. + val scopeIdOrClientId = onFilePayoutAction.scopeId ?: clientId + + return Action( + scopeId = scopeIdOrClientId, + type = PAYMENT_TYPE_ON_FILE_PAYOUT, + accountReferenceId = onFilePayoutAction.accountReferenceId?.let { PiiString(it) }, + ) + } } diff --git a/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayPaymentAction.kt b/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayPaymentAction.kt index 48e91f8..cbf8b22 100644 --- a/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayPaymentAction.kt +++ b/core/src/main/java/app/cash/paykit/core/models/sdk/CashAppPayPaymentAction.kt @@ -54,4 +54,17 @@ sealed class CashAppPayPaymentAction(open val scopeId: String?, open val referen val accountReferenceId: String? = null, override val referenceId: String? = null, ) : CashAppPayPaymentAction(scopeId, referenceId) + + /** + * Creates a payout to a customer. A payout allows a merchant to send money to a customer’s + * Cash App account. + * + * @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. + * @param accountReferenceId Identifier of the account or customer associated to the on file action. + */ + data class OnFilePayoutAction( + override val scopeId: String? = null, + val accountReferenceId: String? = null, + override val referenceId: String? = null, + ) : CashAppPayPaymentAction(scopeId, referenceId) } From 394f8368d7717f5547c99f1f522088b8d3e86ba7 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 28 Jan 2026 16:59:11 +0100 Subject: [PATCH 2/5] Add Tests for CustomerRequestDataFactory --- .gitignore | 1 + .../app/cash/paykit/core/fakes/FakeData.kt | 6 + .../CustomerRequestDataFactoryTests.kt | 333 ++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 core/src/test/java/app/cash/paykit/core/models/request/CustomerRequestDataFactoryTests.kt diff --git a/.gitignore b/.gitignore index 56cc642..29de924 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ +/.claude/settings.local.json diff --git a/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt b/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt index 3287035..a1731df 100644 --- a/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt +++ b/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt @@ -16,6 +16,8 @@ package app.cash.paykit.core.fakes import app.cash.paykit.core.models.sdk.CashAppPayCurrency.USD +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFilePayoutAction import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction object FakeData { @@ -29,7 +31,11 @@ object FakeData { const val USER_AGENT = "Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)" const val SDK_ENVIRONMENT_SANDBOX = "SANDBOX" + const val FAKE_ACCOUNT_REFERENCE_ID = "fake_account_ref_123" + val oneTimePayment = OneTimeAction(USD, FAKE_AMOUNT, BRAND_ID) + val onFilePayment = OnFileAction(BRAND_ID, FAKE_ACCOUNT_REFERENCE_ID) + val onFilePayoutAction = OnFilePayoutAction(BRAND_ID, FAKE_ACCOUNT_REFERENCE_ID) val validCreateCustomerJSONresponse = """{ "request":{ diff --git a/core/src/test/java/app/cash/paykit/core/models/request/CustomerRequestDataFactoryTests.kt b/core/src/test/java/app/cash/paykit/core/models/request/CustomerRequestDataFactoryTests.kt new file mode 100644 index 0000000..ded1184 --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/models/request/CustomerRequestDataFactoryTests.kt @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2023 Cash App + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.paykit.core.models.request + +import app.cash.paykit.core.fakes.FakeData +import app.cash.paykit.core.models.pii.PiiString +import app.cash.paykit.core.models.sdk.CashAppPayCurrency.USD +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFilePayoutAction +import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CustomerRequestDataFactoryTests { + + @Test + fun `build with OnFilePayoutAction returns correct action type`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions).hasSize(1) + assertThat(result.actions[0].type).isEqualTo("ON_FILE_PAYOUT") + } + + @Test + fun `build with OnFilePayoutAction uses scopeId when provided`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].scopeId).isEqualTo(FakeData.BRAND_ID) + } + + @Test + fun `build with OnFilePayoutAction defaults to clientId when scopeId is null`() { + val action = OnFilePayoutAction(scopeId = null) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].scopeId).isEqualTo(FakeData.CLIENT_ID) + } + + @Test + fun `build with OnFilePayoutAction wraps accountReferenceId in PiiString`() { + val accountRef = "customer_123" + val action = OnFilePayoutAction( + scopeId = FakeData.BRAND_ID, + accountReferenceId = accountRef, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].accountReferenceId).isNotNull() + assertThat(result.actions[0].accountReferenceId).isInstanceOf(PiiString::class.java) + assertThat(result.actions[0].accountReferenceId.toString()).isEqualTo(accountRef) + } + + @Test + fun `build with OnFilePayoutAction sets null accountReferenceId when not provided`() { + val action = OnFilePayoutAction( + scopeId = FakeData.BRAND_ID, + accountReferenceId = null, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].accountReferenceId).isNull() + } + + @Test + fun `build with OnFileAction returns correct action type`() { + val action = OnFileAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions).hasSize(1) + assertThat(result.actions[0].type).isEqualTo("ON_FILE_PAYMENT") + } + + @Test + fun `build with OnFileAction uses scopeId when provided`() { + val action = OnFileAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].scopeId).isEqualTo(FakeData.BRAND_ID) + } + + @Test + fun `build with OnFileAction defaults to clientId when scopeId is null`() { + val action = OnFileAction(scopeId = null) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].scopeId).isEqualTo(FakeData.CLIENT_ID) + } + + @Test + fun `build with OnFileAction wraps accountReferenceId in PiiString`() { + val accountRef = "customer_456" + val action = OnFileAction( + scopeId = FakeData.BRAND_ID, + accountReferenceId = accountRef, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].accountReferenceId).isNotNull() + assertThat(result.actions[0].accountReferenceId).isInstanceOf(PiiString::class.java) + assertThat(result.actions[0].accountReferenceId.toString()).isEqualTo(accountRef) + } + + @Test + fun `build with OneTimeAction returns correct action type`() { + val action = OneTimeAction( + currency = USD, + amount = FakeData.FAKE_AMOUNT, + scopeId = FakeData.BRAND_ID, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions).hasSize(1) + assertThat(result.actions[0].type).isEqualTo("ONE_TIME_PAYMENT") + } + + @Test + fun `build with OneTimeAction includes amount and currency`() { + val action = OneTimeAction( + currency = USD, + amount = FakeData.FAKE_AMOUNT, + scopeId = FakeData.BRAND_ID, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].amount_cents).isEqualTo(FakeData.FAKE_AMOUNT) + assertThat(result.actions[0].currency).isEqualTo("USD") + } + + @Test + fun `build with OneTimeAction defaults to clientId when scopeId is null`() { + val action = OneTimeAction( + currency = USD, + amount = FakeData.FAKE_AMOUNT, + scopeId = null, + ) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + ) + + assertThat(result.actions[0].scopeId).isEqualTo(FakeData.CLIENT_ID) + } + + @Test + fun `build with multiple action types preserves order`() { + val oneTimeAction = OneTimeAction(currency = USD, amount = FakeData.FAKE_AMOUNT) + val onFileAction = OnFileAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(oneTimeAction, onFileAction), + ) + + assertThat(result.actions).hasSize(2) + assertThat(result.actions[0].type).isEqualTo("ONE_TIME_PAYMENT") + assertThat(result.actions[1].type).isEqualTo("ON_FILE_PAYMENT") + } + + @Test + fun `build sets channel to IN_APP for new requests`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + isRequestUpdate = false, + ) + + assertThat(result.channel).isEqualTo("IN_APP") + } + + @Test + fun `build sets channel to null for request updates`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + isRequestUpdate = true, + ) + + assertThat(result.channel).isNull() + } + + @Test + fun `build wraps redirectUri in PiiString for new requests`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + isRequestUpdate = false, + ) + + assertThat(result.redirectUri).isNotNull() + assertThat(result.redirectUri.toString()).isEqualTo(FakeData.REDIRECT_URI) + } + + @Test + fun `build sets redirectUri to null for request updates`() { + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = listOf(action), + isRequestUpdate = true, + ) + + assertThat(result.redirectUri).isNull() + } + + @Test + fun `build wraps referenceId in PiiString when provided`() { + val refId = "ref_123" + val action = OnFilePayoutAction(scopeId = FakeData.BRAND_ID) + + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = refId, + paymentActions = listOf(action), + ) + + assertThat(result.referenceId).isNotNull() + assertThat(result.referenceId.toString()).isEqualTo(refId) + } + + @Test + fun `build with empty action list returns empty actions`() { + val result = CustomerRequestDataFactory.build( + clientId = FakeData.CLIENT_ID, + redirectUri = FakeData.REDIRECT_URI, + referenceId = null, + paymentActions = emptyList(), + ) + + assertThat(result.actions).isEmpty() + } +} From 981d4d6a6f4742230e7a88ca5a11ce1221b7f883 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 28 Jan 2026 20:06:05 +0100 Subject: [PATCH 3/5] Use java.time.Instant instead --- CHANGELOG.md | 4 ++ build.gradle | 2 +- core/build.gradle | 6 +- core/consumer-rules.pro | 70 ------------------- .../PayKitAnalyticsEventDispatcherImpl.kt | 4 +- .../cash/paykit/core/impl/CashAppPayImpl.kt | 15 ++-- .../core/models/response/AuthFlowTriggers.kt | 2 +- .../models/response/CustomerResponseData.kt | 7 +- .../cash/paykit/core/network/MoshiProvider.kt | 2 +- .../core/network/adapters/InstantAdapter.kt | 5 +- .../app/cash/paykit/core/PiiStringTests.kt | 2 +- 11 files changed, 27 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0189010..d961530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# NEXT RELEASE | WIP +## Breaking Changes +- Our internal implementation no longer depends on `kotlinx-datetime` + # 2.6.0 ## Breaking Changes - Introduces new button styles and deprecates existing styles. This is also replaces diff --git a/build.gradle b/build.gradle index 553f497..38faba9 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { google_truth_version = '1.4.5' startup_version = '1.2.0' okhttp_version = '5.3.2' - kotlinx_date_version = '0.6.2' + desugar_jdk_libs_version = '2.1.5' versions = [ 'minSdk': 21, diff --git a/core/build.gradle b/core/build.gradle index d662a03..cb58974 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -31,6 +31,7 @@ android { } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } @@ -66,15 +67,14 @@ android { dependencies { + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_jdk_libs_version" + // We want to lock this dependency at a lower than latest version to not force transitive updates onto merchants. //noinspection GradleDependency ksp("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version") //noinspection GradleDependency implementation("com.squareup.moshi:moshi-kotlin:$moshi_version") - // KotlinX Dates. - implementation "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_date_version" - // Provides a lifecycle for the whole application process. implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro index 2346dba..cc900c4 100644 --- a/core/consumer-rules.pro +++ b/core/consumer-rules.pro @@ -3,73 +3,3 @@ -keep enum app.cash.paykit.core.models.sdk.CashAppPayCurrency { *; } -keep enum app.cash.paykit.core.impl.RequestType { *; } -keep enum app.cash.paykit.core.utils.ThreadPurpose { *; } - - -# Rules for Kotlin Serializer - a transitive dependency of KotlinX Datetime. -# Can probably be removed after datetime is updated. - -# Keep `Companion` object fields of serializable classes. -# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. --if @kotlinx.serialization.Serializable class ** --keepclassmembers class <1> { - static <1>$Companion Companion; -} - -# Keep `serializer()` on companion objects (both default and named) of serializable classes. --if @kotlinx.serialization.Serializable class ** { - static **$* *; -} --keepclassmembers class <2>$<3> { - kotlinx.serialization.KSerializer serializer(...); -} - -# Keep `INSTANCE.serializer()` of serializable objects. --if @kotlinx.serialization.Serializable class ** { - public static ** INSTANCE; -} --keepclassmembers class <1> { - public static <1> INSTANCE; - kotlinx.serialization.KSerializer serializer(...); -} - -# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. --keepattributes RuntimeVisibleAnnotations,AnnotationDefault - -# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes -# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900 --dontnote kotlinx.serialization.** - -# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes. -# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning. -# However, since in this case they will not be used, we can disable these warnings --dontwarn kotlinx.serialization.internal.ClassValueReferences - -# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`. -# If you have any, replace classes with those containing named companion objects. --keepattributes InnerClasses # Needed for `getDeclaredClasses`. - --if @kotlinx.serialization.Serializable class -kotlinx.datetime.Instant$Companion, # <-- List serializable classes with named companions. -kotlinx.datetime.Instant$Companion$serializer -{ - static **$* *; -} --keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. - static <1>$$serializer INSTANCE; -} - -# Keep both serializer and serializable classes to save the attribute InnerClasses --keepclasseswithmembers, allowshrinking, allowobfuscation, allowaccessmodification class -kotlinx.datetime.Instant$Companion, # <-- List serializable classes with named companions. -kotlinx.datetime.Instant$Companion$serializer -{ - *; -} - --dontwarn kotlinx.serialization.KSerializer --dontwarn kotlinx.serialization.Serializable --dontwarn kotlinx.serialization.descriptors.PrimitiveKind$STRING --dontwarn kotlinx.serialization.descriptors.PrimitiveKind --dontwarn kotlinx.serialization.descriptors.SerialDescriptor --dontwarn kotlinx.serialization.descriptors.SerialDescriptorsKt --dontwarn kotlinx.serialization.internal.AbstractPolymorphicSerializer \ No newline at end of file diff --git a/core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcherImpl.kt b/core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcherImpl.kt index b6af7ce..0513f37 100644 --- a/core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcherImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcherImpl.kt @@ -288,8 +288,8 @@ internal class PayKitAnalyticsEventDispatcherImpl( clientId, status = customerResponseData?.status, authMobileUrl = customerResponseData?.authFlowTriggers?.mobileUrl, - updatedAt = customerResponseData?.updatedAt?.toEpochMilliseconds(), - createdAt = customerResponseData?.createdAt?.toEpochMilliseconds(), + updatedAt = customerResponseData?.updatedAt?.toEpochMilli(), + createdAt = customerResponseData?.createdAt?.toEpochMilli(), originType = customerResponseData?.origin?.type, originId = customerResponseData?.origin?.id, requestChannel = CHANNEL_IN_APP, diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt index d7a107d..f892522 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt @@ -58,9 +58,11 @@ import app.cash.paykit.core.utils.ThreadPurpose.DEFERRED_REFRESH import app.cash.paykit.core.utils.ThreadPurpose.REFRESH_AUTH_TOKEN import app.cash.paykit.core.utils.orElse import app.cash.paykit.logging.CashAppLogger -import kotlinx.datetime.Clock +import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +import kotlin.time.toKotlinDuration +import java.time.Duration as JavaDuration /** * @param clientId Client Identifier that should be provided by Cash PayKit integration. @@ -480,9 +482,9 @@ internal class CashAppPayImpl( } // Stop refreshing if the request has expired. - val currentTime = Clock.System.now() + val currentTime = Instant.now() val hasExpired = - customerResponseData?.expiresAt?.let { expiresAt -> currentTime > expiresAt } ?: false + customerResponseData?.expiresAt?.let { expiresAt -> currentTime.isAfter(expiresAt) } ?: false if (hasExpired) { logger.logError(CAP_TAG, "Customer request has expired. Stopping refresh.") return@createThread @@ -535,11 +537,12 @@ internal class CashAppPayImpl( return } - val ttlSeconds = customerResponseData.authFlowTriggers.refreshesAt.minus( + val ttl = JavaDuration.between( customerResponseData.createdAt, - ) + customerResponseData.authFlowTriggers.refreshesAt, + ).toKotlinDuration() - val refreshDelay = ttlSeconds.inWholeSeconds.minus(TOKEN_REFRESH_WINDOW.inWholeSeconds) + val refreshDelay = ttl.inWholeSeconds.minus(TOKEN_REFRESH_WINDOW.inWholeSeconds) logger.logVerbose( CAP_TAG, "Scheduling unauthorized customer request refresh in $refreshDelay seconds.", diff --git a/core/src/main/java/app/cash/paykit/core/models/response/AuthFlowTriggers.kt b/core/src/main/java/app/cash/paykit/core/models/response/AuthFlowTriggers.kt index 04b73f0..e7bd0a8 100644 --- a/core/src/main/java/app/cash/paykit/core/models/response/AuthFlowTriggers.kt +++ b/core/src/main/java/app/cash/paykit/core/models/response/AuthFlowTriggers.kt @@ -17,7 +17,7 @@ package app.cash.paykit.core.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import kotlinx.datetime.Instant +import java.time.Instant @JsonClass(generateAdapter = true) data class AuthFlowTriggers( diff --git a/core/src/main/java/app/cash/paykit/core/models/response/CustomerResponseData.kt b/core/src/main/java/app/cash/paykit/core/models/response/CustomerResponseData.kt index d8dd86e..1b9932e 100644 --- a/core/src/main/java/app/cash/paykit/core/models/response/CustomerResponseData.kt +++ b/core/src/main/java/app/cash/paykit/core/models/response/CustomerResponseData.kt @@ -19,8 +19,7 @@ import app.cash.paykit.core.models.common.Action import app.cash.paykit.core.models.pii.PiiString import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import kotlinx.datetime.Clock.System -import kotlinx.datetime.Instant +import java.time.Instant const val STATUS_PENDING = "PENDING" const val STATUS_PROCESSING = "PROCESSING" @@ -57,8 +56,8 @@ data class CustomerResponseData( val referenceId: PiiString?, ) { fun isAuthTokenExpired(): Boolean { - val now = System.now() - val isExpired = now > (authFlowTriggers?.refreshesAt ?: return false) + val now = Instant.now() + val isExpired = now.isAfter(authFlowTriggers?.refreshesAt ?: return false) return isExpired } } diff --git a/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt b/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt index 539466c..cdc3506 100644 --- a/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt +++ b/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt @@ -20,7 +20,7 @@ import app.cash.paykit.core.network.adapters.InstantAdapter import app.cash.paykit.core.network.adapters.PiiStringClearTextAdapter import app.cash.paykit.core.network.adapters.PiiStringRedactAdapter import com.squareup.moshi.Moshi -import kotlinx.datetime.Instant +import java.time.Instant internal object MoshiProvider { fun provideDefault(redactPii: Boolean = false): Moshi { diff --git a/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt b/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt index 265d3cd..aa5d7b5 100644 --- a/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt +++ b/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt @@ -19,8 +19,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader.Token.NULL import com.squareup.moshi.JsonWriter -import kotlinx.datetime.Instant -import kotlinx.datetime.toInstant +import java.time.Instant internal class InstantAdapter : JsonAdapter() { @@ -29,7 +28,7 @@ internal class InstantAdapter : JsonAdapter() { return reader.nextNull() } val timeRFC3339 = reader.nextString() - return timeRFC3339.toInstant() + return Instant.parse(timeRFC3339) } override fun toJson(writer: JsonWriter, value: Instant?) { diff --git a/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt b/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt index efcda33..3a32278 100644 --- a/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt +++ b/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt @@ -23,8 +23,8 @@ import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter -import kotlinx.datetime.Instant import org.junit.Test +import java.time.Instant class PiiStringTests { From 34bca15f99ccdfdd1ac2eb274745d3d96853e2cc Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 2 Feb 2026 15:17:33 +0100 Subject: [PATCH 4/5] Update changelog --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d961530..5f5aec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # NEXT RELEASE | WIP ## Breaking Changes -- Our internal implementation no longer depends on `kotlinx-datetime` + - Our internal implementation no longer depends on `kotlinx-datetime`, and now uses Java 8 time classes, +namely `java.time.Instant`. +As a result, if your app or library supports Android versions below API 26, you must enable +**[coreLibraryDesugaring](https://developer.android.com/studio/write/java8-support-table)** if it isn't already. In most cases, this will be evident at compile time, +as the build will fail with an error similar to the following: + +``` +Dependency XYZ requires core library desugaring to be enabled for :your-app-module. +``` + + - Add new action `ON_FILE_PAYOUT`. A payout allows a merchant to send money to a user's Cash App account. # 2.6.0 ## Breaking Changes From 2299d1ef3fc3b3d5005ae826c12425ee9be5420a Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 2 Feb 2026 15:28:24 +0100 Subject: [PATCH 5/5] Update binary compatibility baseline --- core/api/core.api | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/api/core.api b/core/api/core.api index fd6f925..af75f6b 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -505,18 +505,18 @@ public final class app/cash/paykit/core/models/response/ApiErrorResponseJsonAdap } public final class app/cash/paykit/core/models/response/AuthFlowTriggers { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/datetime/Instant;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/time/Instant;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Lkotlinx/datetime/Instant; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lapp/cash/paykit/core/models/response/AuthFlowTriggers; - public static synthetic fun copy$default (Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lapp/cash/paykit/core/models/response/AuthFlowTriggers; + public final fun component4 ()Ljava/time/Instant; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/time/Instant;)Lapp/cash/paykit/core/models/response/AuthFlowTriggers; + public static synthetic fun copy$default (Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/time/Instant;ILjava/lang/Object;)Lapp/cash/paykit/core/models/response/AuthFlowTriggers; public fun equals (Ljava/lang/Object;)Z public final fun getMobileUrl ()Ljava/lang/String; public final fun getQrCodeImageUrl ()Ljava/lang/String; public final fun getQrCodeSvgUrl ()Ljava/lang/String; - public final fun getRefreshesAt ()Lkotlinx/datetime/Instant; + public final fun getRefreshesAt ()Ljava/time/Instant; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -553,9 +553,9 @@ public final class app/cash/paykit/core/models/response/CustomerProfileJsonAdapt } public final class app/cash/paykit/core/models/response/CustomerResponseData { - public fun (Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;)V + public fun (Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;)V public final fun component1 ()Ljava/util/List; - public final fun component10 ()Lkotlinx/datetime/Instant; + public final fun component10 ()Ljava/time/Instant; public final fun component11 ()Lapp/cash/paykit/core/models/response/CustomerProfile; public final fun component12 ()Ljava/util/List; public final fun component13 ()Lapp/cash/paykit/core/models/pii/PiiString; @@ -565,24 +565,24 @@ public final class app/cash/paykit/core/models/response/CustomerResponseData { public final fun component5 ()Lapp/cash/paykit/core/models/response/Origin; public final fun component6 ()Lapp/cash/paykit/core/models/response/RequesterProfile; public final fun component7 ()Ljava/lang/String; - public final fun component8 ()Lkotlinx/datetime/Instant; - public final fun component9 ()Lkotlinx/datetime/Instant; - public final fun copy (Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;)Lapp/cash/paykit/core/models/response/CustomerResponseData; - public static synthetic fun copy$default (Lapp/cash/paykit/core/models/response/CustomerResponseData;Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;ILjava/lang/Object;)Lapp/cash/paykit/core/models/response/CustomerResponseData; + public final fun component8 ()Ljava/time/Instant; + public final fun component9 ()Ljava/time/Instant; + public final fun copy (Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;)Lapp/cash/paykit/core/models/response/CustomerResponseData; + public static synthetic fun copy$default (Lapp/cash/paykit/core/models/response/CustomerResponseData;Ljava/util/List;Lapp/cash/paykit/core/models/response/AuthFlowTriggers;Ljava/lang/String;Ljava/lang/String;Lapp/cash/paykit/core/models/response/Origin;Lapp/cash/paykit/core/models/response/RequesterProfile;Ljava/lang/String;Ljava/time/Instant;Ljava/time/Instant;Ljava/time/Instant;Lapp/cash/paykit/core/models/response/CustomerProfile;Ljava/util/List;Lapp/cash/paykit/core/models/pii/PiiString;ILjava/lang/Object;)Lapp/cash/paykit/core/models/response/CustomerResponseData; public fun equals (Ljava/lang/Object;)Z public final fun getActions ()Ljava/util/List; public final fun getAuthFlowTriggers ()Lapp/cash/paykit/core/models/response/AuthFlowTriggers; public final fun getChannel ()Ljava/lang/String; - public final fun getCreatedAt ()Lkotlinx/datetime/Instant; + public final fun getCreatedAt ()Ljava/time/Instant; public final fun getCustomerProfile ()Lapp/cash/paykit/core/models/response/CustomerProfile; - public final fun getExpiresAt ()Lkotlinx/datetime/Instant; + public final fun getExpiresAt ()Ljava/time/Instant; public final fun getGrants ()Ljava/util/List; public final fun getId ()Ljava/lang/String; public final fun getOrigin ()Lapp/cash/paykit/core/models/response/Origin; public final fun getReferenceId ()Lapp/cash/paykit/core/models/pii/PiiString; public final fun getRequesterProfile ()Lapp/cash/paykit/core/models/response/RequesterProfile; public final fun getStatus ()Ljava/lang/String; - public final fun getUpdatedAt ()Lkotlinx/datetime/Instant; + public final fun getUpdatedAt ()Ljava/time/Instant; public fun hashCode ()I public final fun isAuthTokenExpired ()Z public fun toString ()Ljava/lang/String;