Skip to content

Commit b62ee84

Browse files
authored
Prepare release 2.2.1 (#114)
* Fix Retrieve Existing request ID behavior for PROCESSING state (#95) * Fix Retrieve Existing request ID behavior for PENDING state * Add distinction between Pending & Processing * 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 * Add enum for GrantType (#99) * Update Robolectric dependency (#102) * Update changelog * Use Epoch usec for Analytics timestamp (#107) * Add UT for Analytics (#108) * 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 * Abstract thread managment away from main class (#113) * 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 * Prepare release 2.2.1 * Add changelog
1 parent 8478e8b commit b62ee84

17 files changed

+346
-54
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# 2.2.1
2+
3+
Here's what has changed on this release:
4+
5+
- Information that can be considered PII is now marked as such by implementing the interface `PiiContent`.
6+
- This is possibly a breaking change, but in practice for the majority of people out there, these shouldn't be properties that you're using.
7+
- Improved Thread management to prevent memory leaks and unexpected behavior.
8+
9+
110
# 2.2.0
211

312
In this release of our open source SDK, we've made a significant update that involves modifying the date types within our returned payloads from string to Instant. Please note that this modification is a breaking change. Here's what has been altered:

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ subprojects { subproject ->
5252
}
5353
}
5454

55-
def NEXT_VERSION = "2.2.1-SNAPSHOT"
55+
def NEXT_VERSION = "2.2.2-SNAPSHOT"
5656

5757
allprojects {
5858
group = 'app.cash.paykit'
59-
version = '2.2.0'
59+
version = '2.2.1'
6060

6161
plugins.withId("com.vanniktech.maven.publish.base") {
6262
mavenPublishing {

core/src/main/java/app/cash/paykit/core/analytics/PayKitAnalyticsEventDispatcherImpl.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import app.cash.paykit.core.models.analytics.payloads.AnalyticsInitializationPay
4242
import app.cash.paykit.core.models.common.Action
4343
import app.cash.paykit.core.models.common.NetworkResult.Failure
4444
import app.cash.paykit.core.models.common.NetworkResult.Success
45+
import app.cash.paykit.core.models.pii.PiiString
4546
import app.cash.paykit.core.models.request.CustomerRequestDataFactory.CHANNEL_IN_APP
4647
import app.cash.paykit.core.models.response.CustomerResponseData
4748
import app.cash.paykit.core.models.response.Grant
@@ -67,7 +68,7 @@ internal class PayKitAnalyticsEventDispatcherImpl(
6768
private val sdkEnvironment: String,
6869
private val payKitAnalytics: PayKitAnalytics,
6970
private val networkManager: NetworkManager,
70-
private val moshi: Moshi = MoshiProvider.provideDefault(),
71+
private val moshi: Moshi = MoshiProvider.provideDefault(redactPii = true),
7172
private val uuidManager: UUIDManager = UUIDManagerRealImpl(),
7273
private val clock: Clock = ClockRealImpl(),
7374
) : PayKitAnalyticsEventDispatcher {
@@ -233,8 +234,8 @@ internal class PayKitAnalyticsEventDispatcherImpl(
233234
action = stateToAnalyticsAction(actionType),
234235
createActions = apiActionsAsJson,
235236
createChannel = CHANNEL_IN_APP,
236-
createRedirectUrl = redirectUri,
237-
createReferenceId = possibleReferenceId,
237+
createRedirectUrl = redirectUri?.let { PiiString(redirectUri) },
238+
createReferenceId = possibleReferenceId?.let { PiiString(possibleReferenceId) },
238239
environment = sdkEnvironment,
239240
)
240241
}

core/src/main/java/app/cash/paykit/core/impl/CashAppCashAppPayImpl.kt

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ import app.cash.paykit.core.models.response.STATUS_APPROVED
5252
import app.cash.paykit.core.models.response.STATUS_PENDING
5353
import app.cash.paykit.core.models.response.STATUS_PROCESSING
5454
import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction
55+
import app.cash.paykit.core.utils.SingleThreadManager
56+
import app.cash.paykit.core.utils.SingleThreadManagerImpl
57+
import app.cash.paykit.core.utils.ThreadPurpose.CHECK_APPROVAL_STATUS
58+
import app.cash.paykit.core.utils.ThreadPurpose.DEFERRED_REFRESH
59+
import app.cash.paykit.core.utils.ThreadPurpose.REFRESH_AUTH_TOKEN
5560
import app.cash.paykit.core.utils.orElse
5661
import kotlinx.datetime.Clock
5762
import kotlin.time.Duration
@@ -67,6 +72,7 @@ internal class CashAppCashAppPayImpl(
6772
private val analyticsEventDispatcher: PayKitAnalyticsEventDispatcher,
6873
private val payKitLifecycleListener: CashAppPayLifecycleObserver,
6974
private val useSandboxEnvironment: Boolean = false,
75+
private val singleThreadManager: SingleThreadManager = SingleThreadManagerImpl(),
7076
initialState: CashAppPayState = NotStarted,
7177
initialCustomerResponseData: CustomerResponseData? = null,
7278
) : CashAppPay, CashAppPayLifecycleListener {
@@ -75,9 +81,6 @@ internal class CashAppCashAppPayImpl(
7581

7682
private var customerResponseData: CustomerResponseData? = initialCustomerResponseData
7783

78-
private var checkForApprovalThread: Thread? = null
79-
private var refreshUnauthorizedThread: Thread? = null
80-
8184
private var currentState: CashAppPayState = initialState
8285
set(value) {
8386
field = value
@@ -265,24 +268,20 @@ internal class CashAppCashAppPayImpl(
265268
*/
266269
private fun deferredAuthorizeCustomerRequest() {
267270
// Stop the thread that refreshes the customer request.
268-
try {
269-
refreshUnauthorizedThread?.interrupt()
270-
} catch (e: Exception) {
271-
logError("Error while interrupting previous thread. Exception: $e")
272-
}
271+
singleThreadManager.interruptThread(REFRESH_AUTH_TOKEN)
273272

274273
currentState = Refreshing
275274

276275
logInfo("Will refresh customer request before proceeding with authorization.")
277-
Thread {
276+
singleThreadManager.createThread(DEFERRED_REFRESH) {
278277
val networkResult = networkManager.retrieveUpdatedRequestData(
279278
clientId,
280279
customerResponseData!!.id,
281280
)
282281
if (networkResult is Failure) {
283282
logError("Failed to refresh expired auth token customer request.")
284283
currentState = CashAppPayExceptionState(networkResult.exception)
285-
return@Thread
284+
return@createThread
286285
}
287286
logInfo("Refreshed customer request with SUCCESS")
288287
customerResponseData = (networkResult as Success).data.customerResponseData
@@ -357,12 +356,7 @@ internal class CashAppCashAppPayImpl(
357356
analyticsEventDispatcher.shutdown()
358357

359358
// Stop any polling operations that might be running.
360-
try {
361-
refreshUnauthorizedThread?.interrupt()
362-
checkForApprovalThread?.interrupt()
363-
} catch (e: Exception) {
364-
logError("Error while interrupting threads. Exception: $e")
365-
}
359+
singleThreadManager.interruptAllThreads()
366360
}
367361

368362
private fun enforceRegisteredStateUpdatesListener() {
@@ -376,14 +370,14 @@ internal class CashAppCashAppPayImpl(
376370
}
377371

378372
private fun poolTransactionStatus() {
379-
checkForApprovalThread = Thread {
373+
singleThreadManager.createThread(CHECK_APPROVAL_STATUS) {
380374
val networkResult = networkManager.retrieveUpdatedRequestData(
381375
clientId,
382376
customerResponseData!!.id,
383377
)
384378
if (networkResult is Failure) {
385379
currentState = CashAppPayExceptionState(networkResult.exception)
386-
return@Thread
380+
return@createThread
387381
}
388382
customerResponseData = (networkResult as Success).data.customerResponseData
389383

@@ -397,47 +391,39 @@ internal class CashAppCashAppPayImpl(
397391
try {
398392
Thread.sleep(500)
399393
} catch (e: InterruptedException) {
400-
return@Thread
394+
return@createThread
401395
}
402396

403397
poolTransactionStatus()
404-
return@Thread
398+
return@createThread
405399
}
406400

407401
// Unsuccessful transaction.
408402
setStateFinished(false)
409403
}
410-
}
411-
checkForApprovalThread?.safeStart(errorMessage = "Could not start checkForApprovalThread.")
404+
}.safeStart(errorMessage = "Could not start checkForApprovalThread.")
412405
}
413406

414407
private fun refreshUnauthorizedCustomerRequest(delay: Duration) {
415-
// Before starting a new thread, cancel any previous one.
416-
try {
417-
refreshUnauthorizedThread?.interrupt()
418-
} catch (e: Exception) {
419-
logError("Error while interrupting previous thread. Exception: $e")
420-
}
421-
422-
refreshUnauthorizedThread = Thread {
408+
singleThreadManager.createThread(REFRESH_AUTH_TOKEN) {
423409
try {
424410
Thread.sleep(delay.inWholeMilliseconds)
425411
} catch (e: InterruptedException) {
426-
return@Thread
412+
return@createThread
427413
}
428414

429415
// Stop refreshing if the request has expired.
430416
val currentTime = Clock.System.now()
431417
val hasExpired = customerResponseData?.expiresAt?.let { expiresAt -> currentTime > expiresAt } ?: false
432418
if (hasExpired) {
433419
logError("Customer request has expired. Stopping refresh.")
434-
return@Thread
420+
return@createThread
435421
}
436422

437423
if (currentState !is ReadyToAuthorize) {
438424
// In this case, we don't want to retry since we're in a state that doesn't allow it.
439425
logError("Not refreshing unauthorized customer request because state is not ReadyToAuthorize")
440-
return@Thread
426+
return@createThread
441427
}
442428

443429
val networkResult = networkManager.retrieveUpdatedRequestData(
@@ -449,14 +435,12 @@ internal class CashAppCashAppPayImpl(
449435

450436
// Retry refreshing unauthorized customer request.
451437
refreshUnauthorizedCustomerRequest(delay)
452-
return@Thread
438+
return@createThread
453439
}
454440
logInfo("Refreshed customer request with SUCCESS")
455441
customerResponseData = (networkResult as Success).data.customerResponseData
456442
refreshUnauthorizedCustomerRequest(delay)
457-
}
458-
459-
refreshUnauthorizedThread?.safeStart("Could not start refreshUnauthorizedThread.", onError = {
443+
}.safeStart("Could not start refreshUnauthorizedThread.", onError = {
460444
refreshUnauthorizedCustomerRequest(delay)
461445
})
462446
}

core/src/main/java/app/cash/paykit/core/models/analytics/payloads/AnalyticsCustomerRequestPayload.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package app.cash.paykit.core.models.analytics.payloads
1717

18+
import app.cash.paykit.core.models.pii.PiiString
1819
import com.squareup.moshi.Json
1920
import com.squareup.moshi.JsonClass
2021

@@ -60,11 +61,11 @@ data class AnalyticsCustomerRequestPayload(
6061

6162
// The redirect URL when creating a Customer Request.
6263
@Json(name = "mobile_cap_pk_customer_request_create_redirect_url")
63-
val createRedirectUrl: String? = null,
64+
val createRedirectUrl: PiiString? = null,
6465

6566
// The reference ID when creating a Customer Request.
6667
@Json(name = "mobile_cap_pk_customer_request_create_reference_id")
67-
val createReferenceId: String? = null,
68+
val createReferenceId: PiiString? = null,
6869

6970
// A string built from the metadata when creating a Customer Request.
7071
@Json(name = "mobile_cap_pk_customer_request_create_metadata")
@@ -96,7 +97,7 @@ data class AnalyticsCustomerRequestPayload(
9697

9798
// The redirect URL of the Customer Request.
9899
@Json(name = "mobile_cap_pk_customer_request_redirect_url")
99-
val redirectUrl: String? = null,
100+
val redirectUrl: PiiString? = null,
100101

101102
// The created at timestamp of the Customer Request.
102103
@Json(name = "mobile_cap_pk_customer_request_created_at")
@@ -120,7 +121,7 @@ data class AnalyticsCustomerRequestPayload(
120121

121122
// The reference ID of the Customer Request.
122123
@Json(name = "mobile_cap_pk_customer_request_reference_id")
123-
val referenceId: String? = null,
124+
val referenceId: PiiString? = null,
124125

125126
// The name of the Requester Profile in the Customer Request.
126127
@Json(name = "mobile_cap_pk_customer_request_requester_name")
@@ -132,7 +133,7 @@ data class AnalyticsCustomerRequestPayload(
132133

133134
// The Cashtag of the Customer Profile in the Customer Request.
134135
@Json(name = "mobile_cap_pk_customer_request_customer_cashtag")
135-
val customerCashTag: String? = null,
136+
val customerCashTag: PiiString? = null,
136137

137138
// A string built from the metadata in the Customer Request.
138139
@Json(name = "mobile_cap_pk_customer_request_metadata")
@@ -148,7 +149,7 @@ data class AnalyticsCustomerRequestPayload(
148149

149150
// The reference ID when updating a Customer Request.
150151
@Json(name = "mobile_cap_pk_customer_request_update_reference_id")
151-
val updateReferenceId: String? = null,
152+
val updateReferenceId: PiiString? = null,
152153

153154
// The redirect URL when creating a Customer Request.
154155
@Json(name = "mobile_cap_pk_customer_request_update_metadata")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (C) 2023 Cash App
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.paykit.core.models.pii
17+
18+
/**
19+
* This is a marker interface for PII content (Personal Identifiable Information). It is meant to signal to the developer that a object
20+
* of this class contains PII and should be treated as such.
21+
*/
22+
interface PiiContent
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2023 Cash App
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.paykit.core.models.pii
17+
18+
/**
19+
* A string that has been classified as Personal Identifiable Information (PII).
20+
*
21+
*/
22+
class PiiString(private var value: String) : PiiContent {
23+
24+
override fun toString(): String {
25+
return value
26+
}
27+
}

core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestData.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package app.cash.paykit.core.models.request
1717

1818
import app.cash.paykit.core.models.common.Action
19+
import app.cash.paykit.core.models.pii.PiiString
1920
import com.squareup.moshi.Json
2021
import com.squareup.moshi.JsonClass
2122

@@ -26,5 +27,5 @@ data class CustomerRequestData(
2627
@Json(name = "channel")
2728
val channel: String?,
2829
@Json(name = "redirect_url")
29-
val redirectUri: String?,
30+
val redirectUri: PiiString?,
3031
)

core/src/main/java/app/cash/paykit/core/models/request/CustomerRequestDataFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package app.cash.paykit.core.models.request
1717

1818
import app.cash.paykit.core.models.common.Action
19+
import app.cash.paykit.core.models.pii.PiiString
1920
import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction
2021
import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OnFileAction
2122
import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction.OneTimeAction
@@ -53,7 +54,7 @@ object CustomerRequestDataFactory {
5354
CustomerRequestData(
5455
actions = actions,
5556
channel = CHANNEL_IN_APP,
56-
redirectUri = redirectUri,
57+
redirectUri = redirectUri?.let { PiiString(redirectUri) },
5758
)
5859
}
5960
}

core/src/main/java/app/cash/paykit/core/models/response/CustomerProfile.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package app.cash.paykit.core.models.response
1717

18+
import app.cash.paykit.core.models.pii.PiiString
1819
import com.squareup.moshi.Json
1920
import com.squareup.moshi.JsonClass
2021

@@ -23,5 +24,5 @@ data class CustomerProfile(
2324
@Json(name = "id")
2425
val id: String,
2526
@Json(name = "cashtag")
26-
val cashTag: String,
27+
val cashTag: PiiString,
2728
)

0 commit comments

Comments
 (0)