From 6129240e7d37e7d8ffb1864413b00dff1d2c9058 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 17 Apr 2023 10:03:16 -0700 Subject: [PATCH 01/26] Fix Retrieve Existing request ID behavior for PROCESSING state (#95) * Fix Retrieve Existing request ID behavior for PENDING state * Add distinction between Pending & Processing --- .../paykit/core/impl/CashAppCashAppPayImpl.kt | 21 +++++-- .../models/response/CustomerResponseData.kt | 2 + .../cash/paykit/core/CashAppPayStateTests.kt | 57 +++++++++++++++++++ .../java/app/cash/paykit/core/FakeData.kt | 1 + 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt index fb6e046..9435ac0 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt @@ -44,6 +44,7 @@ import app.cash.paykit.core.models.common.NetworkResult.Success import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.STATUS_APPROVED import app.cash.paykit.core.models.response.STATUS_PENDING +import app.cash.paykit.core.models.response.STATUS_PROCESSING import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.utils.orElse @@ -196,8 +197,12 @@ internal class CashAppCashAppPayImpl( // Determine what kind of status we got. currentState = when (customerResponseData?.status) { + STATUS_PROCESSING -> { + Authorizing + } + STATUS_PENDING -> { - ReadyToAuthorize(networkResult.data.customerResponseData) + ReadyToAuthorize(customerResponseData!!) } STATUS_APPROVED -> { @@ -208,6 +213,8 @@ internal class CashAppCashAppPayImpl( Declined } } + + updateStateAndPoolForTransactionStatus() } } } @@ -360,16 +367,20 @@ internal class CashAppCashAppPayImpl( } } + private fun updateStateAndPoolForTransactionStatus() { + if (currentState is Authorizing) { + currentState = PollingTransactionStatus + poolTransactionStatus() + } + } + /** * Lifecycle callbacks. */ override fun onApplicationForegrounded() { logError("onApplicationForegrounded") - if (currentState is Authorizing) { - currentState = PollingTransactionStatus - poolTransactionStatus() - } + updateStateAndPoolForTransactionStatus() } override fun onApplicationBackgrounded() { 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 b37ffc8..2022230 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 @@ -20,7 +20,9 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass const val STATUS_PENDING = "PENDING" +const val STATUS_PROCESSING = "PROCESSING" const val STATUS_APPROVED = "APPROVED" +const val STATUS_DECLINED = "DECLINED" @JsonClass(generateAdapter = true) data class CustomerResponseData( diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index eadfc54..bea8bae 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -32,6 +32,9 @@ import app.cash.paykit.core.impl.CashAppPayLifecycleListener import app.cash.paykit.core.models.common.NetworkResult import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.CustomerTopLevelResponse +import app.cash.paykit.core.models.response.STATUS_APPROVED +import app.cash.paykit.core.models.response.STATUS_PENDING +import app.cash.paykit.core.models.response.STATUS_PROCESSING import com.google.common.truth.Truth.assertThat import io.mockk.MockKAnnotations import io.mockk.every @@ -108,6 +111,60 @@ class CashAppPayStateTests { verify { listener.cashAppPayStateDidChange(PollingTransactionStatus) } } + @Test + fun `startWithExistingCustomerRequest fetches existing Approved request`() { + val payKit = createPayKit() + val listener = mockk(relaxed = true) + payKit.registerForStateUpdates(listener) + val customerTopLevelResponse: NetworkResult.Success = mockk() + every { customerTopLevelResponse.data.customerResponseData.status } returns STATUS_APPROVED + every { + networkManager.retrieveUpdatedRequestData( + any(), + any(), + ) + } returns customerTopLevelResponse + + payKit.startWithExistingCustomerRequest(FakeData.REQUEST_ID) + verify { listener.cashAppPayStateDidChange(ofType(Approved::class)) } + } + + @Test + fun `startWithExistingCustomerRequest fetches existing Processing request`() { + val payKit = createPayKit() + val listener = mockk(relaxed = true) + payKit.registerForStateUpdates(listener) + val customerTopLevelResponse: NetworkResult.Success = mockk() + every { customerTopLevelResponse.data.customerResponseData.status } returns STATUS_PROCESSING + every { + networkManager.retrieveUpdatedRequestData( + any(), + any(), + ) + } returns customerTopLevelResponse + + payKit.startWithExistingCustomerRequest(FakeData.REQUEST_ID) + verify { listener.cashAppPayStateDidChange(ofType(PollingTransactionStatus::class)) } + } + + @Test + fun `startWithExistingCustomerRequest fetches existing Pending request`() { + val payKit = createPayKit() + val listener = mockk(relaxed = true) + payKit.registerForStateUpdates(listener) + val customerTopLevelResponse: NetworkResult.Success = mockk() + every { customerTopLevelResponse.data.customerResponseData.status } returns STATUS_PENDING + every { + networkManager.retrieveUpdatedRequestData( + any(), + any(), + ) + } returns customerTopLevelResponse + + payKit.startWithExistingCustomerRequest(FakeData.REQUEST_ID) + verify { listener.cashAppPayStateDidChange(ofType(ReadyToAuthorize::class)) } + } + @Test fun `ReadyToAuthorize State`() { val payKit = createPayKit() diff --git a/core/src/test/java/app/cash/paykit/core/FakeData.kt b/core/src/test/java/app/cash/paykit/core/FakeData.kt index bd99392..b3a1f6c 100644 --- a/core/src/test/java/app/cash/paykit/core/FakeData.kt +++ b/core/src/test/java/app/cash/paykit/core/FakeData.kt @@ -23,6 +23,7 @@ object FakeData { const val BRAND_ID = "fake_brand_id" const val REDIRECT_URI = "fake_redirect_uri" const val FAKE_AMOUNT = 500 + const val REQUEST_ID = "Request-id-fake-123" val oneTimePayment = OneTimeAction(USD, FAKE_AMOUNT, BRAND_ID) From be86a8328f2a3e8a4b26a2c2f83270564d81b053 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 24 Apr 2023 14:42:46 -0700 Subject: [PATCH 02/26] Code cleanup (and research on proguard rules being handed to consumers) (#98) * Add proguard rules to project for automatic client consumption * Fix constant name after renaming class * Concrete R8 rules are actually not needed --- build.gradle | 4 +-- ...Event.kt => AnalyticsEventStream2Event.kt} | 4 +-- .../PayKitAnalyticsEventDispatcherImpl.kt | 20 ++++++------ .../core/models/analytics/AnalyticsEvent.kt | 31 ------------------- .../core/models/analytics/AnalyticsRequest.kt | 23 -------------- .../models/analytics/AnalyticsResponse.kt | 29 ----------------- .../analytics/EventStream2AnalyticsRequest.kt | 23 -------------- .../models/analytics/EventStream2Event.kt | 2 ++ 8 files changed, 16 insertions(+), 120 deletions(-) rename core/src/main/java/app/cash/paykit/core/analytics/{EventStream2Event.kt => AnalyticsEventStream2Event.kt} (88%) delete mode 100644 core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsEvent.kt delete mode 100644 core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsRequest.kt delete mode 100644 core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsResponse.kt delete mode 100644 core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2AnalyticsRequest.kt diff --git a/build.gradle b/build.gradle index 9288962..69a70f7 100644 --- a/build.gradle +++ b/build.gradle @@ -50,11 +50,11 @@ subprojects { subproject -> } } } -def NEXT_VERSION = "2.0.1-SNAPSHOT" +def NEXT_VERSION = "2.0.7-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.0.0' + version = '2.0.6-SNAPSHOT' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing { diff --git a/core/src/main/java/app/cash/paykit/core/analytics/EventStream2Event.kt b/core/src/main/java/app/cash/paykit/core/analytics/AnalyticsEventStream2Event.kt similarity index 88% rename from core/src/main/java/app/cash/paykit/core/analytics/EventStream2Event.kt rename to core/src/main/java/app/cash/paykit/core/analytics/AnalyticsEventStream2Event.kt index 3842964..c964c0f 100644 --- a/core/src/main/java/app/cash/paykit/core/analytics/EventStream2Event.kt +++ b/core/src/main/java/app/cash/paykit/core/analytics/AnalyticsEventStream2Event.kt @@ -20,13 +20,13 @@ import app.cash.paykit.analytics.core.Deliverable /** * Class that represents the payload to be delivered to the ES2 API. */ -internal data class EventStream2Event constructor( +internal data class AnalyticsEventStream2Event constructor( override val content: String, ) : Deliverable { override val type = ESEventType override val metaData = null companion object { - const val ESEventType = "EventStream2Event" + const val ESEventType = "AnalyticsEventStream2Event" } } 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 5c341a7..7d24af5 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 @@ -15,7 +15,6 @@ */ package app.cash.paykit.core.analytics -import EventStream2Event import app.cash.paykit.analytics.PayKitAnalytics import app.cash.paykit.analytics.core.DeliveryHandler import app.cash.paykit.analytics.core.DeliveryListener @@ -32,8 +31,9 @@ import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize import app.cash.paykit.core.CashAppPayState.RetrievingExistingCustomerRequest import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.NetworkManager -import app.cash.paykit.core.analytics.EventStream2Event.Companion.ESEventType +import app.cash.paykit.core.analytics.AnalyticsEventStream2Event.Companion.ESEventType import app.cash.paykit.core.exceptions.CashAppCashAppPayApiNetworkException +import app.cash.paykit.core.models.analytics.EventStream2Event import app.cash.paykit.core.models.analytics.payloads.AnalyticsBasePayload import app.cash.paykit.core.models.analytics.payloads.AnalyticsCustomerRequestPayload import app.cash.paykit.core.models.analytics.payloads.AnalyticsEventListenerPayload @@ -95,7 +95,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( encodeToJsonString(initializationPayload, AnalyticsInitializationPayload.CATALOG) // Schedule event to be sent. - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun eventListenerAdded() { @@ -105,7 +105,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsEventListenerPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun eventListenerRemoved() { @@ -115,7 +115,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsEventListenerPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun createdCustomerRequest( @@ -127,7 +127,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsCustomerRequestPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun updatedCustomerRequest( @@ -139,7 +139,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsCustomerRequestPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun genericStateChanged( @@ -150,7 +150,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( eventFromCustomerResponseData(customerResponseData).copy(action = stateToAnalyticsAction(cashAppPayState)) val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsCustomerRequestPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun stateApproved( @@ -160,7 +160,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( eventFromCustomerResponseData(approved.responseData).copy(action = stateToAnalyticsAction(approved)) val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsCustomerRequestPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun exceptionOccurred( @@ -187,7 +187,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( val es2EventAsJsonString = encodeToJsonString(eventPayload, AnalyticsCustomerRequestPayload.CATALOG) - payKitAnalytics.scheduleForDelivery(EventStream2Event(es2EventAsJsonString)) + payKitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(es2EventAsJsonString)) } override fun shutdown() { diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsEvent.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsEvent.kt deleted file mode 100644 index 998a1c6..0000000 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsEvent.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - */ -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class AnalyticsEvent( - @Json(name = "app_name") - val appName: String, - @Json(name = "catalog_name") - val catalogName: String, - @Json(name = "json_data") - val jsonData: String, - @Json(name = "recorded_at_usec") - val recordedAt: Long, - @Json(name = "uuid") - val uuid: String, -) diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsRequest.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsRequest.kt deleted file mode 100644 index 6492a88..0000000 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsRequest.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class AnalyticsRequest( - @Json(name = "events") - val events: List, -) diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsResponse.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsResponse.kt deleted file mode 100644 index 4d4df69..0000000 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/AnalyticsResponse.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.analytics - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class AnalyticsResponse( - @Json(name = "failure_count") - val failureCount: Int, - @Json(name = "invalid_count") - val invalidCount: Int, - @Json(name = "success_count") - val successCount: Int, -) diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2AnalyticsRequest.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2AnalyticsRequest.kt deleted file mode 100644 index e2eff1a..0000000 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2AnalyticsRequest.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class EventStream2AnalyticsRequest( - @Json(name = "events") - val events: List, -) diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Event.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Event.kt index 214fe84..77670e2 100644 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Event.kt +++ b/core/src/main/java/app/cash/paykit/core/models/analytics/EventStream2Event.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package app.cash.paykit.core.models.analytics + import com.squareup.moshi.Json import com.squareup.moshi.JsonClass From c1fc480257b0bddb3ef0a319091698483866ed1d Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Tue, 25 Apr 2023 10:37:19 -0700 Subject: [PATCH 03/26] Add enum for GrantType (#99) --- build.gradle | 5 +++-- .../cash/paykit/core/models/response/Grant.kt | 3 ++- .../paykit/core/models/response/GrantType.kt | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/app/cash/paykit/core/models/response/GrantType.kt diff --git a/build.gradle b/build.gradle index 69a70f7..4b9c394 100644 --- a/build.gradle +++ b/build.gradle @@ -50,11 +50,12 @@ subprojects { subproject -> } } } -def NEXT_VERSION = "2.0.7-SNAPSHOT" + +def NEXT_VERSION = "2.1.1-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.0.6-SNAPSHOT' + version = '2.1.0' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing { diff --git a/core/src/main/java/app/cash/paykit/core/models/response/Grant.kt b/core/src/main/java/app/cash/paykit/core/models/response/Grant.kt index 85aa4be..7f023b1 100644 --- a/core/src/main/java/app/cash/paykit/core/models/response/Grant.kt +++ b/core/src/main/java/app/cash/paykit/core/models/response/Grant.kt @@ -16,6 +16,7 @@ package app.cash.paykit.core.models.response import app.cash.paykit.core.models.common.Action +import app.cash.paykit.core.models.response.GrantType.UNKNOWN import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -26,7 +27,7 @@ data class Grant( @Json(name = "status") val status: String, @Json(name = "type") - val type: String, + val type: GrantType = UNKNOWN, @Json(name = "action") val action: Action, @Json(name = "channel") diff --git a/core/src/main/java/app/cash/paykit/core/models/response/GrantType.kt b/core/src/main/java/app/cash/paykit/core/models/response/GrantType.kt new file mode 100644 index 0000000..0b5ceab --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/models/response/GrantType.kt @@ -0,0 +1,22 @@ +/* + * 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.response + +enum class GrantType { + ONE_TIME, + EXTENDED, + UNKNOWN, +} From 761eaaadef953d60d76fb3420a8085d6352c6347 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Thu, 27 Apr 2023 19:02:18 -0700 Subject: [PATCH 04/26] Update Robolectric dependency (#102) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4b9c394..5264ab8 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { lifecycle_version = '2.5.1' mockk_version = '1.13.3' coroutines_test_version = '1.6.4' - robolectric_version = '4.9.2' + robolectric_version = '4.10' mockwebserver_version = '4.10.0' google_truth_version = '1.1.3' startup_version = '1.1.1' From 25017aa2963ac9f91f4b80069370a92feaa5b021 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 28 Apr 2023 10:21:35 -0700 Subject: [PATCH 05/26] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6400518..ed0fe64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 2.1.0 + +This version introduces a concrete type for `GrantType` under the `Grant` class. Before this field was a `string`. +This is a breaking change. The following has changed: + +- `Grant.type` from `string` to `GrantType`
+- Possible `GrantType` values: `ONE_TIME`, `EXTENDED`, `UNKNOWN`. These values match their spelling with what is described by our public API. +- For convenience, `ONE_TIME` applies to a grant can only be used once, where `EXTENDED` applies to grants that can be repeatedly used. +- `CashAppPayPaymentAction` no longer contains the `redirectUri` parameter. Instead, pass that value to the `createCustomerRequest` function. +- Fixes to the behavior of `startWithExistingCustomerRequest` + + # 2.0.0 This version introduces support for multiple `CashAppPayPaymentAction` per `createCustomerRequest`. From d84525c8ec767e8cf8612c3193f5a53e43ab99ec Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 5 Jun 2023 15:02:03 -0700 Subject: [PATCH 06/26] Use Epoch usec for Analytics timestamp (#107) --- .../core/analytics/PayKitAnalyticsEventDispatcherImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7d24af5..4758575 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 @@ -50,6 +50,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import java.util.* +import java.util.concurrent.TimeUnit private const val APP_NAME = "paykitsdk-android" private const val PLATFORM = "android" @@ -246,7 +247,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( catalogName = catalog, uuid = UUID.randomUUID().toString(), jsonData = jsonData, - recordedAt = System.nanoTime() * 10, + recordedAt = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()), ) // Transform ES2 event into a JSON String. From 8cc78049a142c55355e40ad016b51681f0b46385 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 21 Jun 2023 10:53:46 -0700 Subject: [PATCH 07/26] Add UT for Analytics (#108) --- .../PayKitAnalyticsEventDispatcherImpl.kt | 11 ++- .../java/app/cash/paykit/core/utils/Clock.kt | 23 +++++ .../cash/paykit/core/utils/ClockRealImpl.kt | 24 +++++ .../app/cash/paykit/core/utils/UUIDManager.kt | 24 +++++ .../paykit/core/utils/UUIDManagerRealImpl.kt | 24 +++++ .../paykit/core/CashAppPayAuthorizeTests.kt | 1 + .../paykit/core/CashAppPayExceptionsTests.kt | 1 + .../cash/paykit/core/CashAppPayStateTests.kt | 1 + .../app/cash/paykit/core/NetworkErrorTests.kt | 1 + .../app/cash/paykit/core/NetworkRetryTests.kt | 1 + .../PayKitAnalyticsEventDispatcherImplTest.kt | 89 +++++++++++++++++++ .../app/cash/paykit/core/fakes/FakeClock.kt | 28 ++++++ .../cash/paykit/core/{ => fakes}/FakeData.kt | 6 +- .../cash/paykit/core/fakes/FakeUUIDManager.kt | 28 ++++++ .../app/cash/paykit/core/utils/ClockTests.kt | 32 +++++++ .../app/cash/paykit/core/utils/UUIDTests.kt | 31 +++++++ .../core/CashAppPayProdExceptionsTests.kt | 1 + 17 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/app/cash/paykit/core/utils/Clock.kt create mode 100644 core/src/main/java/app/cash/paykit/core/utils/ClockRealImpl.kt create mode 100644 core/src/main/java/app/cash/paykit/core/utils/UUIDManager.kt create mode 100644 core/src/main/java/app/cash/paykit/core/utils/UUIDManagerRealImpl.kt create mode 100644 core/src/test/java/app/cash/paykit/core/PayKitAnalyticsEventDispatcherImplTest.kt create mode 100644 core/src/test/java/app/cash/paykit/core/fakes/FakeClock.kt rename core/src/test/java/app/cash/paykit/core/{ => fakes}/FakeData.kt (92%) create mode 100644 core/src/test/java/app/cash/paykit/core/fakes/FakeUUIDManager.kt create mode 100644 core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt create mode 100644 core/src/test/java/app/cash/paykit/core/utils/UUIDTests.kt 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 4758575..bfb0b5c 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 @@ -46,11 +46,14 @@ import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.Grant import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction +import app.cash.paykit.core.utils.Clock +import app.cash.paykit.core.utils.ClockRealImpl +import app.cash.paykit.core.utils.UUIDManager +import app.cash.paykit.core.utils.UUIDManagerRealImpl import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import java.util.* -import java.util.concurrent.TimeUnit private const val APP_NAME = "paykitsdk-android" private const val PLATFORM = "android" @@ -64,6 +67,8 @@ internal class PayKitAnalyticsEventDispatcherImpl( private val payKitAnalytics: PayKitAnalytics, private val networkManager: NetworkManager, private val moshi: Moshi = Moshi.Builder().build(), + private val uuidManager: UUIDManager = UUIDManagerRealImpl(), + private val clock: Clock = ClockRealImpl(), ) : PayKitAnalyticsEventDispatcher { private var eventStreamDeliverHandler: DeliveryHandler? = null @@ -245,9 +250,9 @@ internal class PayKitAnalyticsEventDispatcherImpl( EventStream2Event( appName = APP_NAME, catalogName = catalog, - uuid = UUID.randomUUID().toString(), + uuid = uuidManager.generateUUID(), jsonData = jsonData, - recordedAt = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()), + recordedAt = clock.currentTimeInMicroseconds(), ) // Transform ES2 event into a JSON String. diff --git a/core/src/main/java/app/cash/paykit/core/utils/Clock.kt b/core/src/main/java/app/cash/paykit/core/utils/Clock.kt new file mode 100644 index 0000000..44e5513 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/Clock.kt @@ -0,0 +1,23 @@ +/* + * 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.utils + +internal interface Clock { + /** + * Returns the current Epoch time in microseconds (AKA usec). + */ + fun currentTimeInMicroseconds(): Long +} diff --git a/core/src/main/java/app/cash/paykit/core/utils/ClockRealImpl.kt b/core/src/main/java/app/cash/paykit/core/utils/ClockRealImpl.kt new file mode 100644 index 0000000..90483ae --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/ClockRealImpl.kt @@ -0,0 +1,24 @@ +/* + * 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.utils + +import java.util.concurrent.TimeUnit + +internal class ClockRealImpl : Clock { + override fun currentTimeInMicroseconds(): Long { + return TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) + } +} diff --git a/core/src/main/java/app/cash/paykit/core/utils/UUIDManager.kt b/core/src/main/java/app/cash/paykit/core/utils/UUIDManager.kt new file mode 100644 index 0000000..3613b89 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/UUIDManager.kt @@ -0,0 +1,24 @@ +/* + * 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.utils + +internal interface UUIDManager { + + /** + * Returns a UUID as a String. + */ + fun generateUUID(): String +} diff --git a/core/src/main/java/app/cash/paykit/core/utils/UUIDManagerRealImpl.kt b/core/src/main/java/app/cash/paykit/core/utils/UUIDManagerRealImpl.kt new file mode 100644 index 0000000..374ca76 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/UUIDManagerRealImpl.kt @@ -0,0 +1,24 @@ +/* + * 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.utils + +import java.util.UUID + +internal class UUIDManagerRealImpl : UUIDManager { + override fun generateUUID(): String { + return UUID.randomUUID().toString() + } +} diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt index f4f1ec7..082259b 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt @@ -16,6 +16,7 @@ package app.cash.paykit.core import app.cash.paykit.core.exceptions.CashAppPayIntegrationException +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import app.cash.paykit.core.models.response.CustomerResponseData import io.mockk.every diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt index 2c04db5..420c9d8 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt @@ -16,6 +16,7 @@ package app.cash.paykit.core import app.cash.paykit.core.exceptions.CashAppPayIntegrationException +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import app.cash.paykit.core.models.common.NetworkResult import io.mockk.MockKAnnotations diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index bea8bae..e8801cd 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -27,6 +27,7 @@ import app.cash.paykit.core.CashAppPayState.PollingTransactionStatus import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.android.ApplicationContextHolder +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import app.cash.paykit.core.impl.CashAppPayLifecycleListener import app.cash.paykit.core.models.common.NetworkResult diff --git a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt index 09c42ee..e808a7a 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt @@ -18,6 +18,7 @@ package app.cash.paykit.core import app.cash.paykit.core.CashAppPayState.CashAppPayExceptionState import app.cash.paykit.core.exceptions.CashAppCashAppPayApiNetworkException import app.cash.paykit.core.exceptions.CashAppPayConnectivityNetworkException +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import app.cash.paykit.core.impl.NetworkManagerImpl import app.cash.paykit.core.network.RetryManagerOptions diff --git a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt index a6d0457..b958a30 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt @@ -19,6 +19,7 @@ import app.cash.paykit.core.CashAppPayState.CashAppPayExceptionState import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize import app.cash.paykit.core.NetworkErrorTests.MockListener import app.cash.paykit.core.exceptions.CashAppPayConnectivityNetworkException +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import app.cash.paykit.core.impl.NetworkManagerImpl import app.cash.paykit.core.network.RetryManagerOptions diff --git a/core/src/test/java/app/cash/paykit/core/PayKitAnalyticsEventDispatcherImplTest.kt b/core/src/test/java/app/cash/paykit/core/PayKitAnalyticsEventDispatcherImplTest.kt new file mode 100644 index 0000000..22d974e --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/PayKitAnalyticsEventDispatcherImplTest.kt @@ -0,0 +1,89 @@ +/* + * 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 + +import app.cash.paykit.analytics.PayKitAnalytics +import app.cash.paykit.core.analytics.AnalyticsEventStream2Event +import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcherImpl +import app.cash.paykit.core.fakes.FakeClock +import app.cash.paykit.core.fakes.FakeData +import app.cash.paykit.core.fakes.FakeUUIDManager +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import org.junit.Before +import org.junit.Test + +class PayKitAnalyticsEventDispatcherImplTest { + + @MockK(relaxed = true) + private lateinit var networkManager: NetworkManager + + @MockK(relaxed = true) + private lateinit var paykitAnalytics: PayKitAnalytics + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun `sdkInitialized records valid analytics event`() { + val analyticsDispatcher = buildDispatcher() + analyticsDispatcher.sdkInitialized() + 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"}""" + + verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) } + } + + @Test + fun `eventListenerAdded records valid analytics event`() { + val analyticsDispatcher = buildDispatcher() + analyticsDispatcher.eventListenerAdded() + 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"}""" + + verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) } + } + + @Test + fun `eventListenerRemoved records valid analytics event`() { + val analyticsDispatcher = buildDispatcher() + analyticsDispatcher.eventListenerRemoved() + 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"}""" + + verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) } + } + + @Test + fun `SDK state update records valid analytics event`() { + val analyticsDispatcher = buildDispatcher() + analyticsDispatcher.genericStateChanged(CashAppPayState.Authorizing, null) + 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"}""" + + verify { paykitAnalytics.scheduleForDelivery(AnalyticsEventStream2Event(contents)) } + } + + private fun buildDispatcher() = PayKitAnalyticsEventDispatcherImpl( + FakeData.SDK_VERSION, + FakeData.CLIENT_ID, + FakeData.USER_AGENT, + FakeData.SDK_ENVIRONMENT_SANDBOX, + paykitAnalytics, + networkManager, + uuidManager = FakeUUIDManager(), + clock = FakeClock(), + ) +} diff --git a/core/src/test/java/app/cash/paykit/core/fakes/FakeClock.kt b/core/src/test/java/app/cash/paykit/core/fakes/FakeClock.kt new file mode 100644 index 0000000..8019824 --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/fakes/FakeClock.kt @@ -0,0 +1,28 @@ +/* + * 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.fakes + +import app.cash.paykit.core.utils.Clock + +class FakeClock : Clock { + companion object { + const val NOW = 123L + } + + override fun currentTimeInMicroseconds(): Long { + return NOW + } +} diff --git a/core/src/test/java/app/cash/paykit/core/FakeData.kt b/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt similarity index 92% rename from core/src/test/java/app/cash/paykit/core/FakeData.kt rename to core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt index b3a1f6c..3287035 100644 --- a/core/src/test/java/app/cash/paykit/core/FakeData.kt +++ b/core/src/test/java/app/cash/paykit/core/fakes/FakeData.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package app.cash.paykit.core +package app.cash.paykit.core.fakes import app.cash.paykit.core.models.sdk.CashAppPayCurrency.USD import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction @@ -25,6 +25,10 @@ object FakeData { const val FAKE_AMOUNT = 500 const val REQUEST_ID = "Request-id-fake-123" + const val SDK_VERSION = "1.0.0" + const val USER_AGENT = "Webkit/1.0.0 (Linux; U; Android 12; en-US; Samsung Build/XYZ)" + const val SDK_ENVIRONMENT_SANDBOX = "SANDBOX" + val oneTimePayment = OneTimeAction(USD, FAKE_AMOUNT, BRAND_ID) val validCreateCustomerJSONresponse = """{ diff --git a/core/src/test/java/app/cash/paykit/core/fakes/FakeUUIDManager.kt b/core/src/test/java/app/cash/paykit/core/fakes/FakeUUIDManager.kt new file mode 100644 index 0000000..1e8b3b5 --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/fakes/FakeUUIDManager.kt @@ -0,0 +1,28 @@ +/* + * 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.fakes + +import app.cash.paykit.core.utils.UUIDManager + +class FakeUUIDManager : UUIDManager { + + companion object { + const val UUID = "abc" + } + override fun generateUUID(): String { + return UUID + } +} diff --git a/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt b/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt new file mode 100644 index 0000000..f1d8d6d --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt @@ -0,0 +1,32 @@ +/* + * 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.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ClockTests { + + @Test + fun `currentTimeInMicroseconds should return current time in microseconds`() { + val clock = ClockRealImpl() + val currentTimeInMicroseconds = clock.currentTimeInMicroseconds() + assertThat(currentTimeInMicroseconds).isGreaterThan(0) + + // Microseconds of when the test was written. + assertThat(currentTimeInMicroseconds).isAtLeast(1686318558468000) + } +} diff --git a/core/src/test/java/app/cash/paykit/core/utils/UUIDTests.kt b/core/src/test/java/app/cash/paykit/core/utils/UUIDTests.kt new file mode 100644 index 0000000..8948f44 --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/utils/UUIDTests.kt @@ -0,0 +1,31 @@ +/* + * 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.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class UUIDTests { + @Test + fun `generateUUID should return a different valid UUID each time`() { + val uuidManager = UUIDManagerRealImpl() + val uuid = uuidManager.generateUUID() + assertThat(uuid).isNotEmpty() + assertThat(uuid).hasLength(36) + val secondUuid = uuidManager.generateUUID() + assertThat(secondUuid).isNotEqualTo(uuid) + } +} diff --git a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt index cac2c26..6c126a8 100644 --- a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt +++ b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt @@ -15,6 +15,7 @@ */ package app.cash.paykit.core +import app.cash.paykit.core.fakes.FakeData import app.cash.paykit.core.impl.CashAppCashAppPayImpl import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK From b975e844ba6a2f952b49fb4e43a660e37338cf38 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 28 Jun 2023 17:04:56 -0700 Subject: [PATCH 08/26] Refresh Auth Token for Customer Request (#109) * Initial support for Unauthorized Customer Request refresh * Add useful docs about token refresh window * Improve logging for CashAppCashAppPayImpl * Use Duration instead of Long for better time handling * Remove not needed call to interrupt * Schedule auth token refresh on `startWithExistingCustomerRequest` when status is "pending" * Support deferred auth token refresh upon authorize call * Better thread safety and handling * Update comment * Add new Refreshing state (#110) * Add Refreshing state * Use better explicit tag for logging --- build.gradle | 1 + core/build.gradle | 3 + .../java/app/cash/paykit/core/CashAppPay.kt | 3 + .../app/cash/paykit/core/CashAppPayState.kt | 7 + .../PayKitAnalyticsEventDispatcherImpl.kt | 10 +- .../app/cash/paykit/core/android/LogTag.kt | 18 ++ .../paykit/core/android/ThreadExtensions.kt | 34 ++++ .../paykit/core/impl/CashAppCashAppPayImpl.kt | 164 +++++++++++++++++- .../paykit/core/impl/NetworkManagerImpl.kt | 17 +- .../core/models/response/AuthFlowTriggers.kt | 3 +- .../models/response/CustomerResponseData.kt | 16 +- .../cash/paykit/core/network/MoshiProvider.kt | 26 +++ .../core/network/adapters/InstantAdapter.kt | 42 +++++ .../cash/paykit/core/CashAppPayStateTests.kt | 1 + 14 files changed, 325 insertions(+), 20 deletions(-) create mode 100644 core/src/main/java/app/cash/paykit/core/android/LogTag.kt create mode 100644 core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt create mode 100644 core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt create mode 100644 core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt diff --git a/build.gradle b/build.gradle index 5264ab8..d31bd6b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ buildscript { google_truth_version = '1.1.3' startup_version = '1.1.1' okhttp_version = '4.10.0' + kotlinx_date_version = '0.4.0' versions = [ 'minSdk': 21, diff --git a/core/build.gradle b/core/build.gradle index 9ed5ee5..04ccd74 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -67,6 +67,9 @@ dependencies { //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/src/main/java/app/cash/paykit/core/CashAppPay.kt b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt index d7d0f1b..f6d28d9 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt @@ -249,6 +249,9 @@ object CashAppPayFactory { private val ANALYTICS_DB_NAME_SANDBOX = "paykit-events-sandbox.db" private val ANALYTICS_PROD_ENVIRONMENT = "production" private val ANALYTICS_SANDBOX_ENVIRONMENT = "sandbox" + + // This is the threshold for when in advance of a token expiring we should refresh it. + internal val TOKEN_REFRESH_WINDOW = 10.seconds } interface CashAppPayListener { diff --git a/core/src/main/java/app/cash/paykit/core/CashAppPayState.kt b/core/src/main/java/app/cash/paykit/core/CashAppPayState.kt index cddb82d..658a6ca 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPayState.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPayState.kt @@ -59,6 +59,13 @@ sealed interface CashAppPayState { */ object Authorizing : CashAppPayState + /** + * This state denotes that we're in the process of refreshing the existing customer request, + * in case the authorization has expired. This is typically an in-between between [Authorizing] + * and [PollingTransactionStatus]. + */ + object Refreshing : CashAppPayState + /** * This state denotes that we're actively polling for an authorization update. This state will * typically transition to either [Approved] or [Declined]. 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 bfb0b5c..7b1c9ae 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 @@ -28,6 +28,7 @@ import app.cash.paykit.core.CashAppPayState.Declined import app.cash.paykit.core.CashAppPayState.NotStarted import app.cash.paykit.core.CashAppPayState.PollingTransactionStatus import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize +import app.cash.paykit.core.CashAppPayState.Refreshing import app.cash.paykit.core.CashAppPayState.RetrievingExistingCustomerRequest import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.NetworkManager @@ -46,6 +47,7 @@ import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.Grant import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction +import app.cash.paykit.core.network.MoshiProvider import app.cash.paykit.core.utils.Clock import app.cash.paykit.core.utils.ClockRealImpl import app.cash.paykit.core.utils.UUIDManager @@ -53,7 +55,6 @@ import app.cash.paykit.core.utils.UUIDManagerRealImpl import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter -import java.util.* private const val APP_NAME = "paykitsdk-android" private const val PLATFORM = "android" @@ -66,7 +67,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( private val sdkEnvironment: String, private val payKitAnalytics: PayKitAnalytics, private val networkManager: NetworkManager, - private val moshi: Moshi = Moshi.Builder().build(), + private val moshi: Moshi = MoshiProvider.provideDefault(), private val uuidManager: UUIDManager = UUIDManagerRealImpl(), private val clock: Clock = ClockRealImpl(), ) : PayKitAnalyticsEventDispatcher { @@ -274,8 +275,8 @@ internal class PayKitAnalyticsEventDispatcherImpl( clientId, status = customerResponseData?.status, authMobileUrl = customerResponseData?.authFlowTriggers?.mobileUrl, - updatedAt = customerResponseData?.updatedAt?.toLongOrNull(), - createdAt = customerResponseData?.createdAt?.toLongOrNull(), + updatedAt = customerResponseData?.updatedAt?.toEpochMilliseconds(), + createdAt = customerResponseData?.createdAt?.toEpochMilliseconds(), originType = customerResponseData?.origin?.type, originId = customerResponseData?.origin?.id, requestChannel = CHANNEL_IN_APP, @@ -295,6 +296,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( return when (state) { is Approved -> "approved" Authorizing -> "redirect" + Refreshing -> "refreshing" CreatingCustomerRequest -> "create" Declined -> "declined" NotStarted -> "not_started" diff --git a/core/src/main/java/app/cash/paykit/core/android/LogTag.kt b/core/src/main/java/app/cash/paykit/core/android/LogTag.kt new file mode 100644 index 0000000..4fa7712 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/android/LogTag.kt @@ -0,0 +1,18 @@ +/* + * 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.android + +const val LOG_TAG = "CashAppPay" diff --git a/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt b/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt new file mode 100644 index 0000000..e1b162e --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt @@ -0,0 +1,34 @@ +/* + * 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.android + +import android.util.Log + +/** + * 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. + */ +fun Thread.safeStart(errorMessage: String?, onError: () -> Unit? = {}) { + try { + start() + } catch (e: IllegalThreadStateException) { + // This can happen if the thread is already started. + Log.e(LOG_TAG, errorMessage, e) + onError() + } catch (e: InterruptedException) { + Log.e(LOG_TAG, errorMessage, e) + onError() + } +} diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt index 9435ac0..19d8dd0 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt @@ -22,6 +22,7 @@ import android.util.Log import androidx.annotation.WorkerThread import app.cash.paykit.core.BuildConfig import app.cash.paykit.core.CashAppPay +import app.cash.paykit.core.CashAppPayFactory.TOKEN_REFRESH_WINDOW import app.cash.paykit.core.CashAppPayLifecycleObserver import app.cash.paykit.core.CashAppPayListener import app.cash.paykit.core.CashAppPayState @@ -33,12 +34,17 @@ import app.cash.paykit.core.CashAppPayState.Declined import app.cash.paykit.core.CashAppPayState.NotStarted import app.cash.paykit.core.CashAppPayState.PollingTransactionStatus import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize +import app.cash.paykit.core.CashAppPayState.Refreshing import app.cash.paykit.core.CashAppPayState.RetrievingExistingCustomerRequest import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.NetworkManager import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcher import app.cash.paykit.core.android.ApplicationContextHolder +import app.cash.paykit.core.android.LOG_TAG +import app.cash.paykit.core.android.safeStart import app.cash.paykit.core.exceptions.CashAppPayIntegrationException +import app.cash.paykit.core.exceptions.CashAppPayNetworkErrorType.CONNECTIVITY +import app.cash.paykit.core.exceptions.CashAppPayNetworkException import app.cash.paykit.core.models.common.NetworkResult.Failure import app.cash.paykit.core.models.common.NetworkResult.Success import app.cash.paykit.core.models.response.CustomerResponseData @@ -47,6 +53,9 @@ import app.cash.paykit.core.models.response.STATUS_PENDING import app.cash.paykit.core.models.response.STATUS_PROCESSING import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.utils.orElse +import kotlinx.datetime.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * @param clientId Client Identifier that should be provided by Cash PayKit integration. @@ -66,6 +75,9 @@ internal class CashAppCashAppPayImpl( private var customerResponseData: CustomerResponseData? = initialCustomerResponseData + private var checkForApprovalThread: Thread? = null + private var refreshUnauthorizedThread: Thread? = null + private var currentState: CashAppPayState = initialState set(value) { field = value @@ -77,6 +89,7 @@ internal class CashAppCashAppPayImpl( customerResponseData, ) Authorizing -> analyticsEventDispatcher.genericStateChanged(value, customerResponseData) + Refreshing -> analyticsEventDispatcher.genericStateChanged(value, customerResponseData) Declined -> analyticsEventDispatcher.genericStateChanged(value, customerResponseData) NotStarted -> analyticsEventDispatcher.genericStateChanged(value, customerResponseData) PollingTransactionStatus -> analyticsEventDispatcher.genericStateChanged(value, customerResponseData) @@ -136,6 +149,7 @@ internal class CashAppCashAppPayImpl( is Success -> { customerResponseData = networkResult.data.customerResponseData currentState = ReadyToAuthorize(networkResult.data.customerResponseData) + scheduleUnauthorizedCustomerRequestRefresh(networkResult.data.customerResponseData) } } } @@ -202,6 +216,7 @@ internal class CashAppCashAppPayImpl( } STATUS_PENDING -> { + scheduleUnauthorizedCustomerRequestRefresh(networkResult.data.customerResponseData) ReadyToAuthorize(customerResponseData!!) } @@ -236,9 +251,52 @@ internal class CashAppCashAppPayImpl( return } + if (customerData.isAuthTokenExpired()) { + logInfo("Auth token expired when attempting to authenticate, refreshing before proceeding.") + deferredAuthorizeCustomerRequest() + return + } + authorizeCustomerRequest(customerData) } + /** + * Deferred authorization of a customer request, when the auth token has expired. + */ + private fun deferredAuthorizeCustomerRequest() { + // Stop the thread that refreshes the customer request. + try { + refreshUnauthorizedThread?.interrupt() + } catch (e: Exception) { + logError("Error while interrupting previous thread. Exception: $e") + } + + currentState = Refreshing + + logInfo("Will refresh customer request before proceeding with authorization.") + Thread { + val networkResult = networkManager.retrieveUpdatedRequestData( + clientId, + customerResponseData!!.id, + ) + if (networkResult is Failure) { + logError("Failed to refresh expired auth token customer request.") + currentState = CashAppPayExceptionState(networkResult.exception) + return@Thread + } + logInfo("Refreshed customer request with SUCCESS") + customerResponseData = (networkResult as Success).data.customerResponseData + + if (currentState == Refreshing) { + authorizeCustomerRequest(customerResponseData!!) + } + }.safeStart("Error while attempting to run deferred authorization.", onError = { + if (currentState == Refreshing) { + currentState = CashAppPayExceptionState(CashAppPayNetworkException(CONNECTIVITY)) + } + }) + } + /** * Authorize a customer request with a previously created `customerData`. * This function will set this SDK instance internal state to the `customerData` provided here as a function parameter. @@ -265,13 +323,19 @@ internal class CashAppCashAppPayImpl( // Replace internal state. customerResponseData = customerData + if (customerData.isAuthTokenExpired()) { + logInfo("Auth token expired when attempting to authenticate, refreshing before proceeding.") + deferredAuthorizeCustomerRequest() + return + } + + currentState = Authorizing try { ApplicationContextHolder.applicationContext.startActivity(intent) } catch (activityNotFoundException: ActivityNotFoundException) { currentState = CashAppPayExceptionState(CashAppPayIntegrationException("Unable to open mobileUrl: ${customerData.authFlowTriggers?.mobileUrl}")) return } - currentState = Authorizing } /** @@ -286,10 +350,19 @@ internal class CashAppCashAppPayImpl( * Unregister any previously registered [CashAppPayListener] from PayKit updates. */ override fun unregisterFromStateUpdates() { + logInfo("Unregistering from state updates") callbackListener = null payKitLifecycleListener.unregister(this) analyticsEventDispatcher.eventListenerRemoved() analyticsEventDispatcher.shutdown() + + // Stop any polling operations that might be running. + try { + refreshUnauthorizedThread?.interrupt() + checkForApprovalThread?.interrupt() + } catch (e: Exception) { + logError("Error while interrupting threads. Exception: $e") + } } private fun enforceRegisteredStateUpdatesListener() { @@ -303,7 +376,7 @@ internal class CashAppCashAppPayImpl( } private fun poolTransactionStatus() { - Thread { + checkForApprovalThread = Thread { val networkResult = networkManager.retrieveUpdatedRequestData( clientId, customerResponseData!!.id, @@ -321,7 +394,12 @@ internal class CashAppCashAppPayImpl( // If status is pending, schedule to check again. if (customerResponseData?.status == STATUS_PENDING) { // TODO: Add backoff strategy for long polling. ( https://www.notion.so/cashappcash/Implement-Long-pooling-retry-logic-a9af47e2db9242faa5d64df2596fd78e ) - Thread.sleep(500) + try { + Thread.sleep(500) + } catch (e: InterruptedException) { + return@Thread + } + poolTransactionStatus() return@Thread } @@ -329,11 +407,83 @@ internal class CashAppCashAppPayImpl( // Unsuccessful transaction. setStateFinished(false) } - }.start() + } + checkForApprovalThread?.safeStart(errorMessage = "Could not start checkForApprovalThread.") + } + + private fun refreshUnauthorizedCustomerRequest(delay: Duration) { + // Before starting a new thread, cancel any previous one. + try { + refreshUnauthorizedThread?.interrupt() + } catch (e: Exception) { + logError("Error while interrupting previous thread. Exception: $e") + } + + refreshUnauthorizedThread = Thread { + try { + Thread.sleep(delay.inWholeMilliseconds) + } catch (e: InterruptedException) { + return@Thread + } + + // Stop refreshing if the request has expired. + val currentTime = Clock.System.now() + val hasExpired = customerResponseData?.expiresAt?.let { expiresAt -> currentTime > expiresAt } ?: false + if (hasExpired) { + logError("Customer request has expired. Stopping refresh.") + return@Thread + } + + if (currentState !is ReadyToAuthorize) { + // In this case, we don't want to retry since we're in a state that doesn't allow it. + logError("Not refreshing unauthorized customer request because state is not ReadyToAuthorize") + return@Thread + } + + val networkResult = networkManager.retrieveUpdatedRequestData( + clientId, + customerResponseData!!.id, + ) + if (networkResult is Failure) { + logError("Failed to refresh expiring auth token customer request.") + + // Retry refreshing unauthorized customer request. + refreshUnauthorizedCustomerRequest(delay) + return@Thread + } + logInfo("Refreshed customer request with SUCCESS") + customerResponseData = (networkResult as Success).data.customerResponseData + refreshUnauthorizedCustomerRequest(delay) + } + + refreshUnauthorizedThread?.safeStart("Could not start refreshUnauthorizedThread.", onError = { + refreshUnauthorizedCustomerRequest(delay) + }) + } + + /** + * Given a `customerResponseData` object, this function will schedule a refresh of the customer request + * so that the auth flow trigger is refreshed before it expires. + */ + private fun scheduleUnauthorizedCustomerRequestRefresh(customerResponseData: CustomerResponseData) { + if (customerResponseData.authFlowTriggers?.refreshesAt == null) { + logError("Unable to schedule unauthorized customer request refresh. RefreshesAt is null.") + return + } + + val ttlSeconds = customerResponseData.authFlowTriggers.refreshesAt.minus(customerResponseData.createdAt) + + val refreshDelay = ttlSeconds.inWholeSeconds.minus(TOKEN_REFRESH_WINDOW.inWholeSeconds) + logInfo("Scheduling unauthorized customer request refresh in $refreshDelay seconds.") + refreshUnauthorizedCustomerRequest(refreshDelay.seconds) } private fun logError(errorMessage: String) { - Log.e("PayKit", errorMessage) + Log.e(LOG_TAG, errorMessage) + } + + private fun logInfo(errorMessage: String) { + Log.i(LOG_TAG, errorMessage) } /** @@ -379,11 +529,11 @@ internal class CashAppCashAppPayImpl( */ override fun onApplicationForegrounded() { - logError("onApplicationForegrounded") + logInfo("onApplicationForegrounded") updateStateAndPoolForTransactionStatus() } override fun onApplicationBackgrounded() { - logError("onApplicationBackgrounded") + logInfo("onApplicationBackgrounded") } } diff --git a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt index 29c0215..fe497e4 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt @@ -31,6 +31,7 @@ import app.cash.paykit.core.models.request.CustomerRequestDataFactory import app.cash.paykit.core.models.response.ApiErrorResponse import app.cash.paykit.core.models.response.CustomerTopLevelResponse import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction +import app.cash.paykit.core.network.MoshiProvider import app.cash.paykit.core.network.RetryManager import app.cash.paykit.core.network.RetryManagerImpl import app.cash.paykit.core.network.RetryManagerOptions @@ -159,7 +160,7 @@ internal class NetworkManagerImpl( clientId: String, requestPayload: In?, ): NetworkResult { - val moshi: Moshi = Moshi.Builder().build() + val moshi: Moshi = MoshiProvider.provideDefault() val requestJsonAdapter: JsonAdapter = moshi.adapter() val jsonData: String = requestJsonAdapter.toJson(requestPayload) return executePlainNetworkRequest( @@ -197,7 +198,7 @@ internal class NetworkManagerImpl( requestBuilder.addHeader("Authorization", "Client $clientId") } - val moshi: Moshi = Moshi.Builder().build() + val moshi: Moshi = MoshiProvider.provideDefault() with(requestBuilder) { when (requestType) { @@ -222,7 +223,11 @@ internal class NetworkManagerImpl( // Wait until the next retry. if (retryManager.shouldRetry()) { - Thread.sleep(retryManager.timeUntilNextRetry().inWholeMilliseconds) + try { + Thread.sleep(retryManager.timeUntilNextRetry().inWholeMilliseconds) + } catch (e: InterruptedException) { + return NetworkResult.failure(CashAppPayConnectivityNetworkException(retryException)) + } } return@use } @@ -263,7 +268,11 @@ internal class NetworkManagerImpl( // Wait until the next retry. if (retryManager.shouldRetry()) { - Thread.sleep(retryManager.timeUntilNextRetry().inWholeMilliseconds) + try { + Thread.sleep(retryManager.timeUntilNextRetry().inWholeMilliseconds) + } catch (e: InterruptedException) { + return NetworkResult.failure(CashAppPayConnectivityNetworkException(retryException)) + } } retryException = e } 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 9bc09ed..04b73f0 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,6 +17,7 @@ package app.cash.paykit.core.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.datetime.Instant @JsonClass(generateAdapter = true) data class AuthFlowTriggers( @@ -27,5 +28,5 @@ data class AuthFlowTriggers( @Json(name = "qr_code_svg_url") val qrCodeSvgUrl: String, @Json(name = "refreshes_at") - val refreshesAt: String, + val refreshesAt: Instant, ) 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 2022230..c9aa8d8 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 @@ -18,6 +18,8 @@ package app.cash.paykit.core.models.response import app.cash.paykit.core.models.common.Action import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.datetime.Clock.System +import kotlinx.datetime.Instant const val STATUS_PENDING = "PENDING" const val STATUS_PROCESSING = "PROCESSING" @@ -41,15 +43,21 @@ data class CustomerResponseData( @Json(name = "status") val status: String, @Json(name = "updated_at") - val updatedAt: String, + val updatedAt: Instant, @Json(name = "created_at") - val createdAt: String, + val createdAt: Instant, @Json(name = "expires_at") - val expiresAt: String, + val expiresAt: Instant, @Json(name = "customer_profile") val customerProfile: CustomerProfile?, @Json(name = "grants") val grants: List?, @Json(name = "reference_id") val referenceId: String?, -) +) { + fun isAuthTokenExpired(): Boolean { + val now = System.now() + val isExpired = now > (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 new file mode 100644 index 0000000..f147f8f --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/network/MoshiProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.network + +import app.cash.paykit.core.network.adapters.InstantAdapter +import com.squareup.moshi.Moshi +import kotlinx.datetime.Instant + +internal object MoshiProvider { + fun provideDefault(): Moshi { + return Moshi.Builder().add(Instant::class.java, InstantAdapter()).build() + } +} 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 new file mode 100644 index 0000000..265d3cd --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/network/adapters/InstantAdapter.kt @@ -0,0 +1,42 @@ +/* + * 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.network.adapters + +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 + +internal class InstantAdapter : JsonAdapter() { + + override fun fromJson(reader: JsonReader): Instant? { + if (reader.peek() == NULL) { + return reader.nextNull() + } + val timeRFC3339 = reader.nextString() + return timeRFC3339.toInstant() + } + + override fun toJson(writer: JsonWriter, value: Instant?) { + if (value == null) { + writer.nullValue() + } else { + writer.value(value.toString()) + } + } +} diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index e8801cd..4341742 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -155,6 +155,7 @@ class CashAppPayStateTests { payKit.registerForStateUpdates(listener) val customerTopLevelResponse: NetworkResult.Success = mockk() every { customerTopLevelResponse.data.customerResponseData.status } returns STATUS_PENDING + every { customerTopLevelResponse.data.customerResponseData.authFlowTriggers } returns null every { networkManager.retrieveUpdatedRequestData( any(), From 1338f8d5067e19a68d0cbe09681faa99c204859b Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 5 Jul 2023 10:38:15 -0700 Subject: [PATCH 09/26] Abstract thread managment away from main class (#113) --- .../paykit/core/impl/CashAppCashAppPayImpl.kt | 58 +++++++------------ .../paykit/core/utils/SingleThreadManager.kt | 34 +++++++++++ .../core/utils/SingleThreadManagerImpl.kt | 47 +++++++++++++++ 3 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 core/src/main/java/app/cash/paykit/core/utils/SingleThreadManager.kt create mode 100644 core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt index 19d8dd0..b39902e 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt @@ -52,6 +52,11 @@ import app.cash.paykit.core.models.response.STATUS_APPROVED import app.cash.paykit.core.models.response.STATUS_PENDING import app.cash.paykit.core.models.response.STATUS_PROCESSING import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction +import app.cash.paykit.core.utils.SingleThreadManager +import app.cash.paykit.core.utils.SingleThreadManagerImpl +import app.cash.paykit.core.utils.ThreadPurpose.CHECK_APPROVAL_STATUS +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 kotlinx.datetime.Clock import kotlin.time.Duration @@ -67,6 +72,7 @@ internal class CashAppCashAppPayImpl( private val analyticsEventDispatcher: PayKitAnalyticsEventDispatcher, private val payKitLifecycleListener: CashAppPayLifecycleObserver, private val useSandboxEnvironment: Boolean = false, + private val singleThreadManager: SingleThreadManager = SingleThreadManagerImpl(), initialState: CashAppPayState = NotStarted, initialCustomerResponseData: CustomerResponseData? = null, ) : CashAppPay, CashAppPayLifecycleListener { @@ -75,9 +81,6 @@ internal class CashAppCashAppPayImpl( private var customerResponseData: CustomerResponseData? = initialCustomerResponseData - private var checkForApprovalThread: Thread? = null - private var refreshUnauthorizedThread: Thread? = null - private var currentState: CashAppPayState = initialState set(value) { field = value @@ -265,16 +268,12 @@ internal class CashAppCashAppPayImpl( */ private fun deferredAuthorizeCustomerRequest() { // Stop the thread that refreshes the customer request. - try { - refreshUnauthorizedThread?.interrupt() - } catch (e: Exception) { - logError("Error while interrupting previous thread. Exception: $e") - } + singleThreadManager.interruptThread(REFRESH_AUTH_TOKEN) currentState = Refreshing logInfo("Will refresh customer request before proceeding with authorization.") - Thread { + singleThreadManager.createThread(DEFERRED_REFRESH) { val networkResult = networkManager.retrieveUpdatedRequestData( clientId, customerResponseData!!.id, @@ -282,7 +281,7 @@ internal class CashAppCashAppPayImpl( if (networkResult is Failure) { logError("Failed to refresh expired auth token customer request.") currentState = CashAppPayExceptionState(networkResult.exception) - return@Thread + return@createThread } logInfo("Refreshed customer request with SUCCESS") customerResponseData = (networkResult as Success).data.customerResponseData @@ -357,12 +356,7 @@ internal class CashAppCashAppPayImpl( analyticsEventDispatcher.shutdown() // Stop any polling operations that might be running. - try { - refreshUnauthorizedThread?.interrupt() - checkForApprovalThread?.interrupt() - } catch (e: Exception) { - logError("Error while interrupting threads. Exception: $e") - } + singleThreadManager.interruptAllThreads() } private fun enforceRegisteredStateUpdatesListener() { @@ -376,14 +370,14 @@ internal class CashAppCashAppPayImpl( } private fun poolTransactionStatus() { - checkForApprovalThread = Thread { + singleThreadManager.createThread(CHECK_APPROVAL_STATUS) { val networkResult = networkManager.retrieveUpdatedRequestData( clientId, customerResponseData!!.id, ) if (networkResult is Failure) { currentState = CashAppPayExceptionState(networkResult.exception) - return@Thread + return@createThread } customerResponseData = (networkResult as Success).data.customerResponseData @@ -397,33 +391,25 @@ internal class CashAppCashAppPayImpl( try { Thread.sleep(500) } catch (e: InterruptedException) { - return@Thread + return@createThread } poolTransactionStatus() - return@Thread + return@createThread } // Unsuccessful transaction. setStateFinished(false) } - } - checkForApprovalThread?.safeStart(errorMessage = "Could not start checkForApprovalThread.") + }.safeStart(errorMessage = "Could not start checkForApprovalThread.") } private fun refreshUnauthorizedCustomerRequest(delay: Duration) { - // Before starting a new thread, cancel any previous one. - try { - refreshUnauthorizedThread?.interrupt() - } catch (e: Exception) { - logError("Error while interrupting previous thread. Exception: $e") - } - - refreshUnauthorizedThread = Thread { + singleThreadManager.createThread(REFRESH_AUTH_TOKEN) { try { Thread.sleep(delay.inWholeMilliseconds) } catch (e: InterruptedException) { - return@Thread + return@createThread } // Stop refreshing if the request has expired. @@ -431,13 +417,13 @@ internal class CashAppCashAppPayImpl( val hasExpired = customerResponseData?.expiresAt?.let { expiresAt -> currentTime > expiresAt } ?: false if (hasExpired) { logError("Customer request has expired. Stopping refresh.") - return@Thread + return@createThread } if (currentState !is ReadyToAuthorize) { // In this case, we don't want to retry since we're in a state that doesn't allow it. logError("Not refreshing unauthorized customer request because state is not ReadyToAuthorize") - return@Thread + return@createThread } val networkResult = networkManager.retrieveUpdatedRequestData( @@ -449,14 +435,12 @@ internal class CashAppCashAppPayImpl( // Retry refreshing unauthorized customer request. refreshUnauthorizedCustomerRequest(delay) - return@Thread + return@createThread } logInfo("Refreshed customer request with SUCCESS") customerResponseData = (networkResult as Success).data.customerResponseData refreshUnauthorizedCustomerRequest(delay) - } - - refreshUnauthorizedThread?.safeStart("Could not start refreshUnauthorizedThread.", onError = { + }.safeStart("Could not start refreshUnauthorizedThread.", onError = { refreshUnauthorizedCustomerRequest(delay) }) } diff --git a/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManager.kt b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManager.kt new file mode 100644 index 0000000..c176c2c --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManager.kt @@ -0,0 +1,34 @@ +/* + * 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.utils + +enum class ThreadPurpose { + REFRESH_AUTH_TOKEN, + CHECK_APPROVAL_STATUS, + DEFERRED_REFRESH, +} + +/** + * A manager class that is responsible for creating and managing threads, and guarantee that + * each [ThreadPurpose] has only one thread at any given time. + */ +internal interface SingleThreadManager { + fun createThread(purpose: ThreadPurpose, runnable: Runnable): Thread + + fun interruptThread(purpose: ThreadPurpose) + + fun interruptAllThreads() +} diff --git a/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt new file mode 100644 index 0000000..56d5870 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt @@ -0,0 +1,47 @@ +/* + * 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.utils + +import android.util.Log +import app.cash.paykit.core.android.LOG_TAG + +internal class SingleThreadManagerImpl : SingleThreadManager { + + private val threads: MutableMap = mutableMapOf() + + override fun createThread(purpose: ThreadPurpose, runnable: Runnable): Thread { + // Before creating a new thread of a given type, make sure the last one is interrupted. + interruptThread(purpose) + + val thread = Thread(runnable, purpose.name) + threads[purpose] = thread + return thread + } + + override fun interruptThread(purpose: ThreadPurpose) { + try { + threads[purpose]?.interrupt() + } catch (e: Exception) { + Log.e(LOG_TAG, "Failed to interrupt thread: ${purpose.name}", e) + } finally { + threads[purpose] = null + } + } + + override fun interruptAllThreads() { + threads.keys.forEach { interruptThread(it) } + } +} From 1cb9081836ce6de82e885aa83e0eb65fedf9e569 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Thu, 6 Jul 2023 17:14:36 -0700 Subject: [PATCH 10/26] Redact PII for logs and metrics (#112) * Redact PII for logs and metrics * Add UTs for PiiString * Swap redact with unredacted defaults * Redact more fields; move redaction step to encoding --- .../PayKitAnalyticsEventDispatcherImpl.kt | 7 ++- .../AnalyticsCustomerRequestPayload.kt | 13 ++-- .../cash/paykit/core/models/pii/PiiContent.kt | 22 +++++++ .../cash/paykit/core/models/pii/PiiString.kt | 27 ++++++++ .../models/request/CustomerRequestData.kt | 3 +- .../request/CustomerRequestDataFactory.kt | 3 +- .../core/models/response/CustomerProfile.kt | 3 +- .../models/response/CustomerResponseData.kt | 3 +- .../cash/paykit/core/network/MoshiProvider.kt | 16 ++++- .../adapters/PiiStringClearTextAdapter.kt | 44 +++++++++++++ .../adapters/PiiStringRedactAdapter.kt | 44 +++++++++++++ .../app/cash/paykit/core/PiiStringTests.kt | 63 +++++++++++++++++++ 12 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/app/cash/paykit/core/models/pii/PiiContent.kt create mode 100644 core/src/main/java/app/cash/paykit/core/models/pii/PiiString.kt create mode 100644 core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringClearTextAdapter.kt create mode 100644 core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringRedactAdapter.kt create mode 100644 core/src/test/java/app/cash/paykit/core/PiiStringTests.kt 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 7b1c9ae..6e48be9 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 @@ -42,6 +42,7 @@ import app.cash.paykit.core.models.analytics.payloads.AnalyticsInitializationPay import app.cash.paykit.core.models.common.Action import app.cash.paykit.core.models.common.NetworkResult.Failure import app.cash.paykit.core.models.common.NetworkResult.Success +import app.cash.paykit.core.models.pii.PiiString import app.cash.paykit.core.models.request.CustomerRequestDataFactory.CHANNEL_IN_APP import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.Grant @@ -67,7 +68,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( private val sdkEnvironment: String, private val payKitAnalytics: PayKitAnalytics, private val networkManager: NetworkManager, - private val moshi: Moshi = MoshiProvider.provideDefault(), + private val moshi: Moshi = MoshiProvider.provideDefault(redactPii = true), private val uuidManager: UUIDManager = UUIDManagerRealImpl(), private val clock: Clock = ClockRealImpl(), ) : PayKitAnalyticsEventDispatcher { @@ -233,8 +234,8 @@ internal class PayKitAnalyticsEventDispatcherImpl( action = stateToAnalyticsAction(actionType), createActions = apiActionsAsJson, createChannel = CHANNEL_IN_APP, - createRedirectUrl = redirectUri, - createReferenceId = possibleReferenceId, + createRedirectUrl = redirectUri?.let { PiiString(redirectUri) }, + createReferenceId = possibleReferenceId?.let { PiiString(possibleReferenceId) }, environment = sdkEnvironment, ) } diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt index c9be821..b9b80bd 100644 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt +++ b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt @@ -15,6 +15,7 @@ */ package app.cash.paykit.core.models.analytics.payloads +import app.cash.paykit.core.models.pii.PiiString import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -60,11 +61,11 @@ data class AnalyticsCustomerRequestPayload( // The redirect URL when creating a Customer Request. @Json(name = "mobile_cap_pk_customer_request_create_redirect_url") - val createRedirectUrl: String? = null, + val createRedirectUrl: PiiString? = null, // The reference ID when creating a Customer Request. @Json(name = "mobile_cap_pk_customer_request_create_reference_id") - val createReferenceId: String? = null, + val createReferenceId: PiiString? = null, // A string built from the metadata when creating a Customer Request. @Json(name = "mobile_cap_pk_customer_request_create_metadata") @@ -96,7 +97,7 @@ data class AnalyticsCustomerRequestPayload( // The redirect URL of the Customer Request. @Json(name = "mobile_cap_pk_customer_request_redirect_url") - val redirectUrl: String? = null, + val redirectUrl: PiiString? = null, // The created at timestamp of the Customer Request. @Json(name = "mobile_cap_pk_customer_request_created_at") @@ -120,7 +121,7 @@ data class AnalyticsCustomerRequestPayload( // The reference ID of the Customer Request. @Json(name = "mobile_cap_pk_customer_request_reference_id") - val referenceId: String? = null, + val referenceId: PiiString? = null, // The name of the Requester Profile in the Customer Request. @Json(name = "mobile_cap_pk_customer_request_requester_name") @@ -132,7 +133,7 @@ data class AnalyticsCustomerRequestPayload( // The Cashtag of the Customer Profile in the Customer Request. @Json(name = "mobile_cap_pk_customer_request_customer_cashtag") - val customerCashTag: String? = null, + val customerCashTag: PiiString? = null, // A string built from the metadata in the Customer Request. @Json(name = "mobile_cap_pk_customer_request_metadata") @@ -148,7 +149,7 @@ data class AnalyticsCustomerRequestPayload( // The reference ID when updating a Customer Request. @Json(name = "mobile_cap_pk_customer_request_update_reference_id") - val updateReferenceId: String? = null, + val updateReferenceId: PiiString? = null, // The redirect URL when creating a Customer Request. @Json(name = "mobile_cap_pk_customer_request_update_metadata") diff --git a/core/src/main/java/app/cash/paykit/core/models/pii/PiiContent.kt b/core/src/main/java/app/cash/paykit/core/models/pii/PiiContent.kt new file mode 100644 index 0000000..95c6ade --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/models/pii/PiiContent.kt @@ -0,0 +1,22 @@ +/* + * 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.pii + +/** + * This is a marker interface for PII content (Personal Identifiable Information). It is meant to signal to the developer that a object + * of this class contains PII and should be treated as such. + */ +interface PiiContent diff --git a/core/src/main/java/app/cash/paykit/core/models/pii/PiiString.kt b/core/src/main/java/app/cash/paykit/core/models/pii/PiiString.kt new file mode 100644 index 0000000..8cb1706 --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/models/pii/PiiString.kt @@ -0,0 +1,27 @@ +/* + * 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.pii + +/** + * A string that has been classified as Personal Identifiable Information (PII). + * + */ +class PiiString(private var value: String) : PiiContent { + + override fun toString(): String { + return value + } +} diff --git a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt index 207da5f..b286089 100644 --- a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt +++ b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt @@ -16,6 +16,7 @@ package app.cash.paykit.core.models.request 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 @@ -26,5 +27,5 @@ data class CustomerRequestData( @Json(name = "channel") val channel: String?, @Json(name = "redirect_url") - val redirectUri: String?, + val redirectUri: PiiString?, ) 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 1c9cafe..b202753 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 @@ -16,6 +16,7 @@ package app.cash.paykit.core.models.request 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.OneTimeAction @@ -53,7 +54,7 @@ object CustomerRequestDataFactory { CustomerRequestData( actions = actions, channel = CHANNEL_IN_APP, - redirectUri = redirectUri, + redirectUri = redirectUri?.let { PiiString(redirectUri) }, ) } } diff --git a/core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt b/core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt index b75fdc6..ce9ef02 100644 --- a/core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt +++ b/core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt @@ -15,6 +15,7 @@ */ package app.cash.paykit.core.models.response +import app.cash.paykit.core.models.pii.PiiString import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -23,5 +24,5 @@ data class CustomerProfile( @Json(name = "id") val id: String, @Json(name = "cashtag") - val cashTag: String, + val cashTag: PiiString, ) 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 c9aa8d8..d8dd86e 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 @@ -16,6 +16,7 @@ package app.cash.paykit.core.models.response 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 @@ -53,7 +54,7 @@ data class CustomerResponseData( @Json(name = "grants") val grants: List?, @Json(name = "reference_id") - val referenceId: String?, + val referenceId: PiiString?, ) { fun isAuthTokenExpired(): Boolean { val now = System.now() 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 f147f8f..539466c 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 @@ -15,12 +15,24 @@ */ package app.cash.paykit.core.network +import app.cash.paykit.core.models.pii.PiiString 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 internal object MoshiProvider { - fun provideDefault(): Moshi { - return Moshi.Builder().add(Instant::class.java, InstantAdapter()).build() + fun provideDefault(redactPii: Boolean = false): Moshi { + val builder = Moshi.Builder() + .add(Instant::class.java, InstantAdapter()) + + if (redactPii) { + builder.add(PiiString::class.java, PiiStringRedactAdapter()) + } else { + builder.add(PiiString::class.java, PiiStringClearTextAdapter()) + } + + return builder.build() } } diff --git a/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringClearTextAdapter.kt b/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringClearTextAdapter.kt new file mode 100644 index 0000000..f30fffe --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringClearTextAdapter.kt @@ -0,0 +1,44 @@ +/* + * 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.network.adapters + +import app.cash.paykit.core.models.pii.PiiString +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonReader.Token.NULL +import com.squareup.moshi.JsonWriter + +/** + * This adapter will NOT redact the value of a [PiiString] when serializing to JSON. + */ +internal class PiiStringClearTextAdapter : JsonAdapter() { + + override fun fromJson(reader: JsonReader): PiiString? { + if (reader.peek() == NULL) { + return reader.nextNull() + } + val plainString = reader.nextString() + return PiiString(plainString) + } + + override fun toJson(writer: JsonWriter, value: PiiString?) { + if (value == null) { + writer.nullValue() + } else { + writer.value(value.toString()) + } + } +} diff --git a/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringRedactAdapter.kt b/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringRedactAdapter.kt new file mode 100644 index 0000000..e81677b --- /dev/null +++ b/core/src/main/java/app/cash/paykit/core/network/adapters/PiiStringRedactAdapter.kt @@ -0,0 +1,44 @@ +/* + * 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.network.adapters + +import app.cash.paykit.core.models.pii.PiiString +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonReader.Token.NULL +import com.squareup.moshi.JsonWriter + +/** + * This adapter will redact the value of a [PiiString] when serializing to JSON. + */ +internal class PiiStringRedactAdapter : JsonAdapter() { + + override fun fromJson(reader: JsonReader): PiiString? { + if (reader.peek() == NULL) { + return reader.nextNull() + } + val plainString = reader.nextString() + return PiiString(plainString) + } + + override fun toJson(writer: JsonWriter, value: PiiString?) { + if (value == null) { + writer.nullValue() + } else { + writer.value("FILTERED") + } + } +} diff --git a/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt b/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt new file mode 100644 index 0000000..efcda33 --- /dev/null +++ b/core/src/test/java/app/cash/paykit/core/PiiStringTests.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import app.cash.paykit.core.models.pii.PiiString +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.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 + +class PiiStringTests { + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `test Redact Adapter will redact contents of PiiString`() { + val piiString = PiiString("1234567890") + val moshi = Moshi.Builder() + .add(Instant::class.java, InstantAdapter()) + .add(PiiString::class.java, PiiStringRedactAdapter()) + .build() + + val serialized: JsonAdapter = moshi.adapter() + assertThat(serialized.toJson(piiString)).isEqualTo("\"FILTERED\"") + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun `test Clear Text PiiString Adapter will NOT redact contents of PiiString`() { + val piiString = PiiString("1234567890") + val moshi = Moshi.Builder() + .add(Instant::class.java, InstantAdapter()) + .add(PiiString::class.java, PiiStringClearTextAdapter()) + .build() + + val serialized: JsonAdapter = moshi.adapter() + assertThat(serialized.toJson(piiString)).isEqualTo("\"$piiString\"") + } + + @Test + fun `test PiiString can be obtained as plain text`() { + val value = "1234567890" + val piiString = PiiString(value) + assertThat(piiString.toString()).isEqualTo(value) + } +} From 8a9462431dcf3f46ff75c67e122a281427b52ff0 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 10 Jul 2023 15:58:56 -0700 Subject: [PATCH 11/26] Simplify internal class names (#115) --- core/src/main/java/app/cash/paykit/core/CashAppPay.kt | 6 +++--- .../analytics/PayKitAnalyticsEventDispatcherImpl.kt | 4 ++-- ...rkException.kt => CashAppPayApiNetworkException.kt} | 2 +- .../{CashAppCashAppPayImpl.kt => CashAppPayImpl.kt} | 2 +- .../app/cash/paykit/core/impl/NetworkManagerImpl.kt | 4 ++-- .../app/cash/paykit/core/CashAppPayAuthorizeTests.kt | 4 ++-- .../app/cash/paykit/core/CashAppPayExceptionsTests.kt | 4 ++-- .../java/app/cash/paykit/core/CashAppPayStateTests.kt | 4 ++-- .../java/app/cash/paykit/core/NetworkErrorTests.kt | 10 +++++----- .../java/app/cash/paykit/core/NetworkRetryTests.kt | 4 ++-- .../cash/paykit/core/CashAppPayProdExceptionsTests.kt | 4 ++-- 11 files changed, 24 insertions(+), 24 deletions(-) rename core/src/main/java/app/cash/paykit/core/exceptions/{CashAppCashAppPayApiNetworkException.kt => CashAppPayApiNetworkException.kt} (94%) rename core/src/main/java/app/cash/paykit/core/impl/{CashAppCashAppPayImpl.kt => CashAppPayImpl.kt} (99%) diff --git a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt index f6d28d9..d53a546 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt @@ -24,7 +24,7 @@ import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcher import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcherImpl import app.cash.paykit.core.android.ApplicationContextHolder import app.cash.paykit.core.exceptions.CashAppPayIntegrationException -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.impl.CashAppPayLifecycleObserverImpl import app.cash.paykit.core.impl.NetworkManagerImpl import app.cash.paykit.core.models.response.CustomerResponseData @@ -185,7 +185,7 @@ object CashAppPayFactory { buildPayKitAnalyticsEventDispatcher(clientId, networkManager, analytics, ANALYTICS_PROD_ENVIRONMENT) networkManager.analyticsEventDispatcher = analyticsEventDispatcher - return CashAppCashAppPayImpl( + return CashAppPayImpl( clientId = clientId, networkManager = networkManager, analyticsEventDispatcher = analyticsEventDispatcher, @@ -212,7 +212,7 @@ object CashAppPayFactory { buildPayKitAnalyticsEventDispatcher(clientId, networkManager, analytics, ANALYTICS_SANDBOX_ENVIRONMENT) networkManager.analyticsEventDispatcher = analyticsEventDispatcher - return CashAppCashAppPayImpl( + return CashAppPayImpl( clientId = clientId, networkManager = networkManager, analyticsEventDispatcher = analyticsEventDispatcher, 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 6e48be9..777531e 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 @@ -33,7 +33,7 @@ import app.cash.paykit.core.CashAppPayState.RetrievingExistingCustomerRequest import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.NetworkManager import app.cash.paykit.core.analytics.AnalyticsEventStream2Event.Companion.ESEventType -import app.cash.paykit.core.exceptions.CashAppCashAppPayApiNetworkException +import app.cash.paykit.core.exceptions.CashAppPayApiNetworkException import app.cash.paykit.core.models.analytics.EventStream2Event import app.cash.paykit.core.models.analytics.payloads.AnalyticsBasePayload import app.cash.paykit.core.models.analytics.payloads.AnalyticsCustomerRequestPayload @@ -178,7 +178,7 @@ internal class PayKitAnalyticsEventDispatcherImpl( var eventPayload = eventFromCustomerResponseData(customerResponseData).copy(action = stateToAnalyticsAction(payKitExceptionState)) - eventPayload = if (payKitExceptionState.exception is CashAppCashAppPayApiNetworkException) { + eventPayload = if (payKitExceptionState.exception is CashAppPayApiNetworkException) { val apiError = payKitExceptionState.exception eventPayload.copy( errorCode = apiError.code, diff --git a/core/src/main/java/app/cash/paykit/core/exceptions/CashAppCashAppPayApiNetworkException.kt b/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayApiNetworkException.kt similarity index 94% rename from core/src/main/java/app/cash/paykit/core/exceptions/CashAppCashAppPayApiNetworkException.kt rename to core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayApiNetworkException.kt index 8f36957..cdf5158 100644 --- a/core/src/main/java/app/cash/paykit/core/exceptions/CashAppCashAppPayApiNetworkException.kt +++ b/core/src/main/java/app/cash/paykit/core/exceptions/CashAppPayApiNetworkException.kt @@ -20,7 +20,7 @@ import app.cash.paykit.core.exceptions.CashAppPayNetworkErrorType.API /** * This exception encapsulates all of the metadata provided by an API error. */ -data class CashAppCashAppPayApiNetworkException( +data class CashAppPayApiNetworkException( val category: String, val code: String, val detail: String?, diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt similarity index 99% rename from core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt rename to core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt index b39902e..f9119df 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayImpl.kt @@ -66,7 +66,7 @@ import kotlin.time.Duration.Companion.seconds * @param clientId Client Identifier that should be provided by Cash PayKit integration. * @param useSandboxEnvironment Specify what development environment should be used. */ -internal class CashAppCashAppPayImpl( +internal class CashAppPayImpl( private val clientId: String, private val networkManager: NetworkManager, private val analyticsEventDispatcher: PayKitAnalyticsEventDispatcher, diff --git a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt index fe497e4..578f75b 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt @@ -17,7 +17,7 @@ package app.cash.paykit.core.impl import app.cash.paykit.core.NetworkManager import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcher -import app.cash.paykit.core.exceptions.CashAppCashAppPayApiNetworkException +import app.cash.paykit.core.exceptions.CashAppPayApiNetworkException import app.cash.paykit.core.exceptions.CashAppPayConnectivityNetworkException import app.cash.paykit.core.impl.RequestType.GET import app.cash.paykit.core.impl.RequestType.PATCH @@ -249,7 +249,7 @@ internal class NetworkManagerImpl( is Success -> { val apiError = apiErrorResponse.data.apiErrors.first() - val apiException = CashAppCashAppPayApiNetworkException( + val apiException = CashAppPayApiNetworkException( apiError.category, apiError.code, apiError.detail, diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt index 082259b..3d34c71 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt @@ -17,7 +17,7 @@ package app.cash.paykit.core import app.cash.paykit.core.exceptions.CashAppPayIntegrationException import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.models.response.CustomerResponseData import io.mockk.every import io.mockk.mockk @@ -73,7 +73,7 @@ class CashAppPayAuthorizeTests { } private fun createPayKit() = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = mockk(), payKitLifecycleListener = mockk(relaxed = true), diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt index 420c9d8..bd32078 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt @@ -17,7 +17,7 @@ package app.cash.paykit.core import app.cash.paykit.core.exceptions.CashAppPayIntegrationException import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.models.common.NetworkResult import io.mockk.MockKAnnotations import io.mockk.every @@ -63,7 +63,7 @@ class CashAppPayExceptionsTests { } private fun createPayKit(useSandboxEnvironment: Boolean) = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = networkManager, payKitLifecycleListener = mockk(relaxed = true), diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index 4341742..a58985d 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -28,7 +28,7 @@ import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.android.ApplicationContextHolder import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.impl.CashAppPayLifecycleListener import app.cash.paykit.core.models.common.NetworkResult import app.cash.paykit.core.models.response.CustomerResponseData @@ -285,7 +285,7 @@ class CashAppPayStateTests { initialState: CashAppPayState = NotStarted, initialCustomerResponseData: CustomerResponseData? = null, ) = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = networkManager, payKitLifecycleListener = mockLifecycleListener, diff --git a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt index e808a7a..38b00f9 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt @@ -16,10 +16,10 @@ package app.cash.paykit.core import app.cash.paykit.core.CashAppPayState.CashAppPayExceptionState -import app.cash.paykit.core.exceptions.CashAppCashAppPayApiNetworkException +import app.cash.paykit.core.exceptions.CashAppPayApiNetworkException import app.cash.paykit.core.exceptions.CashAppPayConnectivityNetworkException import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.impl.NetworkManagerImpl import app.cash.paykit.core.network.RetryManagerOptions import com.google.common.truth.Truth.assertThat @@ -107,11 +107,11 @@ class NetworkErrorTests { // Verify that all the appropriate exception wrapping has occurred for a 400 error. assertThat(mockListener.state).isInstanceOf(CashAppPayExceptionState::class.java) assertThat((mockListener.state as CashAppPayExceptionState).exception).isInstanceOf( - CashAppCashAppPayApiNetworkException::class.java, + CashAppPayApiNetworkException::class.java, ) // Verify that all the API error details have been deserialized correctly. - val apiError = (mockListener.state as CashAppPayExceptionState).exception as CashAppCashAppPayApiNetworkException + val apiError = (mockListener.state as CashAppPayExceptionState).exception as CashAppPayApiNetworkException assertThat(apiError.code).isEqualTo("MISSING_REQUIRED_PARAMETER") assertThat(apiError.category).isEqualTo("INVALID_REQUEST_ERROR") assertThat(apiError.field_value).isEqualTo("request.action.amount") @@ -235,7 +235,7 @@ class NetworkErrorTests { } private fun createPayKit(networkManager: NetworkManager) = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = networkManager, payKitLifecycleListener = mockk(relaxed = true), diff --git a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt index b958a30..97a58aa 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt @@ -20,7 +20,7 @@ import app.cash.paykit.core.CashAppPayState.ReadyToAuthorize import app.cash.paykit.core.NetworkErrorTests.MockListener import app.cash.paykit.core.exceptions.CashAppPayConnectivityNetworkException import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import app.cash.paykit.core.impl.NetworkManagerImpl import app.cash.paykit.core.network.RetryManagerOptions import com.google.common.truth.Truth.assertThat @@ -188,7 +188,7 @@ class NetworkRetryTests { } private fun createPayKit(networkManager: NetworkManager) = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = networkManager, payKitLifecycleListener = mockk(relaxed = true), diff --git a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt index 6c126a8..28f911d 100644 --- a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt +++ b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt @@ -16,7 +16,7 @@ package app.cash.paykit.core import app.cash.paykit.core.fakes.FakeData -import app.cash.paykit.core.impl.CashAppCashAppPayImpl +import app.cash.paykit.core.impl.CashAppPayImpl import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.mockk @@ -42,7 +42,7 @@ class CashAppPayProdExceptionsTests { } private fun createPayKit(useSandboxEnvironment: Boolean) = - CashAppCashAppPayImpl( + CashAppPayImpl( clientId = FakeData.CLIENT_ID, networkManager = networkManager, payKitLifecycleListener = mockk(relaxed = true), From 01b9db5dc13dc8c7c384a5a9fe2c2fbfd1dbcb7e Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 14 Jul 2023 15:35:13 -0700 Subject: [PATCH 12/26] Modify CashAppPayButton to allow for dynamic styles (#116) * Modify CashAppPayButton to allow for dynamic styles * Update CHANGELOG * Update testing frameworks and link baseline --- CHANGELOG.md | 28 ++++++ build.gradle | 8 +- core/lint-baseline.xml | 87 ++++++++++++++++++- .../cash/paykit/core/ui/CashAppPayButton.kt | 42 ++------- .../app/cash/paykit/core/utils/ClockTests.kt | 2 +- 5 files changed, 126 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e8790..22d223c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# 2.3.0 + +This version contains change to the bundled Cash App Pay button. +Previously, `light` and `dark` variants of the button were made possible by using 2 different +views, respectively `CashAppPayButtonLight` an `CashAppPayButtonDark`. As of this version, the +there will only be a single `CashAppPayButton` view, which has been updated to support both variants. +To obtain different variants, developers should use the XML `style` attribute to specify the variant they want, as follows: + + +Light Variant: +```xml + +``` + +Dark Variant: +```xml + +``` + +This change makes it possible for developer to use the button in a more flexible way, such as using +a style that changes accordingly to the OS theme. + # 2.2.1 Here's what has changed on this release: diff --git a/build.gradle b/build.gradle index 29cbcec..3c39411 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,9 @@ buildscript { lifecycle_version = '2.5.1' mockk_version = '1.13.3' coroutines_test_version = '1.6.4' - robolectric_version = '4.10' + robolectric_version = '4.10.3' mockwebserver_version = '4.10.0' - google_truth_version = '1.1.3' + google_truth_version = '1.1.5' startup_version = '1.1.1' okhttp_version = '4.10.0' kotlinx_date_version = '0.4.0' @@ -52,11 +52,11 @@ subprojects { subproject -> } } -def NEXT_VERSION = "2.2.2-SNAPSHOT" +def NEXT_VERSION = "2.3.1-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.2.1' + version = '2.3.0-SNAPSHOT' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing { diff --git a/core/lint-baseline.xml b/core/lint-baseline.xml index 816810c..fb1b897 100644 --- a/core/lint-baseline.xml +++ b/core/lint-baseline.xml @@ -1,11 +1,92 @@ - + + message="The resource `R.drawable.cap_btn_background_dark` appears to be unused" + errorLine1="<ripple xmlns:android="http://schemas.android.com/apk/res/android"" + errorLine2="^"> + file="src/main/res/drawable/cap_btn_background_dark.xml" + line="2" + column="1"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/java/app/cash/paykit/core/ui/CashAppPayButton.kt b/core/src/main/java/app/cash/paykit/core/ui/CashAppPayButton.kt index 2b102b8..7df4317 100644 --- a/core/src/main/java/app/cash/paykit/core/ui/CashAppPayButton.kt +++ b/core/src/main/java/app/cash/paykit/core/ui/CashAppPayButton.kt @@ -18,52 +18,28 @@ package app.cash.paykit.core.ui import android.content.Context import android.util.AttributeSet import android.widget.ImageButton -import app.cash.paykit.core.R -abstract class CashAppPayButton(context: Context, attrs: AttributeSet, style: Int) : +/** + * Cash App Pay button. Should be used in conjunction with either `CAPButtonStyle.Light` or `CAPButtonStyle.Dark` styles. + * + * **Note**: Due to its unmanaged nature, the button is merely a stylized button, it's up to developers + * to trigger the correct action on button press, as well as manage any visibility states of the + * button accordingly. + */ +class CashAppPayButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, style: Int = 0) : ImageButton( context, attrs, 0, style, - ) - -/** - * Cash App Pay button to be used in light mode. Notice that the button itself is dark, as - * it is meant for contrast with a light background. - */ -class CashAppPayLightButton(context: Context, attrs: AttributeSet) : - CashAppPayButton( - context, - attrs, - R.style.CAPButtonStyle_Light, ) { - override fun setEnabled(enabled: Boolean) { - super.setEnabled(enabled) - alpha = if (enabled) { - 1f - } else { - .3f - } - } -} -/** - * Cash App Pay button to be used in dark mode. Notice that the button itself is light, as - * it is meant for contrast with a dark background. - */ -class CashAppPayDarkButton(context: Context, attrs: AttributeSet) : - CashAppPayButton( - context, - attrs, - R.style.CAPButtonStyle_Dark, - ) { override fun setEnabled(enabled: Boolean) { super.setEnabled(enabled) alpha = if (enabled) { 1f } else { - .4f + .3f } } } diff --git a/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt b/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt index f1d8d6d..b012779 100644 --- a/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt +++ b/core/src/test/java/app/cash/paykit/core/utils/ClockTests.kt @@ -24,7 +24,7 @@ class ClockTests { fun `currentTimeInMicroseconds should return current time in microseconds`() { val clock = ClockRealImpl() val currentTimeInMicroseconds = clock.currentTimeInMicroseconds() - assertThat(currentTimeInMicroseconds).isGreaterThan(0) + assertThat(currentTimeInMicroseconds).isGreaterThan(0L) // Microseconds of when the test was written. assertThat(currentTimeInMicroseconds).isAtLeast(1686318558468000) From fdd4c04cf236d2280b62cbdbfaa43dc6de848533 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Wed, 19 Jul 2023 13:34:50 -0700 Subject: [PATCH 13/26] Add PR template (#120) --- .github/PULL_REQUEST_TEMPLATE/codegood.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE/codegood.md diff --git a/.github/PULL_REQUEST_TEMPLATE/codegood.md b/.github/PULL_REQUEST_TEMPLATE/codegood.md new file mode 100644 index 0000000..04fd07b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/codegood.md @@ -0,0 +1,13 @@ +## Jira Ticket +[Jira Ticket]() + +## What are you trying to accomplish? + +## How did you accomplish this? + +## Steps to manually test this change: + + +## Visual: \ No newline at end of file From e23626791f91a8a4ff76fd3788cdeb953e6ead7a Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Thu, 20 Jul 2023 14:24:07 -0700 Subject: [PATCH 14/26] Logging abstraction (#119) * Create new module for logging * CashPayLogger implementation, and apply it to Analytics module * Add CAP logger to Core module * Create lint baseline file for logging module * Fix tests * Change Analytics implementation to log only Warn level or above * Add support to observe logging changes * Add some UT for logging module * Add UTs for CashAppLoggerImpl * Add some useful documentation to tests --- analytics-core/build.gradle | 2 + .../cash/paykit/analytics/AnalyticsLogger.kt | 28 +-- .../cash/paykit/analytics/PayKitAnalytics.kt | 37 +-- .../paykit/analytics/core/DeliveryHandler.kt | 4 +- .../paykit/analytics/core/DeliveryWorker.kt | 12 +- .../sqlite/AnalyticsSQLiteDataSource.kt | 21 +- .../cash/paykit/analytics/AnalyticsTest.kt | 3 + .../paykit/analytics/DeliveryWorkerTest.kt | 5 +- .../paykit/analytics/SQLiteDataSourceTest.kt | 218 ++---------------- .../java/app/cash/paykit/analytics/Utils.kt | 2 +- core/build.gradle | 1 + .../java/app/cash/paykit/core/CashAppPay.kt | 15 +- .../app/cash/paykit/core/android/LogTag.kt | 2 +- .../paykit/core/android/ThreadExtensions.kt | 8 +- .../cash/paykit/core/impl/CashAppPayImpl.kt | 66 +++--- .../core/utils/SingleThreadManagerImpl.kt | 11 +- .../paykit/core/CashAppPayAuthorizeTests.kt | 1 + .../paykit/core/CashAppPayExceptionsTests.kt | 1 + .../cash/paykit/core/CashAppPayStateTests.kt | 1 + .../app/cash/paykit/core/NetworkErrorTests.kt | 1 + .../app/cash/paykit/core/NetworkRetryTests.kt | 5 +- .../core/CashAppPayProdExceptionsTests.kt | 1 + logging/.gitignore | 1 + logging/build.gradle.kts | 59 +++++ logging/consumer-rules.pro | 0 logging/lint-baseline.xml | 4 + logging/proguard-rules.pro | 21 ++ logging/src/main/AndroidManifest.xml | 4 + .../cash/paykit/logging/CashAppLogEntry.kt | 23 ++ .../app/cash/paykit/logging/CashAppLogger.kt | 53 +++++ .../paykit/logging/CashAppLoggerHistory.kt | 37 +++ .../cash/paykit/logging/CashAppLoggerImpl.kt | 56 +++++ .../logging/CashAppLoggerHistoryTests.kt | 66 ++++++ .../paykit/logging/CashAppLoggerImplTests.kt | 98 ++++++++ settings.gradle | 1 + 35 files changed, 558 insertions(+), 310 deletions(-) create mode 100644 logging/.gitignore create mode 100644 logging/build.gradle.kts create mode 100644 logging/consumer-rules.pro create mode 100644 logging/lint-baseline.xml create mode 100644 logging/proguard-rules.pro create mode 100644 logging/src/main/AndroidManifest.xml create mode 100644 logging/src/main/java/app/cash/paykit/logging/CashAppLogEntry.kt create mode 100644 logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt create mode 100644 logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt create mode 100644 logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt create mode 100644 logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt create mode 100644 logging/src/test/java/app/cash/paykit/logging/CashAppLoggerImplTests.kt diff --git a/analytics-core/build.gradle b/analytics-core/build.gradle index 5a7a78b..2dc3c33 100644 --- a/analytics-core/build.gradle +++ b/analytics-core/build.gradle @@ -46,6 +46,8 @@ dependencies { testImplementation "junit:junit:$junit_version" testImplementation "io.mockk:mockk:$mockk_version" + implementation project(':logging') + // Robolectric environment. testImplementation "org.robolectric:robolectric:$robolectric_version" } diff --git a/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsLogger.kt b/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsLogger.kt index d1dca71..292a685 100644 --- a/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsLogger.kt +++ b/analytics-core/src/main/java/app/cash/paykit/analytics/AnalyticsLogger.kt @@ -16,43 +16,27 @@ package app.cash.paykit.analytics import android.util.Log +import app.cash.paykit.logging.CashAppLogger class AnalyticsLogger( private val options: AnalyticsOptions, + private val cashAppLogger: CashAppLogger, ) { fun v(tag: String, msg: String) { if (options.logLevel <= Log.VERBOSE) { - log(Log.VERBOSE, tag, msg) - } - } - - fun d(tag: String, msg: String) { - if (options.logLevel <= Log.DEBUG) { - log(Log.DEBUG, tag, msg) - } - } - - fun i(tag: String, msg: String) { - if (options.logLevel <= Log.INFO) { - log(Log.INFO, tag, msg) + cashAppLogger.logVerbose(tag, msg) } } fun w(tag: String, msg: String) { if (options.logLevel <= Log.WARN) { - log(Log.WARN, tag, msg) + cashAppLogger.logWarning(tag, msg) } } - fun e(tag: String, msg: String) { + fun e(tag: String, msg: String, throwable: Throwable? = null) { if (options.logLevel <= Log.ERROR) { - log(Log.ERROR, tag, msg) - } - } - - private fun log(priority: Int, tag: String, msg: String) { - if (!options.isLoggerDisabled) { - Log.println(priority, tag, msg) + cashAppLogger.logError(tag, msg, throwable) } } } diff --git a/analytics-core/src/main/java/app/cash/paykit/analytics/PayKitAnalytics.kt b/analytics-core/src/main/java/app/cash/paykit/analytics/PayKitAnalytics.kt index ae915b0..de3491c 100644 --- a/analytics-core/src/main/java/app/cash/paykit/analytics/PayKitAnalytics.kt +++ b/analytics-core/src/main/java/app/cash/paykit/analytics/PayKitAnalytics.kt @@ -22,6 +22,7 @@ import app.cash.paykit.analytics.core.DeliveryWorker import app.cash.paykit.analytics.persistence.EntriesDataSource import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSQLiteDataSource import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSqLiteHelper +import app.cash.paykit.logging.CashAppLogger import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService @@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean class PayKitAnalytics constructor( private val context: Context, private val options: AnalyticsOptions, + private val cashAppLogger: CashAppLogger, private val sqLiteHelper: AnalyticsSqLiteHelper = AnalyticsSqLiteHelper( context = context, options = options, @@ -41,11 +43,14 @@ class PayKitAnalytics constructor( val entriesDataSource: EntriesDataSource = AnalyticsSQLiteDataSource( sqLiteHelper = sqLiteHelper, options = options, + cashAppLogger = cashAppLogger, ), - private val logger: AnalyticsLogger = AnalyticsLogger(options = options), + private val logger: AnalyticsLogger = AnalyticsLogger(options = options, cashAppLogger = cashAppLogger), vararg initialDeliveryHandlers: DeliveryHandler, ) { - private val TAG = "PayKitAnalytics" + companion object { + private const val TAG = "PayKitAnalytics" + } /** Collection of FutureTasks that perform the delivery work. */ private var deliveryTasks = mutableListOf>() @@ -71,7 +76,7 @@ class PayKitAnalytics constructor( entriesDataSource.resetEntries() ensureExecutorIsUpAndRunning() ensureSchedulerIsUpAndRunning() - logger.i(TAG, "Initialization completed.") + logger.v(TAG, "Initialization completed.") } /** @@ -87,7 +92,7 @@ class PayKitAnalytics constructor( initializeScheduledExecutorService() } } ?: run { - logger.d(TAG, "Creating scheduler service.") + logger.v(TAG, "Creating scheduler service.") initializeScheduledExecutorService() } } @@ -105,7 +110,7 @@ class PayKitAnalytics constructor( executor = Executors.newSingleThreadExecutor() } } ?: run { - logger.d(TAG, "Creating executor service.") + logger.v(TAG, "Creating executor service.") executor = Executors.newSingleThreadExecutor() } } @@ -117,7 +122,7 @@ class PayKitAnalytics constructor( private fun initializeScheduledExecutorService() { shouldShutdown.set(false) scheduler = Executors.newSingleThreadScheduledExecutor().also { - logger.d( + logger.v( TAG, "Initializing scheduled executor service | delay:%ds, interval:%ds".format( Locale.US, @@ -143,7 +148,7 @@ class PayKitAnalytics constructor( while (itr.hasNext()) { itr.next().run { if (isCancelled || isDone) { - logger.d( + logger.v( TAG, "Removing task from queue: ${toString()} (canceled=$isCancelled, done=$isDone)", ) @@ -187,25 +192,25 @@ class PayKitAnalytics constructor( fun scheduleShutdown() { shouldShutdown.set(true) - logger.i(TAG, "Scheduled shutdown.") + logger.v(TAG, "Scheduled shutdown.") } private fun shutdown() { executor?.run { shutdown() - logger.i(TAG, "Executor service shutdown.") + logger.v(TAG, "Executor service shutdown.") } scheduler?.run { shutdown() - logger.i(TAG, "Scheduled executor service shutdown.") + logger.v(TAG, "Scheduled executor service shutdown.") } if (deliveryTasks.isNotEmpty()) { deliveryTasks.clear() - logger.i(TAG, "FutureTask list cleared.") + logger.v(TAG, "FutureTask list cleared.") } sqLiteHelper.close() - logger.i(TAG, "Shutdown completed.") + logger.v(TAG, "Shutdown completed.") } @Synchronized @@ -214,7 +219,7 @@ class PayKitAnalytics constructor( if (existingHandler == null) { handler.setDependencies(entriesDataSource, logger) deliveryHandlers.add(handler) - logger.i( + logger.v( TAG, "Registering %s as delivery handler for %s".format( Locale.US, @@ -237,7 +242,7 @@ class PayKitAnalytics constructor( @Synchronized fun unregisterDeliveryHandler(handler: DeliveryHandler) { deliveryHandlers.remove(handler) - logger.i( + logger.v( TAG, "Unregistering %s as delivery handler for %s".format( Locale.US, @@ -273,7 +278,7 @@ class PayKitAnalytics constructor( ensureExecutorIsUpAndRunning() val handler: DeliveryHandler? = getDeliveryHandler(type) return if (handler != null && handler.deliverableType.equals(type, ignoreCase = true)) { - logger.i(TAG, "Scheduling $type for delivery --- $content") + logger.v(TAG, "Scheduling $type for delivery --- $content") ScheduleDeliverableTask(type, content, metaData).also { executor!!.execute(it) } @@ -294,7 +299,7 @@ class PayKitAnalytics constructor( if (type != null && content != null) { val entryId: Long = entriesDataSource.insertEntry(type, content, metaData) if (entryId > 0) { - logger.d( + logger.v( TAG, String.format("%s scheduled for delivery. id: %d", type, entryId), ) diff --git a/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryHandler.kt b/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryHandler.kt index 2fb554e..b68b99c 100644 --- a/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryHandler.kt +++ b/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryHandler.kt @@ -32,7 +32,7 @@ abstract class DeliveryHandler { private val listener = object : DeliveryListener { override fun onSuccess(entries: List) { - logger?.d( + logger?.v( TAG, "successful delivery, deleting $deliverableType[" + entries.toCommaSeparatedListIds() + "]", ) @@ -40,7 +40,7 @@ abstract class DeliveryHandler { } override fun onError(entries: List) { - logger?.d( + logger?.v( TAG, "DELIVERY_FAILED for $deliverableType[" + entries.toCommaSeparatedListIds() + "]", ) diff --git a/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryWorker.kt b/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryWorker.kt index 8705b68..fe9396e 100644 --- a/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryWorker.kt +++ b/analytics-core/src/main/java/app/cash/paykit/analytics/core/DeliveryWorker.kt @@ -28,39 +28,39 @@ internal class DeliveryWorker( private val logger: AnalyticsLogger, ) : Callable { init { - logger.d(TAG, "DeliveryWorker initialized.") + logger.v(TAG, "DeliveryWorker initialized.") } @Throws(Exception::class) override fun call() { - logger.d(TAG, "Starting delivery [$this]") + logger.v(TAG, "Starting delivery [$this]") for (deliveryHandler in handlers) { val entryType = deliveryHandler.deliverableType val processId: String = dataSource.generateProcessId(entryType) var entries: List = dataSource.getEntriesForDelivery(processId, entryType) if (entries.isNotEmpty()) { - logger.d( + logger.v( TAG, "Processing %s[%d] | processId=%s".format(Locale.US, entries, entries.size, processId), ) } while (entries.isNotEmpty()) { - logger.d(TAG, "DELIVERY_IN_PROGRESS for ids[" + entries.toCommaSeparatedListIds() + "]") + logger.v(TAG, "DELIVERY_IN_PROGRESS for ids[" + entries.toCommaSeparatedListIds() + "]") dataSource.updateStatuses(entries, AnalyticEntry.STATE_DELIVERY_IN_PROGRESS) deliveryHandler.deliver(entries, deliveryHandler.deliveryListener) // get the next batch of events to send entries = dataSource.getEntriesForDelivery(processId, entryType) if (entries.isNotEmpty()) { - logger.d( + logger.v( TAG, "Processing %s[%d] | processId=%s".format(Locale.US, entries, entries.size, processId), ) } } } - logger.d(TAG, "Delivery finished. [$this]") + logger.v(TAG, "Delivery finished. [$this]") } companion object { diff --git a/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/sqlite/AnalyticsSQLiteDataSource.kt b/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/sqlite/AnalyticsSQLiteDataSource.kt index 2ed48e2..7f967e9 100644 --- a/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/sqlite/AnalyticsSQLiteDataSource.kt +++ b/analytics-core/src/main/java/app/cash/paykit/analytics/persistence/sqlite/AnalyticsSQLiteDataSource.kt @@ -17,15 +17,18 @@ package app.cash.paykit.analytics.persistence.sqlite import android.content.ContentValues import android.database.sqlite.SQLiteDatabase -import android.util.Log +import app.cash.paykit.analytics.AnalyticsLogger import app.cash.paykit.analytics.AnalyticsOptions import app.cash.paykit.analytics.persistence.AnalyticEntry import app.cash.paykit.analytics.persistence.EntriesDataSource import app.cash.paykit.analytics.persistence.toCommaSeparatedListIds +import app.cash.paykit.logging.CashAppLogger class AnalyticsSQLiteDataSource( private val sqLiteHelper: AnalyticsSqLiteHelper, options: AnalyticsOptions, + private val cashAppLogger: CashAppLogger, + private val analyticsLogger: AnalyticsLogger = AnalyticsLogger(options, cashAppLogger), ) : EntriesDataSource(options) { @Synchronized @@ -43,10 +46,10 @@ class AnalyticsSQLiteDataSource( values.put(COLUMN_VERSION, options.applicationVersionCode.toString()) insertId = sqLiteHelper.database.insert(TABLE_SYNC_ENTRIES, null, values) if (insertId < 0) { - Log.e(TAG, "Unable to insert record into the $TABLE_SYNC_ENTRIES, values: $content") + analyticsLogger.e(TAG, "Unable to insert record into the $TABLE_SYNC_ENTRIES, values: $content") } } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Exception when trying to insert record into the $TABLE_SYNC_ENTRIES, values: $content", e) } return insertId } @@ -58,7 +61,7 @@ class AnalyticsSQLiteDataSource( val whereClauseForDelete = "$COLUMN_ID IN (${entries.toCommaSeparatedListIds()})" database.delete(TABLE_SYNC_ENTRIES, whereClauseForDelete, null) } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Unable to delete entries", e) } } @@ -95,7 +98,7 @@ class AnalyticsSQLiteDataSource( } } } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Unable to mark entries for delivery", e) } } @@ -127,7 +130,7 @@ class AnalyticsSQLiteDataSource( } } } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Unable to get entries with process id $processId, entryType $entryType and $state state", e) } return entries } @@ -140,7 +143,7 @@ class AnalyticsSQLiteDataSource( "UPDATE $TABLE_SYNC_ENTRIES SET $COLUMN_STATE=$status WHERE id IN (" + entries.toCommaSeparatedListIds() + ");" database.execSQL(query) } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Unable to update statuses", e) } } @@ -152,12 +155,12 @@ class AnalyticsSQLiteDataSource( """UPDATE $TABLE_SYNC_ENTRIES SET $COLUMN_STATE=${AnalyticEntry.STATE_NEW}, $COLUMN_PROCESS_ID=NULL;""" database.execSQL(query) } catch (e: Exception) { - Log.e("", "", e) + analyticsLogger.e(TAG, "Unable to reset entries", e) } } companion object { - private const val TAG = "EntriesDataSource" + private const val TAG = "AnalyticsSQLiteDataSource" const val TABLE_SYNC_ENTRIES = "entries" const val COLUMN_ID = "id" const val COLUMN_TYPE = "type" diff --git a/analytics-core/src/test/java/app/cash/paykit/analytics/AnalyticsTest.kt b/analytics-core/src/test/java/app/cash/paykit/analytics/AnalyticsTest.kt index 2edfe45..d5d7597 100644 --- a/analytics-core/src/test/java/app/cash/paykit/analytics/AnalyticsTest.kt +++ b/analytics-core/src/test/java/app/cash/paykit/analytics/AnalyticsTest.kt @@ -23,6 +23,7 @@ import app.cash.paykit.analytics.core.DeliveryListener import app.cash.paykit.analytics.persistence.AnalyticEntry import app.cash.paykit.analytics.persistence.EntriesDataSource import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSqLiteHelper +import app.cash.paykit.logging.CashAppLogger import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -58,6 +59,7 @@ class AnalyticsTest { ) private val analyticsSqLiteHelper: AnalyticsSqLiteHelper = mockk(relaxed = true) + private val cashAppLogger: CashAppLogger = mockk(relaxed = true) private val entriesDataSource: EntriesDataSource = mockk(relaxed = true) private val app = RuntimeEnvironment.getApplication() @@ -265,6 +267,7 @@ class AnalyticsTest { context = app, options = testOptions, sqLiteHelper = analyticsSqLiteHelper, + cashAppLogger = cashAppLogger, entriesDataSource = entriesDataSource, ) } diff --git a/analytics-core/src/test/java/app/cash/paykit/analytics/DeliveryWorkerTest.kt b/analytics-core/src/test/java/app/cash/paykit/analytics/DeliveryWorkerTest.kt index 3ea5490..d92dd00 100644 --- a/analytics-core/src/test/java/app/cash/paykit/analytics/DeliveryWorkerTest.kt +++ b/analytics-core/src/test/java/app/cash/paykit/analytics/DeliveryWorkerTest.kt @@ -22,6 +22,7 @@ import app.cash.paykit.analytics.persistence.AnalyticEntry import app.cash.paykit.analytics.persistence.AnalyticEntry.Companion.STATE_DELIVERY_FAILED import app.cash.paykit.analytics.persistence.AnalyticEntry.Companion.STATE_DELIVERY_IN_PROGRESS import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSQLiteDataSource +import app.cash.paykit.logging.CashAppLogger import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -34,12 +35,14 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DeliveryWorkerTest { + private val cashAppLogger: CashAppLogger = mockk(relaxed = true) + @Test fun testNoDeliveryHandlers() { val dataSource: AnalyticsSQLiteDataSource = mockk(relaxed = true) val analyticsOptions: AnalyticsOptions = mockk(relaxed = true) val handlers = ArrayList() - val worker = DeliveryWorker(dataSource, handlers, AnalyticsLogger(analyticsOptions)) + val worker = DeliveryWorker(dataSource, handlers, AnalyticsLogger(analyticsOptions, cashAppLogger)) worker.call() verify(inverse = true) { dataSource.generateProcessId(any()) } diff --git a/analytics-core/src/test/java/app/cash/paykit/analytics/SQLiteDataSourceTest.kt b/analytics-core/src/test/java/app/cash/paykit/analytics/SQLiteDataSourceTest.kt index 0eb83ee..07f2d4e 100644 --- a/analytics-core/src/test/java/app/cash/paykit/analytics/SQLiteDataSourceTest.kt +++ b/analytics-core/src/test/java/app/cash/paykit/analytics/SQLiteDataSourceTest.kt @@ -20,6 +20,8 @@ import app.cash.paykit.analytics.Utils.insertSyncEntry import app.cash.paykit.analytics.persistence.AnalyticEntry import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSQLiteDataSource import app.cash.paykit.analytics.persistence.sqlite.AnalyticsSqLiteHelper +import app.cash.paykit.logging.CashAppLogger +import io.mockk.mockk import org.junit.After import org.junit.Assert import org.junit.Assert.assertEquals @@ -36,6 +38,7 @@ class SQLiteDataSourceTest { private lateinit var options: AnalyticsOptions private lateinit var helper: AnalyticsSqLiteHelper + private val cashAppLogger: CashAppLogger = mockk(relaxed = true) private val app = RuntimeEnvironment.getApplication() @Before @@ -58,7 +61,7 @@ class SQLiteDataSourceTest { @Test fun testInsertEntries() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) val entryId: Long = dataSource.insertEntry("TYPE_1", "load.testInsertEntry", "metadata.testInsertEntry") assertTrue(entryId > 0) @@ -79,7 +82,7 @@ class SQLiteDataSourceTest { @Test fun testDeleteEntries() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) val pkgId1: Long = dataSource.insertEntry("TYPE_1", "load.testInsertEntry.1", "metadata.testInsertEntry.1") val pkgId2: Long = @@ -97,94 +100,16 @@ class SQLiteDataSourceTest { @Test fun testGetEntriesByProcessIdAndState() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) // @formatter:off val p1 = insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") val p2 = insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") val p3 = insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p4 = insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p5: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p6: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p7: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p8: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p9: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p10: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) - val p11: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p12: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_FAILED, - "", - "", - "", - ) - val p13: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p14: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p15: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p16: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p17: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p18: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p19: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") + val p20 = insertSyncEntry(helper, "PROCESS_2", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") val p21 = insertSyncEntry(helper, "PROCESS_2", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p22 = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) - val p23 = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p24 = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_FAILED, - "", - "", - "", - ) // @formatter:on var entries = dataSource.getEntriesByProcessIdAndState( @@ -215,21 +140,12 @@ class SQLiteDataSourceTest { @Test fun testMarkEntriesForSynchronization() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) // @formatter:off // entries that are unassigned to sync process (2 types of entries, one entry for every possible state) val p1: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") val p2: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_DELIVERY_FAILED, "", "", "") - val p3: Long = insertSyncEntry( - helper, - null, - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) val p4: Long = insertSyncEntry( helper, null, @@ -239,27 +155,6 @@ class SQLiteDataSourceTest { "", "", ) - val p5: Long = insertSyncEntry(helper, null, "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p6: Long = - insertSyncEntry(helper, null, "TYPE_2", AnalyticEntry.STATE_DELIVERY_FAILED, "", "", "") - val p7: Long = insertSyncEntry( - helper, - null, - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p8: Long = insertSyncEntry( - helper, - null, - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) // entries assigned to PROCESS_1 sync process (2 types of entries, one entry for every possible state) val p9: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") @@ -272,15 +167,6 @@ class SQLiteDataSourceTest { "", "", ) - val p11: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) val p12: Long = insertSyncEntry( helper, "PROCESS_1", @@ -290,35 +176,6 @@ class SQLiteDataSourceTest { "", "", ) - val p13: Long = - insertSyncEntry(helper, "PROCESS_1", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p14: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_FAILED, - "", - "", - "", - ) - val p15: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p16: Long = insertSyncEntry( - helper, - "PROCESS_1", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) // entries assigned to PROCESS_2 sync process (2 types of entries, one entry for every possible state) val p17: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") @@ -331,53 +188,6 @@ class SQLiteDataSourceTest { "", "", ) - val p19: Long = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p20: Long = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_1", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) - val p21: Long = - insertSyncEntry(helper, "PROCESS_2", "TYPE_2", AnalyticEntry.STATE_NEW, "", "", "") - val p22: Long = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_FAILED, - "", - "", - "", - ) - val p23: Long = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_IN_PROGRESS, - "", - "", - "", - ) - val p24: Long = insertSyncEntry( - helper, - "PROCESS_2", - "TYPE_2", - AnalyticEntry.STATE_DELIVERY_PENDING, - "", - "", - "", - ) // @formatter:on dataSource.markEntriesForDelivery("PROCESS_1", "TYPE_1") @@ -403,7 +213,7 @@ class SQLiteDataSourceTest { @Test fun testUpdateStatuses() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) // @formatter:off val p1: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") val p2: Long = insertSyncEntry( @@ -453,10 +263,10 @@ class SQLiteDataSourceTest { @Test fun testResetEntries() { - val dataSource = AnalyticsSQLiteDataSource(helper, options) + val dataSource = AnalyticsSQLiteDataSource(helper, options, cashAppLogger) // @formatter:off - val p1: Long = insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") - val p2: Long = insertSyncEntry( + insertSyncEntry(helper, null, "TYPE_1", AnalyticEntry.STATE_NEW, "", "", "") + insertSyncEntry( helper, "PROCESS_1", "TYPE_1", @@ -465,7 +275,7 @@ class SQLiteDataSourceTest { "", "", ) - val p3: Long = insertSyncEntry( + insertSyncEntry( helper, "PROCESS_1", "TYPE_1", @@ -474,7 +284,7 @@ class SQLiteDataSourceTest { "", "", ) - val p4: Long = insertSyncEntry( + insertSyncEntry( helper, "PROCESS_1", "TYPE_1", diff --git a/analytics-core/src/test/java/app/cash/paykit/analytics/Utils.kt b/analytics-core/src/test/java/app/cash/paykit/analytics/Utils.kt index 5faebaa..df66965 100644 --- a/analytics-core/src/test/java/app/cash/paykit/analytics/Utils.kt +++ b/analytics-core/src/test/java/app/cash/paykit/analytics/Utils.kt @@ -26,7 +26,7 @@ import java.lang.reflect.Field internal object Utils { - fun getPrivateField(obj: Any, fieldName: String?): Any? { + fun getPrivateField(obj: Any, fieldName: String): Any? { try { val field: Field = obj.javaClass.getDeclaredField(fieldName) field.isAccessible = true diff --git a/core/build.gradle b/core/build.gradle index 04ccd74..59168c7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -79,6 +79,7 @@ dependencies { //noinspection GradleDependency implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation project(':logging') implementation project(':analytics-core') // TEST RELATED. diff --git a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt index d53a546..b2d0cdb 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt @@ -31,6 +31,8 @@ import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import app.cash.paykit.core.network.OkHttpProvider import app.cash.paykit.core.utils.UserAgentProvider +import app.cash.paykit.logging.CashAppLogger +import app.cash.paykit.logging.CashAppLoggerImpl import kotlin.time.Duration.Companion.seconds interface CashAppPay { @@ -135,7 +137,7 @@ object CashAppPayFactory { private val cashAppPayLifecycleObserver: CashAppPayLifecycleObserver = CashAppPayLifecycleObserverImpl() - private fun buildPayKitAnalytics(isSandbox: Boolean) = + private fun buildPayKitAnalytics(isSandbox: Boolean, cashAppLogger: CashAppLogger) = with(ApplicationContextHolder.applicationContext) { val info = packageManager.getPackageInfo(packageName, 0) @@ -156,11 +158,12 @@ object CashAppPayFactory { context = ApplicationContextHolder.applicationContext, options = AnalyticsOptions( delay = 10.seconds, - logLevel = Log.VERBOSE, + logLevel = Log.WARN, databaseName = dbName, isLoggerDisabled = !BuildConfig.DEBUG, applicationVersionCode = versionCode!!.toInt(), // casting as int gives us the "legacy" version code ), + cashAppLogger = cashAppLogger, ) } @@ -180,7 +183,7 @@ object CashAppPayFactory { userAgentValue = getUserAgentValue(), okHttpClient = defaultOkHttpClient, ) - val analytics = buildPayKitAnalytics(isSandbox = false) + val analytics = buildPayKitAnalytics(isSandbox = false, cashAppPayLogger) val analyticsEventDispatcher = buildPayKitAnalyticsEventDispatcher(clientId, networkManager, analytics, ANALYTICS_PROD_ENVIRONMENT) networkManager.analyticsEventDispatcher = analyticsEventDispatcher @@ -191,6 +194,7 @@ object CashAppPayFactory { analyticsEventDispatcher = analyticsEventDispatcher, payKitLifecycleListener = cashAppPayLifecycleObserver, useSandboxEnvironment = false, + logger = cashAppPayLogger, ) } @@ -207,7 +211,7 @@ object CashAppPayFactory { okHttpClient = defaultOkHttpClient, ) - val analytics = buildPayKitAnalytics(isSandbox = true) + val analytics = buildPayKitAnalytics(isSandbox = true, cashAppPayLogger) val analyticsEventDispatcher = buildPayKitAnalyticsEventDispatcher(clientId, networkManager, analytics, ANALYTICS_SANDBOX_ENVIRONMENT) networkManager.analyticsEventDispatcher = analyticsEventDispatcher @@ -218,6 +222,7 @@ object CashAppPayFactory { analyticsEventDispatcher = analyticsEventDispatcher, payKitLifecycleListener = cashAppPayLifecycleObserver, useSandboxEnvironment = true, + logger = cashAppPayLogger, ) } @@ -241,6 +246,8 @@ object CashAppPayFactory { private val defaultOkHttpClient = OkHttpProvider.provideOkHttpClient() + private val cashAppPayLogger: CashAppLogger = CashAppLoggerImpl() + // Do NOT add `const` to these, as it will invalidate reflection for our Dev App. private val BASE_URL_SANDBOX = "https://sandbox.api.cash.app/customer-request/v1/" private val BASE_URL_PRODUCTION = "https://api.cash.app/customer-request/v1/" diff --git a/core/src/main/java/app/cash/paykit/core/android/LogTag.kt b/core/src/main/java/app/cash/paykit/core/android/LogTag.kt index 4fa7712..393d144 100644 --- a/core/src/main/java/app/cash/paykit/core/android/LogTag.kt +++ b/core/src/main/java/app/cash/paykit/core/android/LogTag.kt @@ -15,4 +15,4 @@ */ package app.cash.paykit.core.android -const val LOG_TAG = "CashAppPay" +const val CAP_TAG = "CashAppPay" diff --git a/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt b/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt index e1b162e..20e027a 100644 --- a/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt +++ b/core/src/main/java/app/cash/paykit/core/android/ThreadExtensions.kt @@ -15,20 +15,20 @@ */ package app.cash.paykit.core.android -import android.util.Log +import app.cash.paykit.logging.CashAppLogger /** * 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. */ -fun Thread.safeStart(errorMessage: String?, onError: () -> Unit? = {}) { +fun Thread.safeStart(errorMessage: String?, logger: CashAppLogger, onError: () -> Unit? = {}) { try { start() } catch (e: IllegalThreadStateException) { // This can happen if the thread is already started. - Log.e(LOG_TAG, errorMessage, e) + logger.logError(CAP_TAG, errorMessage ?: "", e) onError() } catch (e: InterruptedException) { - Log.e(LOG_TAG, errorMessage, e) + logger.logError(CAP_TAG, errorMessage ?: "", e) onError() } } 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 f9119df..8fdd95d 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 @@ -18,7 +18,6 @@ package app.cash.paykit.core.impl import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri -import android.util.Log import androidx.annotation.WorkerThread import app.cash.paykit.core.BuildConfig import app.cash.paykit.core.CashAppPay @@ -40,7 +39,7 @@ import app.cash.paykit.core.CashAppPayState.UpdatingCustomerRequest import app.cash.paykit.core.NetworkManager import app.cash.paykit.core.analytics.PayKitAnalyticsEventDispatcher import app.cash.paykit.core.android.ApplicationContextHolder -import app.cash.paykit.core.android.LOG_TAG +import app.cash.paykit.core.android.CAP_TAG import app.cash.paykit.core.android.safeStart import app.cash.paykit.core.exceptions.CashAppPayIntegrationException import app.cash.paykit.core.exceptions.CashAppPayNetworkErrorType.CONNECTIVITY @@ -58,6 +57,7 @@ import app.cash.paykit.core.utils.ThreadPurpose.CHECK_APPROVAL_STATUS 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 kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -72,7 +72,8 @@ internal class CashAppPayImpl( private val analyticsEventDispatcher: PayKitAnalyticsEventDispatcher, private val payKitLifecycleListener: CashAppPayLifecycleObserver, private val useSandboxEnvironment: Boolean = false, - private val singleThreadManager: SingleThreadManager = SingleThreadManagerImpl(), + private val logger: CashAppLogger, + private val singleThreadManager: SingleThreadManager = SingleThreadManagerImpl(logger), initialState: CashAppPayState = NotStarted, initialCustomerResponseData: CustomerResponseData? = null, ) : CashAppPay, CashAppPayLifecycleListener { @@ -105,7 +106,8 @@ internal class CashAppPayImpl( // Notify listener of State change. callbackListener?.cashAppPayStateDidChange(value) .orElse { - logError( + logger.logError( + CAP_TAG, "State changed to ${value.javaClass.simpleName}, but no listeners were notified." + "Make sure that you've used `registerForStateUpdates` to receive PayKit state updates.", ) @@ -136,7 +138,7 @@ internal class CashAppPayImpl( // Validate [paymentActions] is not empty. if (paymentActions.isEmpty()) { val exceptionText = "paymentAction should not be empty" - currentState = softCrashOrStateException(CashAppPayIntegrationException(exceptionText)) + currentState = softCrashOrStateException(exceptionText, CashAppPayIntegrationException(exceptionText)) return } @@ -179,7 +181,7 @@ internal class CashAppPayImpl( // Validate [paymentActions] is not empty. if (paymentActions.isEmpty()) { val exceptionText = "paymentAction should not be empty" - currentState = softCrashOrStateException(CashAppPayIntegrationException(exceptionText)) + currentState = softCrashOrStateException(exceptionText, CashAppPayIntegrationException(exceptionText)) return } @@ -247,6 +249,7 @@ internal class CashAppPayImpl( if (customerData == null) { logAndSoftCrash( + "No customer data found when attempting to authorize.", CashAppPayIntegrationException( "Can't call authorizeCustomerRequest user before calling `createCustomerRequest`. Alternatively provide your own customerData", ), @@ -255,7 +258,7 @@ internal class CashAppPayImpl( } if (customerData.isAuthTokenExpired()) { - logInfo("Auth token expired when attempting to authenticate, refreshing before proceeding.") + logger.logVerbose(CAP_TAG, "Auth token expired when attempting to authenticate, refreshing before proceeding.") deferredAuthorizeCustomerRequest() return } @@ -272,24 +275,24 @@ internal class CashAppPayImpl( currentState = Refreshing - logInfo("Will refresh customer request before proceeding with authorization.") + logger.logVerbose(CAP_TAG, "Will refresh customer request before proceeding with authorization.") singleThreadManager.createThread(DEFERRED_REFRESH) { val networkResult = networkManager.retrieveUpdatedRequestData( clientId, customerResponseData!!.id, ) if (networkResult is Failure) { - logError("Failed to refresh expired auth token customer request.") + logger.logError(CAP_TAG, "Failed to refresh expired auth token customer request.", networkResult.exception) currentState = CashAppPayExceptionState(networkResult.exception) return@createThread } - logInfo("Refreshed customer request with SUCCESS") + logger.logVerbose(CAP_TAG, "Refreshed customer request with SUCCESS") customerResponseData = (networkResult as Success).data.customerResponseData if (currentState == Refreshing) { authorizeCustomerRequest(customerResponseData!!) } - }.safeStart("Error while attempting to run deferred authorization.", onError = { + }.safeStart("Error while attempting to run deferred authorization.", logger, onError = { if (currentState == Refreshing) { currentState = CashAppPayExceptionState(CashAppPayNetworkException(CONNECTIVITY)) } @@ -323,7 +326,7 @@ internal class CashAppPayImpl( customerResponseData = customerData if (customerData.isAuthTokenExpired()) { - logInfo("Auth token expired when attempting to authenticate, refreshing before proceeding.") + logger.logVerbose(CAP_TAG, "Auth token expired when attempting to authenticate, refreshing before proceeding.") deferredAuthorizeCustomerRequest() return } @@ -349,7 +352,7 @@ internal class CashAppPayImpl( * Unregister any previously registered [CashAppPayListener] from PayKit updates. */ override fun unregisterFromStateUpdates() { - logInfo("Unregistering from state updates") + logger.logVerbose(CAP_TAG, "Unregistering from state updates") callbackListener = null payKitLifecycleListener.unregister(this) analyticsEventDispatcher.eventListenerRemoved() @@ -362,6 +365,7 @@ internal class CashAppPayImpl( private fun enforceRegisteredStateUpdatesListener() { if (callbackListener == null) { logAndSoftCrash( + "No listener registered for state updates.", CashAppPayIntegrationException( "Shouldn't call this function before registering for state updates via `registerForStateUpdates`.", ), @@ -401,7 +405,7 @@ internal class CashAppPayImpl( // Unsuccessful transaction. setStateFinished(false) } - }.safeStart(errorMessage = "Could not start checkForApprovalThread.") + }.safeStart(errorMessage = "Could not start checkForApprovalThread.", logger) } private fun refreshUnauthorizedCustomerRequest(delay: Duration) { @@ -416,13 +420,13 @@ internal class CashAppPayImpl( val currentTime = Clock.System.now() val hasExpired = customerResponseData?.expiresAt?.let { expiresAt -> currentTime > expiresAt } ?: false if (hasExpired) { - logError("Customer request has expired. Stopping refresh.") + logger.logError(CAP_TAG, "Customer request has expired. Stopping refresh.") return@createThread } if (currentState !is ReadyToAuthorize) { // In this case, we don't want to retry since we're in a state that doesn't allow it. - logError("Not refreshing unauthorized customer request because state is not ReadyToAuthorize") + logger.logWarning(CAP_TAG, "Not refreshing unauthorized customer request because state is not ReadyToAuthorize") return@createThread } @@ -431,16 +435,16 @@ internal class CashAppPayImpl( customerResponseData!!.id, ) if (networkResult is Failure) { - logError("Failed to refresh expiring auth token customer request.") + logger.logError(CAP_TAG, "Failed to refresh expiring auth token customer request.", networkResult.exception) // Retry refreshing unauthorized customer request. refreshUnauthorizedCustomerRequest(delay) return@createThread } - logInfo("Refreshed customer request with SUCCESS") + logger.logVerbose(CAP_TAG, "Refreshed customer request with SUCCESS") customerResponseData = (networkResult as Success).data.customerResponseData refreshUnauthorizedCustomerRequest(delay) - }.safeStart("Could not start refreshUnauthorizedThread.", onError = { + }.safeStart("Could not start refreshUnauthorizedThread.", logger, onError = { refreshUnauthorizedCustomerRequest(delay) }) } @@ -451,31 +455,23 @@ internal class CashAppPayImpl( */ private fun scheduleUnauthorizedCustomerRequestRefresh(customerResponseData: CustomerResponseData) { if (customerResponseData.authFlowTriggers?.refreshesAt == null) { - logError("Unable to schedule unauthorized customer request refresh. RefreshesAt is null.") + logger.logError(CAP_TAG, "Unable to schedule unauthorized customer request refresh. RefreshesAt is null.") return } val ttlSeconds = customerResponseData.authFlowTriggers.refreshesAt.minus(customerResponseData.createdAt) val refreshDelay = ttlSeconds.inWholeSeconds.minus(TOKEN_REFRESH_WINDOW.inWholeSeconds) - logInfo("Scheduling unauthorized customer request refresh in $refreshDelay seconds.") + logger.logVerbose(CAP_TAG, "Scheduling unauthorized customer request refresh in $refreshDelay seconds.") refreshUnauthorizedCustomerRequest(refreshDelay.seconds) } - private fun logError(errorMessage: String) { - Log.e(LOG_TAG, errorMessage) - } - - private fun logInfo(errorMessage: String) { - Log.i(LOG_TAG, errorMessage) - } - /** * This function will log in production, additionally it will throw an exception in sandbox or debug mode. */ @Throws - private fun logAndSoftCrash(exception: Exception) { - logError("Error occurred. E.: $exception") + private fun logAndSoftCrash(msg: String, exception: Exception) { + logger.logError(CAP_TAG, msg, exception) if (useSandboxEnvironment || BuildConfig.DEBUG) { throw exception } @@ -485,8 +481,8 @@ internal class CashAppPayImpl( * This function will throw the provided [exception] during development, or change the SDK state to [CashAppPayExceptionState] otherwise. */ @Throws - private fun softCrashOrStateException(exception: Exception): CashAppPayExceptionState { - logError("Error occurred. E.: $exception") + private fun softCrashOrStateException(msg: String, exception: Exception): CashAppPayExceptionState { + logger.logError(CAP_TAG, msg, exception) if (useSandboxEnvironment || BuildConfig.DEBUG) { throw exception } @@ -513,11 +509,11 @@ internal class CashAppPayImpl( */ override fun onApplicationForegrounded() { - logInfo("onApplicationForegrounded") + logger.logVerbose(CAP_TAG, "onApplicationForegrounded") updateStateAndPoolForTransactionStatus() } override fun onApplicationBackgrounded() { - logInfo("onApplicationBackgrounded") + logger.logVerbose(CAP_TAG, "onApplicationBackgrounded") } } diff --git a/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt index 56d5870..60abae2 100644 --- a/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/utils/SingleThreadManagerImpl.kt @@ -15,10 +15,9 @@ */ package app.cash.paykit.core.utils -import android.util.Log -import app.cash.paykit.core.android.LOG_TAG +import app.cash.paykit.logging.CashAppLogger -internal class SingleThreadManagerImpl : SingleThreadManager { +internal class SingleThreadManagerImpl(private val logger: CashAppLogger) : SingleThreadManager { private val threads: MutableMap = mutableMapOf() @@ -35,7 +34,7 @@ internal class SingleThreadManagerImpl : SingleThreadManager { try { threads[purpose]?.interrupt() } catch (e: Exception) { - Log.e(LOG_TAG, "Failed to interrupt thread: ${purpose.name}", e) + logger.logError(TAG, "Failed to interrupt thread: ${purpose.name}", e) } finally { threads[purpose] = null } @@ -44,4 +43,8 @@ internal class SingleThreadManagerImpl : SingleThreadManager { override fun interruptAllThreads() { threads.keys.forEach { interruptThread(it) } } + + companion object { + private const val TAG = "SingleThreadManager" + } } diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt index 3d34c71..826183f 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayAuthorizeTests.kt @@ -79,5 +79,6 @@ class CashAppPayAuthorizeTests { payKitLifecycleListener = mockk(relaxed = true), useSandboxEnvironment = true, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) } diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt index bd32078..24280a8 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt @@ -69,5 +69,6 @@ class CashAppPayExceptionsTests { payKitLifecycleListener = mockk(relaxed = true), useSandboxEnvironment = useSandboxEnvironment, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) } diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index a58985d..5d643e7 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -293,6 +293,7 @@ class CashAppPayStateTests { initialState = initialState, initialCustomerResponseData = initialCustomerResponseData, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) /** diff --git a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt index 38b00f9..31c835d 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkErrorTests.kt @@ -241,5 +241,6 @@ class NetworkErrorTests { payKitLifecycleListener = mockk(relaxed = true), useSandboxEnvironment = true, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) } diff --git a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt index 97a58aa..e7e76ee 100644 --- a/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt +++ b/core/src/test/java/app/cash/paykit/core/NetworkRetryTests.kt @@ -38,7 +38,9 @@ import kotlin.time.toDuration class NetworkRetryTests { - private val MAX_RETRIES = 2 + companion object { + private const val MAX_RETRIES = 2 + } @Test fun `failed network request will be retried and succeed`() { @@ -194,5 +196,6 @@ class NetworkRetryTests { payKitLifecycleListener = mockk(relaxed = true), useSandboxEnvironment = true, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) } diff --git a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt index 28f911d..f49033f 100644 --- a/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt +++ b/core/src/testRelease/java/app/cash/paykit/core/CashAppPayProdExceptionsTests.kt @@ -48,5 +48,6 @@ class CashAppPayProdExceptionsTests { payKitLifecycleListener = mockk(relaxed = true), useSandboxEnvironment = useSandboxEnvironment, analyticsEventDispatcher = mockk(relaxed = true), + logger = mockk(relaxed = true), ) } diff --git a/logging/.gitignore b/logging/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/logging/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts new file mode 100644 index 0000000..d934e7b --- /dev/null +++ b/logging/build.gradle.kts @@ -0,0 +1,59 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("com.vanniktech.maven.publish.base") +} + +//https://issuetracker.google.com/issues/226095015 +com.android.tools.analytics.AnalyticsSettings.optedIn = false + +android { + namespace = "app.cash.paykit.logging" + compileSdk = 31 + + defaultConfig { + minSdk = 21 + + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + lint { + abortOnError = true + htmlReport = true + warningsAsErrors = true + checkAllWarnings = true + baseline = file("lint-baseline.xml") + } +} + +val junit_version = rootProject.extra["junit_version"] as String +val google_truth_version = rootProject.extra["google_truth_version"] as String +val robolectric_version = rootProject.extra["robolectric_version"] as String + +dependencies { + testImplementation("junit:junit:$junit_version") + testImplementation("com.google.truth:truth:$google_truth_version") + testImplementation("org.robolectric:robolectric:$robolectric_version") +} + +mavenPublishing { + // AndroidMultiVariantLibrary(publish a sources jar, publish a javadoc jar) + configure(AndroidSingleVariantLibrary("release", true, true)) +} \ No newline at end of file diff --git a/logging/consumer-rules.pro b/logging/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/logging/lint-baseline.xml b/logging/lint-baseline.xml new file mode 100644 index 0000000..0722790 --- /dev/null +++ b/logging/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/logging/proguard-rules.pro b/logging/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/logging/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/logging/src/main/AndroidManifest.xml b/logging/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/logging/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLogEntry.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLogEntry.kt new file mode 100644 index 0000000..9507318 --- /dev/null +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLogEntry.kt @@ -0,0 +1,23 @@ +/* + * 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.logging + +data class CashAppLogEntry( + val level: Int, + val tag: String, + val msg: String, + val throwable: Throwable? = null, +) diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt new file mode 100644 index 0000000..82d2914 --- /dev/null +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt @@ -0,0 +1,53 @@ +/* + * 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.logging + +interface CashAppLogger { + + /** + * Log a message with level VERBOSE. + */ + fun logVerbose(tag: String, msg: String) + + /** + * Log a message with level WARNING. + */ + fun logWarning(tag: String, msg: String) + + /** + * Log a message with level ERROR. Optionally include a [Throwable] to log along the error message. + */ + fun logError(tag: String, msg: String, throwable: Throwable? = null) + + /** + * Retrieve all logs. + */ + fun retrieveLogs(): List + + /** + * Set a listener to be notified when a new log is added. + */ + fun setListener(listener: CashAppLoggerListener) + + /** + * Remove the currently registered listener, if any. + */ + fun removeListener() +} + +interface CashAppLoggerListener { + fun onNewLog(log: CashAppLogEntry) +} diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt new file mode 100644 index 0000000..d034cf7 --- /dev/null +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt @@ -0,0 +1,37 @@ +/* + * 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.logging + +import java.util.LinkedList + +internal class CashAppLoggerHistory { + companion object { + private const val HISTORY_MAX_SIZE = 200 + } + + private val history = LinkedList() + + fun log(entry: CashAppLogEntry) { + history.add(entry) + if (history.size > HISTORY_MAX_SIZE) { + history.removeFirst() + } + } + + fun retrieveLogs(): List { + return history.toList() + } +} diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt new file mode 100644 index 0000000..228ddc7 --- /dev/null +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt @@ -0,0 +1,56 @@ +/* + * 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.logging + +import android.util.Log + +class CashAppLoggerImpl : CashAppLogger { + + private val history = CashAppLoggerHistory() + private var listener: CashAppLoggerListener? = null + + override fun logVerbose(tag: String, msg: String) { + history.log(CashAppLogEntry(Log.VERBOSE, tag, msg)) + + // We purposely don't reuse the same CashAppLogEntry instance here to avoid leaking. + listener?.onNewLog(CashAppLogEntry(Log.VERBOSE, tag, msg)) + Log.v(tag, msg) + } + + override fun logWarning(tag: String, msg: String) { + history.log(CashAppLogEntry(Log.WARN, tag, msg)) + listener?.onNewLog(CashAppLogEntry(Log.WARN, tag, msg)) + Log.w(tag, msg) + } + + override fun logError(tag: String, msg: String, throwable: Throwable?) { + history.log(CashAppLogEntry(Log.ERROR, tag, msg, throwable)) + listener?.onNewLog(CashAppLogEntry(Log.ERROR, tag, msg, throwable)) + Log.e(tag, msg, throwable) + } + + override fun retrieveLogs(): List { + return history.retrieveLogs() + } + + override fun setListener(listener: CashAppLoggerListener) { + this.listener = listener + } + + override fun removeListener() { + this.listener = null + } +} diff --git a/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt new file mode 100644 index 0000000..1214577 --- /dev/null +++ b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt @@ -0,0 +1,66 @@ +/* + * 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.logging + +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class CashAppLoggerHistoryTests { + private lateinit var loggerHistory: CashAppLoggerHistory + + @Before + fun setUp() { + loggerHistory = CashAppLoggerHistory() + } + + @Test + fun `test log adds entry to history`() { + val entry = CashAppLogEntry(1, "tag1", "message1") + loggerHistory.log(entry) + assertThat(loggerHistory.retrieveLogs()).contains(entry) + } + + @Test + fun `test log removes first entry when history exceeds max size`() { + val oldEntry = CashAppLogEntry(1, "tag1", "message1") + val newEntry = CashAppLogEntry(2, "tag2", "message2") + + loggerHistory.log(oldEntry) + repeat(200) { + // this should remove the first "oldEntry" added above. + loggerHistory.log(newEntry) + } + + assertThat(loggerHistory.retrieveLogs()).doesNotContain(oldEntry) + assertThat(loggerHistory.retrieveLogs()).contains(newEntry) + } + + @Test + fun `test retrieveLogs returns a list containing all entries in history`() { + val entries = listOf( + CashAppLogEntry(1, "tag1", "message1"), + CashAppLogEntry(2, "tag2", "message2"), + CashAppLogEntry(3, "tag3", "message3"), + ) + + entries.forEach { + loggerHistory.log(it) + } + + assertThat(loggerHistory.retrieveLogs()).containsExactlyElementsIn(entries) + } +} diff --git a/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerImplTests.kt b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerImplTests.kt new file mode 100644 index 0000000..ea0015c --- /dev/null +++ b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerImplTests.kt @@ -0,0 +1,98 @@ +/* + * 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.logging + +import android.util.Log +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLog + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowLog::class]) +class CashAppLoggerImplTests { + + private lateinit var logger: CashAppLoggerImpl + private lateinit var fakeListener: CashAppLoggerListener + private lateinit var logEntries: MutableList + + @Before + fun setUp() { + // ShadowLog.stream = System.out redirects all logs to standard output. + // On the below tests ShadowLog.getLogs() gets all the logs sent to LogCat. + ShadowLog.stream = System.out + logger = CashAppLoggerImpl() + logEntries = mutableListOf() + + fakeListener = object : CashAppLoggerListener { + override fun onNewLog(log: CashAppLogEntry) { + logEntries.add(log) + } + } + logger.setListener(fakeListener) + } + + @Test + fun `test logVerbose logs correctly`() { + val tag = "tag" + val msg = "verbose log" + + logger.logVerbose(tag, msg) + + val expectedLogEntry = CashAppLogEntry(Log.VERBOSE, tag, msg) + assertThat(logger.retrieveLogs()).contains(expectedLogEntry) + assertThat(logEntries).contains(expectedLogEntry) + assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.type == Log.VERBOSE }).isTrue() + } + + @Test + fun `test logWarning logs correctly`() { + val tag = "tag" + val msg = "warning log" + + logger.logWarning(tag, msg) + + val expectedLogEntry = CashAppLogEntry(Log.WARN, tag, msg) + assertThat(logger.retrieveLogs()).contains(expectedLogEntry) + assertThat(logEntries).contains(expectedLogEntry) + assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.type == Log.WARN }).isTrue() + } + + @Test + fun `test logError logs correctly`() { + val tag = "tag" + val msg = "error log" + val throwable = Throwable("stuff happens") + + logger.logError(tag, msg, throwable) + + val expectedLogEntry = CashAppLogEntry(Log.ERROR, tag, msg, throwable) + assertThat(logger.retrieveLogs()).contains(expectedLogEntry) + assertThat(logEntries).contains(expectedLogEntry) + assertThat(ShadowLog.getLogs().any { it.tag == tag && it.msg == msg && it.throwable == throwable && it.type == Log.ERROR }).isTrue() + } + + @Test + fun `test removeListener removes the listener`() { + logger.removeListener() + logger.logVerbose("tag", "message") + + assertThat(logEntries).isEmpty() + } +} diff --git a/settings.gradle b/settings.gradle index 307b27e..66cfe7c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ dependencyResolutionManagement { rootProject.name = "Pay Kit SDK" include ':core' include ':analytics-core' +include ':logging' From d8a4001dca2ee7e8576970910f8b07b22bf7ca82 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 21 Jul 2023 13:42:31 -0700 Subject: [PATCH 15/26] Set default PR template (#121) --- .github/pull_request_template.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d3c2e45 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## Jira Ticket +[Jira Ticket]() + +## What are you trying to accomplish? + +## How did you accomplish this? + +## Steps to manually test this change: + + +## Visual: + +| Before | After | +|--------|-------| +| | | \ No newline at end of file From 282d76438cf48cd45d046f8a03b0891fd92d682a Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Tue, 1 Aug 2023 14:28:29 -0700 Subject: [PATCH 16/26] Make Initializer open to modification (#123) --- CHANGELOG.md | 2 ++ .../main/java/app/cash/paykit/core/CashAppPayInitializer.kt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d223c..2ee86d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 2.3.0 +Class `CashAppPayInitializer` was made open, so that androidx.startup can be manually overridden. + This version contains change to the bundled Cash App Pay button. Previously, `light` and `dark` variants of the button were made possible by using 2 different views, respectively `CashAppPayButtonLight` an `CashAppPayButtonDark`. As of this version, the diff --git a/core/src/main/java/app/cash/paykit/core/CashAppPayInitializer.kt b/core/src/main/java/app/cash/paykit/core/CashAppPayInitializer.kt index d342bb3..c1130dd 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPayInitializer.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPayInitializer.kt @@ -20,10 +20,10 @@ import androidx.annotation.Keep import androidx.startup.Initializer import app.cash.paykit.core.android.ApplicationContextHolder -internal interface CashAppPayInitializerStub +interface CashAppPayInitializerStub @Keep -internal class CashAppPayInitializer : Initializer { +class CashAppPayInitializer : Initializer { override fun create(context: Context): CashAppPayInitializerStub { ApplicationContextHolder.init(context.applicationContext) return object : CashAppPayInitializerStub {} From eb25c8d3a0041c1bc82db4e9332d0e797f2773b1 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 4 Aug 2023 09:38:17 -0700 Subject: [PATCH 17/26] Update Spotless Plugin and Ktlint version (#124) --- build.gradle | 4 ++-- .../core/impl/CashAppPayLifecycleObserverImpl.kt | 4 ++-- .../payloads/AnalyticsCustomerRequestPayload.kt | 10 +++++----- .../payloads/AnalyticsEventListenerPayload.kt | 6 +++--- .../payloads/AnalyticsInitializationPayload.kt | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 3c39411..370389d 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ plugins { id 'com.android.application' version '7.4.2' apply false id 'com.android.library' version '7.4.2' apply false id 'org.jetbrains.kotlin.android' version '1.6.21' apply false - id "com.diffplug.spotless" version "6.17.0" + id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" } @@ -38,7 +38,7 @@ subprojects { subproject -> kotlin { target("src/**/*.kt") // ktlint doesn't honour .editorconfig yet: https://github.com/diffplug/spotless/issues/142 - ktlint('0.48.2').editorConfigOverride([ + ktlint('0.49.1').editorConfigOverride([ 'insert_final_newline': 'true', 'end_of_line': 'lf', 'charset': 'utf-8', diff --git a/core/src/main/java/app/cash/paykit/core/impl/CashAppPayLifecycleObserverImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayLifecycleObserverImpl.kt index 7ac363c..026a2d9 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/CashAppPayLifecycleObserverImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/CashAppPayLifecycleObserverImpl.kt @@ -38,7 +38,7 @@ internal class CashAppPayLifecycleObserverImpl( private var mainHandler: Handler = Handler(Looper.getMainLooper()) /* - * Functions to register & unregister instances of [PayKitLifecycleListener]. + * Functions to register & unregister instances of [PayKitLifecycleListener]. */ override fun register(newInstance: CashAppPayLifecycleListener) { @@ -78,7 +78,7 @@ internal class CashAppPayLifecycleObserverImpl( } /* - * Callback functions from [DefaultLifecycleObserver]. + * Callback functions from [DefaultLifecycleObserver]. */ override fun onStart(owner: LifecycleOwner) { diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt index b9b80bd..f89c4a0 100644 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt +++ b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt @@ -26,8 +26,8 @@ import com.squareup.moshi.JsonClass data class AnalyticsCustomerRequestPayload( /* - * Common fields. - */ + * Common fields. + */ @Json(name = "mobile_cap_pk_customer_request_sdk_version") override val sdkVersion: String, @@ -44,8 +44,8 @@ data class AnalyticsCustomerRequestPayload( override val environment: String, /* - * Create Request. - */ + * Create Request. + */ // This represents the SDK State. @Json(name = "mobile_cap_pk_customer_request_action") @@ -72,7 +72,7 @@ data class AnalyticsCustomerRequestPayload( val createMetadata: String? = null, /* - * Generic Event properties. + * Generic Event properties. */ // The status of the Customer Request. diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsEventListenerPayload.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsEventListenerPayload.kt index 428786e..a7024eb 100644 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsEventListenerPayload.kt +++ b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsEventListenerPayload.kt @@ -24,8 +24,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class AnalyticsEventListenerPayload( /* - * Common fields. - */ + * Common fields. + */ @Json(name = "mobile_cap_pk_event_listener_sdk_version") sdkVersion: String, @@ -42,7 +42,7 @@ class AnalyticsEventListenerPayload( override val environment: String, /* - * Event Specific fields. + * Event Specific fields. */ /** diff --git a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsInitializationPayload.kt b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsInitializationPayload.kt index 57923cc..86dbc55 100644 --- a/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsInitializationPayload.kt +++ b/core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsInitializationPayload.kt @@ -24,8 +24,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class AnalyticsInitializationPayload( /* - * Common fields. - */ + * Common fields. + */ @Json(name = "mobile_cap_pk_initialization_sdk_version") sdkVersion: String, From 28838869067161dfd941d828f9e2c038b27589e4 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 18 Aug 2023 14:33:43 -0700 Subject: [PATCH 18/26] Add minify rules (#125) * Add consumer-rules for minify * Bump release version; update release notes * Add more minify rules --- CHANGELOG.md | 8 ++-- build.gradle | 4 +- core/consumer-rules.pro | 87 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee86d1..8f2d169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -# 2.3.0 +# UPCOMING - 2.3.0 -Class `CashAppPayInitializer` was made open, so that androidx.startup can be manually overridden. - -This version contains change to the bundled Cash App Pay button. + - The class `CashAppPayInitializer` was made open, so that androidx.startup can be manually overridden. + - This version bundles fixes for minify enabled builds. + - This version contains change to the bundled Cash App Pay button. Previously, `light` and `dark` variants of the button were made possible by using 2 different views, respectively `CashAppPayButtonLight` an `CashAppPayButtonDark`. As of this version, the there will only be a single `CashAppPayButton` view, which has been updated to support both variants. diff --git a/build.gradle b/build.gradle index 370389d..61722a8 100644 --- a/build.gradle +++ b/build.gradle @@ -52,11 +52,11 @@ subprojects { subproject -> } } -def NEXT_VERSION = "2.3.1-SNAPSHOT" +def NEXT_VERSION = "2.3.3-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.3.0-SNAPSHOT' + version = '2.3.2-SNAPSHOT' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing { diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro index e69de29..0f0c193 100644 --- a/core/consumer-rules.pro +++ b/core/consumer-rules.pro @@ -0,0 +1,87 @@ +# Keep enums within the project. +-keep enum app.cash.paykit.core.models.response.GrantType { *; } +-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 + + +# OkHttp related. Can be removed after OkHttp is updated. +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE \ No newline at end of file From 22a6975f1f62ab4d18f743319212cf74f412346f Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 18 Aug 2023 14:44:56 -0700 Subject: [PATCH 19/26] Update Core dependency on OkHttp (#126) * Add consumer-rules for minify * Bump release version; update release notes * Add more minify rules * Update Core dependency on OkHttp --- CHANGELOG.md | 8 +++++++- build.gradle | 2 +- core/consumer-rules.pro | 14 +------------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2d169..0f28d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ - The class `CashAppPayInitializer` was made open, so that androidx.startup can be manually overridden. - This version bundles fixes for minify enabled builds. - - This version contains change to the bundled Cash App Pay button. + - Updated internal dependency on `OkHttp` to version `4.11.0`. + +## Breaking Changes + + - This version contains a change to the bundled Cash App Pay button. Previously, `light` and `dark` variants of the button were made possible by using 2 different views, respectively `CashAppPayButtonLight` an `CashAppPayButtonDark`. As of this version, the there will only be a single `CashAppPayButton` view, which has been updated to support both variants. @@ -28,6 +32,8 @@ Dark Variant: This change makes it possible for developer to use the button in a more flexible way, such as using a style that changes accordingly to the OS theme. +You should migrate any instances of `CashAppPayButtonLight` and `CashAppPayButtonDark` to `CashAppPayButton`. + # 2.2.1 Here's what has changed on this release: diff --git a/build.gradle b/build.gradle index 61722a8..b1a3d73 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { mockwebserver_version = '4.10.0' google_truth_version = '1.1.5' startup_version = '1.1.1' - okhttp_version = '4.10.0' + okhttp_version = '4.11.0' kotlinx_date_version = '0.4.0' versions = [ diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro index 0f0c193..2346dba 100644 --- a/core/consumer-rules.pro +++ b/core/consumer-rules.pro @@ -72,16 +72,4 @@ kotlinx.datetime.Instant$Companion$serializer -dontwarn kotlinx.serialization.descriptors.PrimitiveKind -dontwarn kotlinx.serialization.descriptors.SerialDescriptor -dontwarn kotlinx.serialization.descriptors.SerialDescriptorsKt --dontwarn kotlinx.serialization.internal.AbstractPolymorphicSerializer - - -# OkHttp related. Can be removed after OkHttp is updated. --dontwarn org.bouncycastle.jsse.BCSSLParameters --dontwarn org.bouncycastle.jsse.BCSSLSocket --dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider --dontwarn org.conscrypt.Conscrypt$Version --dontwarn org.conscrypt.Conscrypt --dontwarn org.conscrypt.ConscryptHostnameVerifier --dontwarn org.openjsse.javax.net.ssl.SSLParameters --dontwarn org.openjsse.javax.net.ssl.SSLSocket --dontwarn org.openjsse.net.ssl.OpenJSSE \ No newline at end of file +-dontwarn kotlinx.serialization.internal.AbstractPolymorphicSerializer \ No newline at end of file From 899d5d20b10aebb9ca14d4e0e90d234bae07ff24 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Thu, 2 Nov 2023 14:45:19 -0700 Subject: [PATCH 20/26] Update Android target version (#133) * Increase compile version to API 33 * Update CHANGELOG * Update AGP to 8.1.2 (#134) * Update AGP to 8.1.2 * Update Java version for CI to 17 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yaml | 2 +- CHANGELOG.md | 4 ++++ analytics-core/build.gradle | 4 ++++ build.gradle | 12 ++++++------ core/build.gradle | 6 +++++- gradle/wrapper/gradle-wrapper.properties | 2 +- logging/build.gradle.kts | 2 +- 8 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eac705d..a07fdfc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Static Analysis run: ./gradlew lint spotlessCheck diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1e7ca16..1dbf575 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Publish Artifacts run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ab6f5..97471a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.4.0 - UPCOMING + + - Update Android target SDK version to API 33 + # 2.3.0 - The class `CashAppPayInitializer` was made open, so that androidx.startup can be manually overridden. diff --git a/analytics-core/build.gradle b/analytics-core/build.gradle index 2dc3c33..4ed399d 100644 --- a/analytics-core/build.gradle +++ b/analytics-core/build.gradle @@ -39,6 +39,10 @@ android { warningsAsErrors true baseline file("lint-baseline.xml") } + + buildFeatures { + buildConfig = true + } } dependencies { diff --git a/build.gradle b/build.gradle index c269f61..333b86c 100644 --- a/build.gradle +++ b/build.gradle @@ -18,15 +18,15 @@ buildscript { versions = [ 'minSdk': 21, - 'compileSdk': 31, - 'targetSdk': 31, + 'compileSdk': 33, + 'targetSdk': 33, ] } } plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.1.2' apply false + id 'com.android.library' version '8.1.2' apply false id 'org.jetbrains.kotlin.android' version '1.6.21' apply false id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" @@ -52,11 +52,11 @@ subprojects { subproject -> } } -def NEXT_VERSION = "2.3.4-SNAPSHOT" +def NEXT_VERSION = "2.4.1-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.3.0' + version = '2.4.0-SNAPSHOT' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing { diff --git a/core/build.gradle b/core/build.gradle index 59168c7..d24e38b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -8,7 +8,7 @@ plugins { task sourceJar(type: Jar) { from android.sourceSets.main.java.srcDirs - classifier "sources" + archiveClassifier.set('sources') } android { @@ -57,6 +57,10 @@ android { includeAndroidResources = true } } + + buildFeatures { + buildConfig = true + } } dependencies { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ce631a7..65d136f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Feb 09 13:50:05 PST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts index d934e7b..047a768 100644 --- a/logging/build.gradle.kts +++ b/logging/build.gradle.kts @@ -11,7 +11,7 @@ com.android.tools.analytics.AnalyticsSettings.optedIn = false android { namespace = "app.cash.paykit.logging" - compileSdk = 31 + compileSdk = 33 defaultConfig { minSdk = 21 From c96b2f0a8893f9a3d3dde67c4729dd23c78372a7 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Mon, 4 Dec 2023 16:03:07 -0800 Subject: [PATCH 21/26] Update AGP & Gradle (#138) * Update AGP and Gradle * Update Robolectric dependency --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 333b86c..9aa10dd 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { lifecycle_version = '2.5.1' mockk_version = '1.13.3' coroutines_test_version = '1.6.4' - robolectric_version = '4.10.3' + robolectric_version = '4.11.1' mockwebserver_version = '4.10.0' google_truth_version = '1.1.5' startup_version = '1.1.1' @@ -25,8 +25,8 @@ buildscript { } plugins { - id 'com.android.application' version '8.1.2' apply false - id 'com.android.library' version '8.1.2' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false id 'org.jetbrains.kotlin.android' version '1.6.21' apply false id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 65d136f..2f95b34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Feb 09 13:50:05 PST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 3b8e953eb16fd045b41336916053c096a7f8906f Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 12 Jan 2024 09:59:02 -0800 Subject: [PATCH 22/26] Pedro/sdk kotlin 1.8 (#139) * Update AGP version * Update Kotlin to 1.8.x, along with various library dependencies * Update Dev App versions and baseline lint files --- analytics-core/build.gradle | 8 +-- analytics-core/lint-baseline.xml | 2 +- build.gradle | 23 ++++---- core/build.gradle | 12 ++--- core/lint-baseline.xml | 90 +------------------------------- logging/build.gradle.kts | 8 +-- logging/lint-baseline.xml | 2 +- 7 files changed, 29 insertions(+), 116 deletions(-) diff --git a/analytics-core/build.gradle b/analytics-core/build.gradle index 4ed399d..ef4d204 100644 --- a/analytics-core/build.gradle +++ b/analytics-core/build.gradle @@ -25,11 +25,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = '1.8' + kotlin { + jvmToolchain(11) } lintOptions { diff --git a/analytics-core/lint-baseline.xml b/analytics-core/lint-baseline.xml index 27ab162..f30ba1a 100644 --- a/analytics-core/lint-baseline.xml +++ b/analytics-core/lint-baseline.xml @@ -1,4 +1,4 @@ - + diff --git a/build.gradle b/build.gradle index 9aa10dd..551fc4d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,20 @@ import com.vanniktech.maven.publish.SonatypeHost +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { junit_androidx_version = '1.1.5' junit_version = '4.13.2' - moshi_version = '1.13.0' - lifecycle_version = '2.5.1' - mockk_version = '1.13.3' - coroutines_test_version = '1.6.4' + moshi_version = '1.15.0' + lifecycle_version = '2.6.2' + mockk_version = '1.13.5' + coroutines_test_version = '1.7.3' robolectric_version = '4.11.1' - mockwebserver_version = '4.10.0' - google_truth_version = '1.1.5' + google_truth_version = '1.2.0' startup_version = '1.1.1' - okhttp_version = '4.11.0' - kotlinx_date_version = '0.4.0' + okhttp_version = '4.12.0' + kotlinx_date_version = '0.4.1' versions = [ 'minSdk': 21, @@ -25,9 +25,9 @@ buildscript { } plugins { - id 'com.android.application' version '8.2.0' apply false - id 'com.android.library' version '8.2.0' apply false - id 'org.jetbrains.kotlin.android' version '1.6.21' apply false + id 'com.android.application' version '8.2.1' apply false + id 'com.android.library' version '8.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.8.21' apply false id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" } @@ -55,6 +55,7 @@ subprojects { subproject -> def NEXT_VERSION = "2.4.1-SNAPSHOT" allprojects { + group = 'app.cash.paykit' version = '2.4.0-SNAPSHOT' diff --git a/core/build.gradle b/core/build.gradle index d24e38b..4a40fcd 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' - id("com.google.devtools.ksp").version("1.6.21-1.0.5") + id("com.google.devtools.ksp").version("1.8.21-1.0.11") id 'project-report' // run ./gradlew htmlDependencyReport id "com.vanniktech.maven.publish.base" } @@ -34,11 +34,11 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = '1.8' + kotlin { + jvmToolchain(11) } resourcePrefix 'cap_' @@ -92,7 +92,7 @@ dependencies { testImplementation "io.mockk:mockk:$mockk_version" testImplementation "com.google.truth:truth:$google_truth_version" androidTestImplementation "androidx.test.ext:junit-ktx:$junit_androidx_version" - testImplementation "com.squareup.okhttp3:mockwebserver:$mockwebserver_version" + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version" // Robolectric environment. testImplementation "org.robolectric:robolectric:$robolectric_version" // Coroutines test support. diff --git a/core/lint-baseline.xml b/core/lint-baseline.xml index fb1b897..f30ba1a 100644 --- a/core/lint-baseline.xml +++ b/core/lint-baseline.xml @@ -1,92 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts index 047a768..4056ac2 100644 --- a/logging/build.gradle.kts +++ b/logging/build.gradle.kts @@ -26,12 +26,12 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "1.8" + kotlin { + jvmToolchain(11) } lint { diff --git a/logging/lint-baseline.xml b/logging/lint-baseline.xml index 0722790..f30ba1a 100644 --- a/logging/lint-baseline.xml +++ b/logging/lint-baseline.xml @@ -1,4 +1,4 @@ - + From e48a5f69372cc6b250cd5756e2be3eeed8ef37ae Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Tue, 27 Feb 2024 10:09:16 -0800 Subject: [PATCH 23/26] Update AGP and dev app version --- .idea/kotlinc.xml | 2 +- .idea/misc.xml | 2 +- build.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 7e340a7..217e5c5 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 67639da..0ad17cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/build.gradle b/build.gradle index 551fc4d..b1a4fd3 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ buildscript { } plugins { - id 'com.android.application' version '8.2.1' apply false - id 'com.android.library' version '8.2.1' apply false + id 'com.android.application' version '8.2.2' apply false + id 'com.android.library' version '8.2.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.21' apply false id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" From 9957ef100939133e2f3a88050ab1c3af504e4570 Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Tue, 16 Jul 2024 07:24:06 -0700 Subject: [PATCH 24/26] Use reference_id param correctly (#145) * Add Start Screen to split different playgrouds within Dev App * Improve webview logs display * Increase log history size * Fix accountReferenceId param missing from network call * Update some of the project libraries * Fix test related to size of log history * Minor changes based on PR feedback --- CHANGELOG.md | 1 + build.gradle | 6 ++--- core/build.gradle | 2 +- .../models/request/CustomerRequestData.kt | 2 ++ .../request/CustomerRequestDataFactory.kt | 13 ++++++++-- .../app/cash/paykit/logging/CashAppLogger.kt | 9 +++++++ .../paykit/logging/CashAppLoggerHistory.kt | 2 +- .../cash/paykit/logging/CashAppLoggerImpl.kt | 24 +++++++++++++++++++ .../logging/CashAppLoggerHistoryTests.kt | 2 +- 9 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97471a8..870c22b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.4.0 - UPCOMING - Update Android target SDK version to API 33 + - Fix: `OnFileAction` param `accountReferenceId` is now properly being sent over the network # 2.3.0 diff --git a/build.gradle b/build.gradle index b1a4fd3..c5cb48c 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ buildscript { lifecycle_version = '2.6.2' mockk_version = '1.13.5' coroutines_test_version = '1.7.3' - robolectric_version = '4.11.1' - google_truth_version = '1.2.0' + robolectric_version = '4.13' + google_truth_version = '1.4.4' startup_version = '1.1.1' okhttp_version = '4.12.0' kotlinx_date_version = '0.4.1' @@ -27,7 +27,7 @@ buildscript { plugins { id 'com.android.application' version '8.2.2' apply false id 'com.android.library' version '8.2.2' apply false - id 'org.jetbrains.kotlin.android' version '1.8.21' apply false + id 'org.jetbrains.kotlin.android' version '1.8.22' apply false id "com.diffplug.spotless" version "6.20.0" id "com.vanniktech.maven.publish.base" version "0.25.1" } diff --git a/core/build.gradle b/core/build.gradle index 4a40fcd..e4d88c7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' - id("com.google.devtools.ksp").version("1.8.21-1.0.11") + id("com.google.devtools.ksp").version("1.8.22-1.0.11") id 'project-report' // run ./gradlew htmlDependencyReport id "com.vanniktech.maven.publish.base" } diff --git a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt index b286089..bebcbad 100644 --- a/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt +++ b/core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt @@ -28,4 +28,6 @@ data class CustomerRequestData( val channel: String?, @Json(name = "redirect_url") val redirectUri: PiiString?, + @Json(name = "reference_id") + val referenceId: PiiString?, ) 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 b202753..7383626 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 @@ -37,9 +37,16 @@ object CustomerRequestDataFactory { isRequestUpdate: Boolean = false, ): CustomerRequestData { val actions = ArrayList(paymentActions.size) + var possibleReferenceId: String? = null + for (paymentAction in paymentActions) { when (paymentAction) { - is OnFileAction -> actions.add(buildFromOnFileAction(clientId, paymentAction)) + is OnFileAction -> { + actions.add(buildFromOnFileAction(clientId, paymentAction)) + if (paymentAction.accountReferenceId != null) { + possibleReferenceId = paymentAction.accountReferenceId + } + } is OneTimeAction -> actions.add(buildFromOneTimeAction(clientId, paymentAction)) } } @@ -49,12 +56,14 @@ object CustomerRequestDataFactory { actions = actions, channel = null, redirectUri = null, + referenceId = possibleReferenceId?.let { PiiString(it) }, ) } else { CustomerRequestData( actions = actions, channel = CHANNEL_IN_APP, - redirectUri = redirectUri?.let { PiiString(redirectUri) }, + redirectUri = redirectUri?.let { PiiString(it) }, + referenceId = possibleReferenceId?.let { PiiString(it) }, ) } } diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt index 82d2914..44a1716 100644 --- a/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLogger.kt @@ -37,6 +37,15 @@ interface CashAppLogger { */ fun retrieveLogs(): List + /** + * Retrieves all logs, compiled as a single string. + * Each log entry is separated by two newline characters. + * The format of each log entry is: "LEVEL: MESSAGE". + * + * If you need more control over the format, use [retrieveLogs] instead. + */ + fun logsAsString(): String + /** * Set a listener to be notified when a new log is added. */ diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt index d034cf7..1979fb7 100644 --- a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerHistory.kt @@ -19,7 +19,7 @@ import java.util.LinkedList internal class CashAppLoggerHistory { companion object { - private const val HISTORY_MAX_SIZE = 200 + private const val HISTORY_MAX_SIZE = 5000 } private val history = LinkedList() diff --git a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt index 228ddc7..8a514a4 100644 --- a/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt +++ b/logging/src/main/java/app/cash/paykit/logging/CashAppLoggerImpl.kt @@ -46,6 +46,30 @@ class CashAppLoggerImpl : CashAppLogger { return history.retrieveLogs() } + override fun logsAsString(): String { + return buildString { + for (log in history.retrieveLogs()) { + append(logLevelToString(log.level)).append(": ").append(log.msg) + if (log.throwable != null) { + append("\n").append(" Exception: ").append(log.throwable.cause).append(": ").append(log.throwable.message) + } + append("\n\n") + } + } + } + + private fun logLevelToString(level: Int): String { + return when (level) { + Log.VERBOSE -> "VERBOSE" + Log.DEBUG -> "DEBUG" + Log.INFO -> "INFO" + Log.WARN -> "WARN" + Log.ERROR -> "ERROR" + Log.ASSERT -> "ASSERT" + else -> "UNKNOWN" + } + } + override fun setListener(listener: CashAppLoggerListener) { this.listener = listener } diff --git a/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt index 1214577..7a45fe5 100644 --- a/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt +++ b/logging/src/test/java/app/cash/paykit/logging/CashAppLoggerHistoryTests.kt @@ -40,7 +40,7 @@ class CashAppLoggerHistoryTests { val newEntry = CashAppLogEntry(2, "tag2", "message2") loggerHistory.log(oldEntry) - repeat(200) { + repeat(5000) { // this should remove the first "oldEntry" added above. loggerHistory.log(newEntry) } From 67845e0aca85e63839887664ad29f56a719346bb Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 19 Jul 2024 10:07:35 -0700 Subject: [PATCH 25/26] Reference id mixup fix (#147) * Correct distinction between reference_id and account_reference_id * Spotless apply * Make function changes be backwards compatible * Update CHANGELOG * Fix UTs --- CHANGELOG.md | 5 ++- .../java/app/cash/paykit/core/CashAppPay.kt | 11 +++++- .../app/cash/paykit/core/NetworkManager.kt | 2 + .../PayKitAnalyticsEventDispatcherImpl.kt | 11 ------ .../cash/paykit/core/impl/CashAppPayImpl.kt | 37 +++++++++++++++---- .../paykit/core/impl/NetworkManagerImpl.kt | 23 ++++++++++-- .../cash/paykit/core/models/common/Action.kt | 3 ++ .../request/CustomerRequestDataFactory.kt | 14 +++---- .../models/sdk/CashAppPayPaymentAction.kt | 12 +++--- .../paykit/core/CashAppPayExceptionsTests.kt | 2 +- .../cash/paykit/core/CashAppPayStateTests.kt | 4 +- 11 files changed, 84 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c9fa7..4ec1210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# 2.5.0 - UPCOMING +# 2.5.0 + + - Fix correct usage of `account_reference_id` in `OnFileAction` + - Add `reference_id` as a parameter of creating a customer request. This is to bring the Android SDK to parity with iOS and Web. # 2.4.0 diff --git a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt index b2d0cdb..2f22479 100644 --- a/core/src/main/java/app/cash/paykit/core/CashAppPay.kt +++ b/core/src/main/java/app/cash/paykit/core/CashAppPay.kt @@ -45,9 +45,10 @@ interface CashAppPay { * @param paymentAction A wrapper class that contains all of the necessary ingredients for building a customer requests. * Look at [PayKitPaymentAction] for more details. * @param redirectUri The URI for Cash App to redirect back to your app. If you do not set this, back navigation from CashApp might not work as intended. + * @param referenceId Optional identifier generated by you for this request, typically used to associate the resource with a record in an external system. */ @WorkerThread - fun createCustomerRequest(paymentAction: CashAppPayPaymentAction, redirectUri: String?) + fun createCustomerRequest(paymentAction: CashAppPayPaymentAction, redirectUri: String?, referenceId: String? = null) /** * Create customer request given list of [CashAppPayPaymentAction]. @@ -57,9 +58,11 @@ interface CashAppPay { * @param paymentActions A wrapper class that contains all of the necessary ingredients for building one or more customer requests. * Look at [PayKitPaymentAction] for more details. * @param redirectUri The URI for Cash App to redirect back to your app. If you do not set this, back navigation from CashApp might not work as intended. + * @param referenceId Optional identifier generated by you for this request, typically used to associate the resource with a record in an external system. + * */ @WorkerThread - fun createCustomerRequest(paymentActions: List, redirectUri: String?) + fun createCustomerRequest(paymentActions: List, redirectUri: String?, referenceId: String? = null) /** * Update an existing customer request given its [requestId] and the updated definitions contained within [CashAppPayPaymentAction]. @@ -69,11 +72,13 @@ interface CashAppPay { * @param requestId ID of the request we intent do update. * @param paymentAction A wrapper class that contains all of the necessary ingredients for updating a customer request for a given [requestId]. * Look at [CashAppPayPaymentAction] for more details. + * @param referenceId Optional identifier generated by you for this request, typically used to associate the resource with a record in an external system. */ @WorkerThread fun updateCustomerRequest( requestId: String, paymentAction: CashAppPayPaymentAction, + referenceId: String? = null, ) /** @@ -84,11 +89,13 @@ interface CashAppPay { * @param requestId ID of the request we intent do update. * @param paymentActions A wrapper class that contains all of the necessary ingredients for updating one more more customer requests that share the same [requestId]. * Look at [CashAppPayPaymentAction] for more details. + * @param referenceId Optional identifier generated by you for this request, typically used to associate the resource with a record in an external system. */ @WorkerThread fun updateCustomerRequest( requestId: String, paymentActions: List, + referenceId: String? = null, ) /** diff --git a/core/src/main/java/app/cash/paykit/core/NetworkManager.kt b/core/src/main/java/app/cash/paykit/core/NetworkManager.kt index c5f184b..f507d99 100644 --- a/core/src/main/java/app/cash/paykit/core/NetworkManager.kt +++ b/core/src/main/java/app/cash/paykit/core/NetworkManager.kt @@ -28,12 +28,14 @@ internal interface NetworkManager { clientId: String, paymentActions: List, redirectUri: String?, + referenceId: String?, ): NetworkResult @Throws(IOException::class) fun updateCustomerRequest( clientId: String, requestId: String, + referenceId: String?, paymentActions: List, ): NetworkResult 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 777531e..f560789 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 @@ -47,7 +47,6 @@ import app.cash.paykit.core.models.request.CustomerRequestDataFactory.CHANNEL_IN import app.cash.paykit.core.models.response.CustomerResponseData import app.cash.paykit.core.models.response.Grant import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction -import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction import app.cash.paykit.core.network.MoshiProvider import app.cash.paykit.core.utils.Clock import app.cash.paykit.core.utils.ClockRealImpl @@ -218,14 +217,6 @@ internal class PayKitAnalyticsEventDispatcherImpl( val moshiAdapter: JsonAdapter> = moshi.adapter() val apiActionsAsJson: String = moshiAdapter.toJson(apiActions) - // Inner payload of the ES2 event. - var possibleReferenceId: String? = null - for (paymentAction in paymentKitActions) { - if (paymentAction is OnFileAction) { - possibleReferenceId = paymentAction.accountReferenceId - } - } - return AnalyticsCustomerRequestPayload( sdkVersion, userAgent, @@ -235,7 +226,6 @@ internal class PayKitAnalyticsEventDispatcherImpl( createActions = apiActionsAsJson, createChannel = CHANNEL_IN_APP, createRedirectUrl = redirectUri?.let { PiiString(redirectUri) }, - createReferenceId = possibleReferenceId?.let { PiiString(possibleReferenceId) }, environment = sdkEnvironment, ) } @@ -285,7 +275,6 @@ internal class PayKitAnalyticsEventDispatcherImpl( customerId = customerResponseData?.customerProfile?.id, customerCashTag = customerResponseData?.customerProfile?.cashTag, requestId = customerResponseData?.id, - referenceId = customerResponseData?.referenceId, environment = sdkEnvironment, ) } 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 8fdd95d..8e80afd 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 @@ -120,8 +120,12 @@ internal class CashAppPayImpl( analyticsEventDispatcher.sdkInitialized() } - override fun createCustomerRequest(paymentAction: CashAppPayPaymentAction, redirectUri: String?) { - createCustomerRequest(listOf(paymentAction), redirectUri) + override fun createCustomerRequest( + paymentAction: CashAppPayPaymentAction, + redirectUri: String?, + referenceId: String?, + ) { + createCustomerRequest(listOf(paymentAction), redirectUri, referenceId) } /** @@ -132,7 +136,11 @@ internal class CashAppPayImpl( * Look at [PayKitPaymentAction] for more details. */ @WorkerThread - override fun createCustomerRequest(paymentActions: List, redirectUri: String?) { + override fun createCustomerRequest( + paymentActions: List, + redirectUri: String?, + referenceId: String?, + ) { enforceRegisteredStateUpdatesListener() // Validate [paymentActions] is not empty. @@ -145,7 +153,12 @@ internal class CashAppPayImpl( currentState = CreatingCustomerRequest // Network call. - val networkResult = networkManager.createCustomerRequest(clientId, paymentActions, redirectUri) + val networkResult = networkManager.createCustomerRequest( + clientId = clientId, + paymentActions = paymentActions, + redirectUri = redirectUri, + referenceId = referenceId, + ) when (networkResult) { is Failure -> { currentState = CashAppPayExceptionState(networkResult.exception) @@ -159,8 +172,12 @@ internal class CashAppPayImpl( } } - override fun updateCustomerRequest(requestId: String, paymentAction: CashAppPayPaymentAction) { - updateCustomerRequest(requestId, listOf(paymentAction)) + override fun updateCustomerRequest( + requestId: String, + paymentAction: CashAppPayPaymentAction, + referenceId: String?, + ) { + updateCustomerRequest(requestId, listOf(paymentAction), referenceId) } /** @@ -175,6 +192,7 @@ internal class CashAppPayImpl( override fun updateCustomerRequest( requestId: String, paymentActions: List, + referenceId: String?, ) { enforceRegisteredStateUpdatesListener() @@ -188,7 +206,12 @@ internal class CashAppPayImpl( currentState = UpdatingCustomerRequest // Network request. - val networkResult = networkManager.updateCustomerRequest(clientId, requestId, paymentActions) + val networkResult = networkManager.updateCustomerRequest( + clientId = clientId, + requestId = requestId, + referenceId = referenceId, + paymentActions = paymentActions, + ) when (networkResult) { is Failure -> { currentState = CashAppPayExceptionState(networkResult.exception) diff --git a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt index 578f75b..3beaec9 100644 --- a/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt +++ b/core/src/main/java/app/cash/paykit/core/impl/NetworkManagerImpl.kt @@ -81,8 +81,14 @@ internal class NetworkManagerImpl( clientId: String, paymentActions: List, redirectUri: String?, + referenceId: String?, ): NetworkResult { - val customerRequestData = CustomerRequestDataFactory.build(clientId, redirectUri, paymentActions) + val customerRequestData = CustomerRequestDataFactory.build( + clientId = clientId, + redirectUri = redirectUri, + referenceId = referenceId, + paymentActions = paymentActions, + ) val createCustomerRequest = CreateCustomerRequest( idempotencyKey = UUID.randomUUID().toString(), customerRequestData = customerRequestData, @@ -103,16 +109,27 @@ internal class NetworkManagerImpl( override fun updateCustomerRequest( clientId: String, requestId: String, + referenceId: String?, paymentActions: List, ): NetworkResult { val customerRequestData = - CustomerRequestDataFactory.build(clientId, null, paymentActions, isRequestUpdate = true) + CustomerRequestDataFactory.build( + clientId = clientId, + redirectUri = null, + referenceId = referenceId, + paymentActions = paymentActions, + isRequestUpdate = true, + ) val createCustomerRequest = CreateCustomerRequest( customerRequestData = customerRequestData, ) // Record analytics. - analyticsEventDispatcher?.updatedCustomerRequest(requestId, paymentActions, customerRequestData.actions) + analyticsEventDispatcher?.updatedCustomerRequest( + requestId = requestId, + paymentKitActions = paymentActions, + apiActions = customerRequestData.actions, + ) return executeNetworkRequest( PATCH, diff --git a/core/src/main/java/app/cash/paykit/core/models/common/Action.kt b/core/src/main/java/app/cash/paykit/core/models/common/Action.kt index 6f05a3d..9dda99f 100644 --- a/core/src/main/java/app/cash/paykit/core/models/common/Action.kt +++ b/core/src/main/java/app/cash/paykit/core/models/common/Action.kt @@ -15,6 +15,7 @@ */ package app.cash.paykit.core.models.common +import app.cash.paykit.core.models.pii.PiiString import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -28,4 +29,6 @@ data class Action( val scopeId: String, @Json(name = "type") val type: String, + @Json(name = "account_reference_id") + val accountReferenceId: PiiString? = null, ) 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 7383626..24c5981 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 @@ -33,20 +33,15 @@ object CustomerRequestDataFactory { fun build( clientId: String, redirectUri: String?, + referenceId: String?, paymentActions: List, isRequestUpdate: Boolean = false, ): CustomerRequestData { val actions = ArrayList(paymentActions.size) - var possibleReferenceId: String? = null for (paymentAction in paymentActions) { when (paymentAction) { - is OnFileAction -> { - actions.add(buildFromOnFileAction(clientId, paymentAction)) - if (paymentAction.accountReferenceId != null) { - possibleReferenceId = paymentAction.accountReferenceId - } - } + is OnFileAction -> actions.add(buildFromOnFileAction(clientId, paymentAction)) is OneTimeAction -> actions.add(buildFromOneTimeAction(clientId, paymentAction)) } } @@ -56,14 +51,14 @@ object CustomerRequestDataFactory { actions = actions, channel = null, redirectUri = null, - referenceId = possibleReferenceId?.let { PiiString(it) }, + referenceId = referenceId?.let { PiiString(it) }, ) } else { CustomerRequestData( actions = actions, channel = CHANNEL_IN_APP, redirectUri = redirectUri?.let { PiiString(it) }, - referenceId = possibleReferenceId?.let { PiiString(it) }, + referenceId = referenceId?.let { PiiString(it) }, ) } } @@ -78,6 +73,7 @@ object CustomerRequestDataFactory { return Action( scopeId = scopeIdOrClientId, type = PAYMENT_TYPE_ON_FILE, + accountReferenceId = onFileAction.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 d660d6b..9ef153f 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 @@ -20,7 +20,7 @@ import app.cash.paykit.core.CashAppPay /** * This class holds the information necessary for [CashAppPay.createCustomerRequest] to be executed. */ -sealed class CashAppPayPaymentAction(scopeId: String?) { +sealed class CashAppPayPaymentAction(open val scopeId: String?, open val referenceId: String?) { /** * Describes an intent for a client to charge a customer a given amount. @@ -38,8 +38,9 @@ sealed class CashAppPayPaymentAction(scopeId: String?) { data class OneTimeAction( val currency: CashAppPayCurrency?, val amount: Int?, - val scopeId: String? = null, - ) : CashAppPayPaymentAction(scopeId) + override val scopeId: String? = null, + override val referenceId: String? = null, + ) : CashAppPayPaymentAction(scopeId, referenceId) /** * Describes an intent for a client to store a customer's account, allowing a client to create payments @@ -49,8 +50,9 @@ sealed class CashAppPayPaymentAction(scopeId: String?) { * @param accountReferenceId Identifier of the account or customer associated to the on file action. */ data class OnFileAction( - val scopeId: String? = null, + override val scopeId: String? = null, val accountReferenceId: String? = null, + override val referenceId: String? = null, ) : - CashAppPayPaymentAction(scopeId) + CashAppPayPaymentAction(scopeId, referenceId) } diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt index 24280a8..0f7a8a0 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayExceptionsTests.kt @@ -56,7 +56,7 @@ class CashAppPayExceptionsTests { val listener = mockk(relaxed = true) payKit.registerForStateUpdates(listener) - every { networkManager.createCustomerRequest(any(), any(), any()) } returns NetworkResult.failure( + every { networkManager.createCustomerRequest(any(), any(), any(), any()) } returns NetworkResult.failure( Exception("bad"), ) payKit.createCustomerRequest(FakeData.oneTimePayment, FakeData.REDIRECT_URI) diff --git a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt index 5d643e7..50da4d0 100644 --- a/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt +++ b/core/src/test/java/app/cash/paykit/core/CashAppPayStateTests.kt @@ -76,7 +76,7 @@ class CashAppPayStateTests { val listener = mockk(relaxed = true) payKit.registerForStateUpdates(listener) - every { networkManager.createCustomerRequest(any(), any(), any()) } returns NetworkResult.failure( + every { networkManager.createCustomerRequest(any(), any(), any(), any()) } returns NetworkResult.failure( Exception("bad"), ) @@ -95,6 +95,7 @@ class CashAppPayStateTests { any(), any(), any(), + any(), ) } returns NetworkResult.failure( Exception("bad"), @@ -180,6 +181,7 @@ class CashAppPayStateTests { any(), any(), any(), + any(), ) } returns customerTopLevelResponse From 56c0a380967223c3f3d6b0b0222a7835265e0e9b Mon Sep 17 00:00:00 2001 From: Pedro Veloso Date: Fri, 19 Jul 2024 19:14:42 +0200 Subject: [PATCH 26/26] Prepare release 2.5.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 66c2bd7..4d1aff1 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ def NEXT_VERSION = "2.5.1-SNAPSHOT" allprojects { group = 'app.cash.paykit' - version = '2.5.0-SNAPSHOT' + version = '2.5.0' plugins.withId("com.vanniktech.maven.publish.base") { mavenPublishing {