Skip to content

Commit 2a50ca3

Browse files
Merge pull request #10 from afterpay/maintenance/update-from-upstream-4.3.0
EIT-2581: Update from upstream v4.3.0
2 parents 1cee27c + b91b71f commit 2a50ca3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1833
-116
lines changed

Diff for: .idea/androidTestResultsUserPreferences.xml

+37
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .idea/deploymentTargetDropDown.xml

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Add `afterpay-android-button` to your `build.gradle` dependencies.
1515

1616
```gradle
1717
dependencies {
18-
implementation 'com.afterpay:afterpay-android-button:4.0.2'
18+
implementation 'com.afterpay:afterpay-android-button:4.3.0'
1919
}
2020
```
2121

Diff for: afterpay/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ dependencies {
4646
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
4747
implementation "androidx.core:core-ktx:${versions.core_ktx}"
4848
implementation "androidx.appcompat:appcompat:${versions.app_compat}"
49+
4950
testImplementation "junit:junit:${versions.junit}"
5051
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
52+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlinx_coroutines}"
53+
testImplementation "io.mockk:mockk:${versions.mockk}"
5154
}
5255

5356
apply plugin: 'com.vanniktech.maven.publish'

Diff for: afterpay/src/main/kotlin/com/afterpay/android/Afterpay.kt

+75
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package com.afterpay.android
22

