Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Connect SDK] Integrate with backend calls for client secret #9287

Merged
5 changes: 5 additions & 0 deletions stripe-connect-example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ apply plugin: 'org.jetbrains.kotlin.plugin.serialization'

dependencies {
implementation project(":stripe-connect")
implementation project(':stripe-core')

// Kotlin
implementation libs.kotlin.coroutines
implementation libs.kotlin.coroutinesAndroid
implementation libs.kotlin.serialization

// Networking
implementation libs.fuel
implementation libs.fuelCoroutines

// AndroidX
implementation libs.androidx.activity
implementation libs.androidx.annotation
Expand Down
18 changes: 18 additions & 0 deletions stripe-connect-example/dependencies/dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -629,9 +629,27 @@
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.25 (*)
| \--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.25
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.25 (*)
+--- project :stripe-core
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.25 (*)
| +--- androidx.browser:browser:1.7.0 (*)
| +--- com.google.dagger:dagger:2.50
| | \--- javax.inject:javax.inject:1
| +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*)
| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 (*)
| +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3 (*)
| +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 (*)
| \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.25 (*)
+--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*)
+--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 (*)
+--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3 (*)
+--- com.github.kittinunf.fuel:fuel:2.3.1
| +--- com.github.kittinunf.result:result:3.1.0
| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 1.9.25 (*)
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.10 -> 1.9.25 (*)
+--- com.github.kittinunf.fuel:fuel-coroutines:2.3.1
| +--- com.github.kittinunf.fuel:fuel:2.3.1 (*)
| +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9 -> 1.8.1 (*)
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.10 -> 1.9.25 (*)
+--- androidx.activity:activity-ktx:1.8.2 (*)
+--- androidx.annotation:annotation:1.7.1 -> 1.8.0 (*)
+--- androidx.appcompat:appcompat:1.6.1 (*)
Expand Down
10 changes: 7 additions & 3 deletions stripe-connect-example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application android:name=".App">
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -12,11 +16,11 @@
</activity>

<activity
android:name=".ui.accountonboarding.AccountOnboardingExampleActivity"
android:name=".ui.features.accountonboarding.AccountOnboardingExampleActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<activity
android:name=".ui.payouts.PayoutsExampleActivity"
android:name=".ui.features.payouts.PayoutsExampleActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
</application>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stripe.android.connectsdk.example

import android.app.Application
import android.os.StrictMode

class App : Application() {
override fun onCreate() {
super.onCreate()

StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectAll()
.penaltyLog()
.build()
)

StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.stripe.android.connectsdk.example.ui.accountonboarding.AccountOnboardingExampleActivity
import com.stripe.android.connectsdk.example.ui.payouts.PayoutsExampleActivity
import com.stripe.android.connectsdk.example.ui.features.accountonboarding.AccountOnboardingExampleActivity
import com.stripe.android.connectsdk.example.ui.features.payouts.PayoutsExampleActivity

class MainActivity : ComponentActivity() {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,89 @@
package com.stripe.android.connectsdk.example.networking

import kotlinx.coroutines.delay
import com.github.kittinunf.fuel.core.Deserializable
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.core.Request
import com.github.kittinunf.fuel.core.Response
import com.github.kittinunf.fuel.core.awaitResult
import com.github.kittinunf.result.Result
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

class EmbeddedComponentService {
suspend fun fetchClientSecret(): String? {
// TODO MXMOBILE-2511 - add backend call
delay(ARTIFICIAL_DELAY_MS)
return null
private val fuel = FuelManager.instance
.apply {
// add logging
addRequestInterceptor(RequestLogger(tag = "EmbeddedComponentService"))
addResponseInterceptor(ResponseLogger(tag = "EmbeddedComponentService"))

// add headers
addRequestInterceptor(ApplicationJsonHeaderInterceptor)
addRequestInterceptor(UserAgentHeader)
}

/**
* Returns the publishable key for use in the Stripe Connect SDK as well as a list
* of available merchants. Throws a [FuelError] exception on network issues and other errors.
*/
suspend fun getAccounts(): GetAccountsResponse {
return fuel.get(EXAMPLE_BACKEND_URL + "app_info")
.awaitModel(GetAccountsResponse.serializer())
.get()
}

/**
* Returns the client secret for the given merchant account to be used in the Stripe Connect SDK.
* Throws a [FuelError] exception on network issues and other errors.
*/
suspend fun fetchClientSecret(account: String): String {
return fuel.post(EXAMPLE_BACKEND_URL + "account_session")
.header("account", account)
.awaitModel(FetchClientSecretResponse.serializer())
.get()
.clientSecret
}

companion object {
private const val ARTIFICIAL_DELAY_MS = 3000L
private const val EXAMPLE_BACKEND_URL = "https://stripe-connect-mobile-example-v1.glitch.me/"
}
}

@Serializable
data class FetchClientSecretResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: IMO we should prefer having only one public class per file. It makes finding things a lot easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I'll refactor in a follow-up

@SerialName("client_secret")
val clientSecret: String
)

@Serializable
data class GetAccountsResponse(
@SerialName("publishable_key")
val publishableKey: String,
@SerialName("available_merchants")
val availableMerchants: List<Merchant>
)

@Serializable
data class Merchant(
@SerialName("merchant_id")
val merchantId: String,
@SerialName("display_name")
val displayName: String
)

suspend fun <T : Any> Request.awaitModel(
serializer: DeserializationStrategy<T>
): Result<T, FuelError> {
val deserializer = object : Deserializable<T> {

override fun deserialize(response: Response): T {
println(response.toString())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

val body = response.body().asString("application/json")
return Json.decodeFromString(serializer, body)
}
}

return awaitResult(deserializer)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.stripe.android.connectsdk.example.networking

import android.os.Build
import com.github.kittinunf.fuel.core.FoldableRequestInterceptor
import com.github.kittinunf.fuel.core.FoldableResponseInterceptor
import com.github.kittinunf.fuel.core.RequestTransformer
import com.github.kittinunf.fuel.core.ResponseTransformer
import com.github.kittinunf.fuel.core.extensions.cUrlString
import com.stripe.android.connectsdk.example.BuildConfig
import com.stripe.android.core.Logger
import com.stripe.android.core.version.StripeSdkVersion

object ApplicationJsonHeaderInterceptor : FoldableRequestInterceptor {
override fun invoke(next: RequestTransformer): RequestTransformer {
return { request ->
next(request.header("content-type", "application/json"))
}
}
}

object UserAgentHeader : FoldableRequestInterceptor {
private fun getUserAgent(): String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be a val

val androidBrand = Build.BRAND
val androidDevice = Build.MODEL
val osVersion = Build.VERSION.SDK_INT
return buildString {
append("Stripe/ConnectSDKExample")
append(" (Android $androidBrand $androidDevice; (OS Version $osVersion))+")
append(" Version/${StripeSdkVersion.VERSION_NAME}")
}
}

override fun invoke(next: RequestTransformer): RequestTransformer {
return { request ->
next(request.header("User-Agent", getUserAgent()))
}
}
}

class RequestLogger(
private val tag: String,
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG),
) : FoldableRequestInterceptor {
override fun invoke(next: RequestTransformer): RequestTransformer {
return { request ->
logger.info("($tag) Request: ${request.cUrlString()}")
next(request)
}
}
}

class ResponseLogger(
private val tag: String,
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG),
) : FoldableResponseInterceptor {
override fun invoke(next: ResponseTransformer): ResponseTransformer {
return { request, response ->
logger.info("($tag) Response: $response")
next(request, response)
}
}
}

This file was deleted.

This file was deleted.

Loading
Loading