33
import android.content.Context
44
import android.content.Intent
5+
import androidx.annotation.WorkerThread
6+
import com.afterpay.android.cashapp.AfterpayCashAppCheckout
7+
import com.afterpay.android.cashapp.CashAppSignOrderResult
8+
import com.afterpay.android.cashapp.CashAppValidationResponse
59
import com.afterpay.android.internal.AfterpayDrawable
610
import com.afterpay.android.internal.AfterpayString
711
import com.afterpay.android.internal.ApiV3
@@ -70,6 +74,9 @@ object Afterpay {
7074
internal var checkoutV2Handler: AfterpayCheckoutV2Handler? = null
7175
private set
7276

77+
val environment: AfterpayEnvironment?
78+
get() = configuration?.environment
79+
7380
/**
7481
* Returns an [Intent] for the given [context] and [checkoutUrl] that can be passed to
7582
* [startActivityForResult][android.app.Activity.startActivityForResult] to initiate the
@@ -95,6 +102,74 @@ object Afterpay {
95102
): Intent = Intent(context, AfterpayCheckoutV2Activity::class.java)
96103
.putCheckoutV2OptionsExtra(options)
97104

105+
/**
106+
* Signs an Afterpay Cash App order for the relevant [token] and calls
107+
* calls [complete] when done. This method should be called prior to calling
108+
* createCustomerRequest on the Cash App Pay Kit SDK
109+
*/
110+
@JvmStatic
111+
@WorkerThread
112+
suspend fun signCashAppOrderToken(
113+
token: String,
114+
complete: (CashAppSignOrderResult) -> Unit,
115+
) {
116+
AfterpayCashAppCheckout.performSignPaymentRequest(token, complete)
117+
}
118+
119+
/**
120+
* Async version of the [signCashAppOrderToken] method.
121+
*
122+
* Signs an Afterpay Cash App order for the relevant [token] and calls
123+
* [complete] when done. This method should be called prior to calling
124+
* createCustomerRequest on the Cash App Pay Kit SDK
125+
*/
126+
@DelicateCoroutinesApi
127+
@JvmStatic
128+
fun signCashAppOrderTokenAsync(
129+
token: String,
130+
complete: (CashAppSignOrderResult) -> Unit,
131+
): CompletableFuture<Unit?> {
132+
return GlobalScope.future {
133+
signCashAppOrderToken(token, complete)
134+
}
135+
}
136+
137+
/**
138+
* Validates the Cash App order for the relevant [jwt], [customerId] and [grantId]
139+
* and calls [complete] once finished. This method should be called for a One Time payment
140+
* once the Cash App order is in the approved state
141+
*/
142+
@JvmStatic
143+
@WorkerThread
144+
fun validateCashAppOrder(
145+
jwt: String,
146+
customerId: String,
147+
grantId: String,
148+
complete: (CashAppValidationResponse) -> Unit,
149+
) {
150+
AfterpayCashAppCheckout.validatePayment(jwt, customerId, grantId, complete)
151+
}
152+
153+
/**
154+
* Async version of the [validateCashAppOrder] method.
155+
*
156+
* Validates the Cash App order for the relevant [jwt], [customerId] and [grantId]
157+
* and calls [complete] once finished. This method should be called for a One Time payment
158+
* once the Cash App order is in the approved state
159+
*/
160+
@DelicateCoroutinesApi
161+
@JvmStatic
162+
fun validateCashAppOrderAsync(
163+
jwt: String,
164+
customerId: String,
165+
grantId: String,
166+
complete: (CashAppValidationResponse) -> Unit,
167+
): CompletableFuture<Unit> {
168+
return GlobalScope.future {
169+
validateCashAppOrder(jwt, customerId, grantId, complete)
170+
}
171+
}
172+
98173
/**
99174
* Returns the [token][String] parsed from the given [intent] returned by a successful
100175
* Afterpay checkout.
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
package com.afterpay.android
22

3+
import java.net.URL
34
import java.util.Locale
45

5-
enum class AfterpayEnvironment {
6-
SANDBOX, PRODUCTION;
6+
const val API_PLUS_SANDBOX_BASE_URL = "https://api-plus.us-sandbox.afterpay.com"
7+
const val API_PLUS_PRODUCTION_BASE_URL = "https://api-plus.us.afterpay.com"
8+
9+
enum class AfterpayEnvironment(
10+
val payKitClientId: String,
11+
val cashAppPaymentSigningUrl: URL,
12+
val cashAppPaymentValidationUrl: URL,
13+
) {
14+
SANDBOX(
15+
payKitClientId = "CAS-CI_AFTERPAY",
16+
cashAppPaymentSigningUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/sign-payment"),
17+
cashAppPaymentValidationUrl = URL("$API_PLUS_SANDBOX_BASE_URL/v2/payments/validate-payment"),
18+
),
19+
20+
PRODUCTION(
21+
payKitClientId = "CA-CI_AFTERPAY",
22+
cashAppPaymentSigningUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/sign-payment"),
23+
cashAppPaymentValidationUrl = URL("$API_PLUS_PRODUCTION_BASE_URL/v2/payments/validate-payment"),
24+
),
25+
;
726

827
override fun toString(): String = name.lowercase(Locale.ROOT)
928
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.afterpay.android.cashapp
2+
3+
data class AfterpayCashApp(
4+
val amount: Double,
5+
val redirectUri: String,
6+
val merchantId: String,
7+
val brandId: String,
8+
val jwt: String,
9+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.afterpay.android.cashapp
2+
3+
import com.afterpay.android.BuildConfig
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.decodeFromString
6+
import kotlinx.serialization.encodeToString
7+
import kotlinx.serialization.json.Json
8+
import java.io.InvalidObjectException
9+
import java.io.OutputStreamWriter
10+
import java.net.HttpURLConnection
11+
import java.net.URL
12+
import javax.net.ssl.HttpsURLConnection
13+
14+
internal object AfterpayCashAppApi {
15+
private val json = Json { ignoreUnknownKeys = true }
16+
17+
internal inline fun <reified T, reified B> cashRequest(url: URL, method: CashHttpVerb, body: B): Result<T> {
18+
val connection = url.openConnection() as HttpsURLConnection
19+
return try {
20+
configure(connection, method)
21+
val payload = (body as? String) ?: json.encodeToString(body)
22+
23+
OutputStreamWriter(connection.outputStream).use { writer ->
24+
writer.write(payload)
25+
writer.flush()
26+
}
27+
28+
if (connection.errorStream == null && connection.responseCode < HttpURLConnection.HTTP_BAD_REQUEST) {
29+
connection.inputStream.bufferedReader().use { reader ->
30+
val data = reader.readText()
31+
val result = json.decodeFromString<T>(data)
32+
Result.success(result)
33+
}
34+
} else {
35+
throw InvalidObjectException("Unexpected response code: ${connection.responseCode}.")
36+
}
37+
} catch (exception: Exception) {
38+
Result.failure(exception)
39+
}
40+
}
41+
42+
private fun configure(connection: HttpsURLConnection, type: CashHttpVerb) {
43+
connection.requestMethod = type.name
44+
connection.setRequestProperty("${BuildConfig.AfterpayLibraryVersion}-android", "X-Afterpay-SDK")
45+
when (type) {
46+
CashHttpVerb.POST, CashHttpVerb.PUT -> {
47+
connection.setRequestProperty("Content-Type", "application/json")
48+
connection.setRequestProperty("Accept", "application/json")
49+
}
50+
else -> { }
51+
}
52+
when (type) {
53+
CashHttpVerb.GET -> {
54+
connection.doInput = true
55+
connection.doOutput = false
56+
}
57+
CashHttpVerb.PUT -> {
58+
connection.doInput = true
59+
connection.doOutput = false
60+
}
61+
CashHttpVerb.POST -> {
62+
connection.doInput = true
63+
connection.doOutput = true
64+
}
65+
}
66+
}
67+
68+
internal enum class CashHttpVerb {
69+
POST, PUT, GET
70+
}
71+
72+
@Serializable
73+
internal data class ApiErrorCashApp(
74+
val errorCode: String,
75+
val errorId: String,
76+
val message: String,
77+
val httpStatusCode: Int,
78+
)
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.afterpay.android.cashapp
2+
3+
import com.afterpay.android.Afterpay
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.runBlocking
6+
import kotlinx.coroutines.withContext
7+
import kotlinx.serialization.encodeToString
8+
import kotlinx.serialization.json.Json
9+
10+
sealed class CashAppSignOrderResult {
11+
data class Success(val response: AfterpayCashApp) : CashAppSignOrderResult()
12+
data class Failure(val error: Throwable) : CashAppSignOrderResult()
13+
}
14+
15+
sealed class CashAppValidationResponse {
16+
data class Success(val response: AfterpayCashAppValidationResponse) : CashAppValidationResponse()
17+
data class Failure(val error: Throwable) : CashAppValidationResponse()
18+
}
19+
20+
object AfterpayCashAppCheckout {
21+
suspend fun performSignPaymentRequest(token: String, complete: (CashAppSignOrderResult) -> Unit) {
22+
runCatching {
23+
signPayment(token)
24+
.onSuccess { response ->
25+
AfterpayCashAppJwt.decode(response.jwtToken)
26+
.onSuccess { jwtBody ->
27+
val cashApp = AfterpayCashApp(
28+
amount = jwtBody.amount.amount.toDouble(),
29+
redirectUri = jwtBody.redirectUrl,
30+
merchantId = jwtBody.externalMerchantId,
31+
brandId = response.externalBrandId,
32+
jwt = response.jwtToken,
33+
)
34+
35+
complete(CashAppSignOrderResult.Success(cashApp))
36+
}
37+
.onFailure {
38+
complete(CashAppSignOrderResult.Failure(it))
39+
}
40+
}
41+
.onFailure {
42+
complete(CashAppSignOrderResult.Failure(it))
43+
}
44+
}
45+
}
46+
47+
private suspend fun signPayment(token: String): Result<AfterpayCashAppSigningResponse> {
48+
return runCatching {
49+
val url = Afterpay.environment?.cashAppPaymentSigningUrl ?: throw Exception("No signing url found")
50+
val payload = """{ "token": "$token" }"""
51+
52+
val response = withContext(Dispatchers.IO) {
53+
AfterpayCashAppApi.cashRequest<AfterpayCashAppSigningResponse, String>(
54+
url = url,
55+
method = AfterpayCashAppApi.CashHttpVerb.POST,
56+
body = payload,
57+
)
58+
}.getOrThrow()
59+
60+
response
61+
}
62+
}
63+
64+
fun validatePayment(
65+
jwt: String,
66+
customerId: String,
67+
grantId: String,
68+
complete: (validationResponse: CashAppValidationResponse) -> Unit,
69+
) {
70+
return runBlocking {
71+
Afterpay.environment?.cashAppPaymentValidationUrl?.let { url ->
72+
val request = AfterpayCashAppValidationRequest(
73+
jwt = jwt,
74+
externalCustomerId = customerId,
75+
externalGrantId = grantId,
76+
)
77+
78+
val payload = Json.encodeToString(request)
79+
80+
val response = withContext(Dispatchers.Unconfined) {
81+
AfterpayCashAppApi.cashRequest<AfterpayCashAppValidationResponse, String>(
82+
url = url,
83+
method = AfterpayCashAppApi.CashHttpVerb.POST,
84+
body = payload,
85+
)
86+
}
87+
88+
response
89+
.onSuccess {
90+
when (it.status) {
91+
"SUCCESS" -> complete(CashAppValidationResponse.Success(it))
92+
else -> complete(CashAppValidationResponse.Failure(Exception("status is ${it.status}")))
93+
}
94+
}
95+
.onFailure {
96+
complete(CashAppValidationResponse.Failure(Exception(it.message)))
97+
}
98+
99+
Unit
100+
}
101+
} ?: complete(CashAppValidationResponse.Failure(Exception("environment not set")))
102+
}
103+
}

0 commit comments

Comments
 (0)