Skip to content

Commit 0931ac1

Browse files
Create financial-connections-lite module (#10385)
* Adds fc-core module. * Removes test file. * Regenerates APIs. * Removes unneeded deps. * Style fixes. * Reverts unneeded changes. * ktlint fixes. * Moves contracts to core. * Cleans imports. * Updates dependencies. * Style fixes. * Adds basic module. * Updates depenencies. * Fixes tests. * Fixes tests. * Adds webview. * Removes lite contract. * Adds sync call. * Reverts wrong changes. * Updates API. * Updates dependencies. * Update financial-connections-lite/build.gradle Co-authored-by: Till Hellmund <[email protected]> * Update financial-connections-lite/src/main/java/com/stripe/android/financialconnections/lite/FinancialConnectionsLiteViewModel.kt Co-authored-by: Till Hellmund <[email protected]> * Update financial-connections-lite/src/main/java/com/stripe/android/financialconnections/lite/repository/FinancialConnectionsLiteRepository.kt Co-authored-by: Till Hellmund <[email protected]> * PR feedback. --------- Co-authored-by: Till Hellmund <[email protected]>
1 parent 6271344 commit 0931ac1

File tree

18 files changed

+785
-1
lines changed

18 files changed

+785
-1
lines changed

financial-connections-core/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt

+5
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ sealed class FinancialConnectionsSheetActivityArgs(
5555

5656
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
5757
fun isValid(): Boolean = runCatching { validate() }.isSuccess
58+
59+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
60+
companion object {
61+
const val EXTRA_ARGS = "FinancialConnectionsSheetActivityArgs"
62+
}
5863
}

financial-connections-lite/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public final class com/stripe/android/financialconnections/lite/BuildConfig {
2+
public static final field BUILD_TYPE Ljava/lang/String;
3+
public static final field DEBUG Z
4+
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
5+
public fun <init> ()V
6+
}
7+
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
apply from: configs.androidLibrary
2+
3+
apply plugin: 'checkstyle'
4+
apply plugin: 'org.jetbrains.kotlin.plugin.parcelize'
5+
apply plugin: 'kotlinx-serialization'
6+
apply plugin: 'com.google.devtools.ksp'
7+
8+
android {
9+
defaultConfig {
10+
testApplicationId "com.stripe.android.financialconnections.test"
11+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
12+
}
13+
testOptions {
14+
unitTests {
15+
includeAndroidResources = true
16+
}
17+
}
18+
}
19+
20+
dependencies {
21+
api project(":financial-connections-core")
22+
23+
implementation libs.androidx.activity
24+
implementation libs.androidx.fragment
25+
implementation libs.androidx.lifecycle
26+
implementation libs.androidx.annotation
27+
implementation libs.androidx.browser
28+
implementation libs.androidx.viewModel
29+
implementation libs.dagger
30+
implementation libs.kotlin.coroutines
31+
implementation libs.kotlin.serialization
32+
33+
testImplementation testLibs.json
34+
testImplementation testLibs.junit
35+
36+
androidTestUtil testLibs.testOrchestrator
37+
}
38+
39+
android {
40+
kotlinOptions {
41+
freeCompilerArgs += [
42+
"-opt-in=kotlin.RequiresOptIn",
43+
"-Xconsistent-data-class-copy-visibility",
44+
]
45+
}
46+
}
47+
48+
ext {
49+
artifactId = "financial-connections-lite"
50+
artifactName = "financial-connections-lite"
51+
artifactDescrption = "The Financial Connections Lite module of Stripe Android SDK"
52+
}
53+
54+
apply from: "${rootDir}/deploy/deploy.gradle"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# We don't directly reference enum fields annotated with @Serializable
2+
-keep @kotlinx.serialization.Serializable enum com.stripe.android.financialconnections.** {
3+
*;
4+
}
5+

financial-connections-lite/dependencies/dependencies.txt

+347
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<SmellBaseline>
3+
<ManuallySuppressedIssues/>
4+
<CurrentIssues />
5+
</SmellBaseline>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
STRIPE_ANDROID_NAMESPACE=com.stripe.android.financialconnections.lite
2+
enable_dokka=true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<application>
5+
<activity android:name=".FinancialConnectionsSheetLiteActivity" />
6+
</application>
7+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.stripe.android.financialconnections.lite
2+
3+
import android.content.Context
4+
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.ViewModel
6+
import androidx.lifecycle.ViewModelProvider
7+
import androidx.lifecycle.createSavedStateHandle
8+
import androidx.lifecycle.viewModelScope
9+
import androidx.lifecycle.viewmodel.CreationExtras
10+
import com.stripe.android.core.Logger
11+
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs
12+
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.Companion.EXTRA_ARGS
13+
import com.stripe.android.financialconnections.lite.FinancialConnectionsLiteViewModel.ViewEffect.OpenAuthFlowWithUrl
14+
import com.stripe.android.financialconnections.lite.di.Di
15+
import com.stripe.android.financialconnections.lite.repository.FinancialConnectionsLiteRepository
16+
import kotlinx.coroutines.CoroutineDispatcher
17+
import kotlinx.coroutines.flow.MutableSharedFlow
18+
import kotlinx.coroutines.flow.SharedFlow
19+
import kotlinx.coroutines.launch
20+
21+
internal class FinancialConnectionsLiteViewModel(
22+
private val logger: Logger,
23+
private val savedStateHandle: SavedStateHandle,
24+
private val repository: FinancialConnectionsLiteRepository,
25+
workContext: CoroutineDispatcher,
26+
applicationId: String
27+
) : ViewModel() {
28+
29+
private val args: FinancialConnectionsSheetActivityArgs
30+
get() = savedStateHandle.get<FinancialConnectionsSheetActivityArgs>(EXTRA_ARGS)!!
31+
32+
private val _viewEffects = MutableSharedFlow<ViewEffect>()
33+
val viewEffects: SharedFlow<ViewEffect>
34+
get() = _viewEffects
35+
36+
init {
37+
viewModelScope.launch(workContext) {
38+
repository.synchronize(
39+
configuration = args.configuration,
40+
applicationId = applicationId
41+
).onSuccess { sync ->
42+
_viewEffects.emit(
43+
OpenAuthFlowWithUrl(requireNotNull(sync.manifest.hostedAuthUrl))
44+
)
45+
}.onFailure { throwable ->
46+
// TODO - handle error state
47+
logger.error("Failed to synchronize session", throwable)
48+
}
49+
}
50+
}
51+
52+
internal sealed class ViewEffect {
53+
data class OpenAuthFlowWithUrl(val url: String) : ViewEffect()
54+
}
55+
56+
class Factory : ViewModelProvider.Factory {
57+
58+
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
59+
val savedStateHandle = extras.createSavedStateHandle()
60+
val appContext = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Context
61+
62+
if (modelClass.isAssignableFrom(FinancialConnectionsLiteViewModel::class.java)) {
63+
@Suppress("UNCHECKED_CAST")
64+
return FinancialConnectionsLiteViewModel(
65+
savedStateHandle = savedStateHandle,
66+
applicationId = appContext.packageName,
67+
logger = Di.logger,
68+
workContext = Di.workContext,
69+
repository = Di.repository()
70+
) as T
71+
}
72+
throw IllegalArgumentException("Unknown ViewModel class")
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.stripe.android.financialconnections.lite
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.os.Bundle
7+
import android.webkit.WebView
8+
import android.widget.FrameLayout
9+
import androidx.activity.ComponentActivity
10+
import androidx.activity.viewModels
11+
import androidx.lifecycle.lifecycleScope
12+
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs
13+
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.Companion.EXTRA_ARGS
14+
import com.stripe.android.financialconnections.lite.FinancialConnectionsLiteViewModel.ViewEffect.OpenAuthFlowWithUrl
15+
import kotlinx.coroutines.launch
16+
17+
internal class FinancialConnectionsSheetLiteActivity : ComponentActivity() {
18+
19+
private lateinit var webView: WebView
20+
21+
private val viewModel: FinancialConnectionsLiteViewModel by viewModels {
22+
FinancialConnectionsLiteViewModel.Factory()
23+
}
24+
25+
@SuppressLint("SetJavaScriptEnabled")
26+
override fun onCreate(savedInstanceState: Bundle?) {
27+
super.onCreate(savedInstanceState)
28+
29+
val args = getArgs(intent)
30+
if (args == null) {
31+
finish()
32+
return
33+
}
34+
35+
val frameLayout = FrameLayout(this)
36+
webView = setupWebView()
37+
frameLayout.addView(webView)
38+
setContentView(frameLayout)
39+
40+
lifecycleScope.launch {
41+
viewModel.viewEffects.collect { viewEffect ->
42+
when (viewEffect) {
43+
is OpenAuthFlowWithUrl -> webView.loadUrl(viewEffect.url)
44+
}
45+
}
46+
}
47+
}
48+
49+
@SuppressLint("SetJavaScriptEnabled")
50+
private fun setupWebView(): WebView {
51+
return WebView(this).also {
52+
val webSettings = it.settings
53+
webSettings.javaScriptEnabled = true
54+
webSettings.useWideViewPort = true
55+
webSettings.loadWithOverviewMode = true
56+
}
57+
}
58+
59+
companion object {
60+
fun intent(context: Context, args: FinancialConnectionsSheetActivityArgs): Intent {
61+
return Intent(context, FinancialConnectionsSheetLiteActivity::class.java).apply {
62+
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
63+
putExtra(EXTRA_ARGS, args)
64+
}
65+
}
66+
67+
fun getArgs(intent: Intent): FinancialConnectionsSheetActivityArgs? {
68+
return intent.getParcelableExtra(EXTRA_ARGS)
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.stripe.android.financialconnections.lite.di
2+
3+
import com.stripe.android.core.ApiVersion
4+
import com.stripe.android.core.BuildConfig
5+
import com.stripe.android.core.Logger
6+
import com.stripe.android.core.networking.ApiRequest
7+
import com.stripe.android.core.networking.DefaultStripeNetworkClient
8+
import com.stripe.android.financialconnections.lite.network.FinancialConnectionsLiteRequestExecutor
9+
import com.stripe.android.financialconnections.lite.repository.FinancialConnectionsLiteRepository
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.serialization.json.Json
12+
13+
internal object Di {
14+
private val apiVersion = ApiVersion(
15+
betas = setOf("financial_connections_client_api_beta=v1")
16+
)
17+
private val apiRequestFactory = ApiRequest.Factory(
18+
apiVersion = apiVersion.code
19+
)
20+
21+
private val json = Json {
22+
ignoreUnknownKeys = true
23+
}
24+
25+
val workContext = Dispatchers.IO
26+
val logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG)
27+
28+
fun repository(): FinancialConnectionsLiteRepository = FinancialConnectionsLiteRepository(
29+
requestExecutor = FinancialConnectionsLiteRequestExecutor(
30+
stripeNetworkClient = DefaultStripeNetworkClient(
31+
workContext = workContext,
32+
logger = logger
33+
),
34+
json = json,
35+
logger = logger
36+
),
37+
apiRequestFactory = apiRequestFactory
38+
)
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.stripe.android.financialconnections.lite.network
2+
3+
import com.stripe.android.core.Logger
4+
import com.stripe.android.core.exception.APIConnectionException
5+
import com.stripe.android.core.exception.APIException
6+
import com.stripe.android.core.exception.AuthenticationException
7+
import com.stripe.android.core.exception.InvalidRequestException
8+
import com.stripe.android.core.exception.PermissionException
9+
import com.stripe.android.core.exception.RateLimitException
10+
import com.stripe.android.core.model.parsers.StripeErrorJsonParser
11+
import com.stripe.android.core.networking.HTTP_TOO_MANY_REQUESTS
12+
import com.stripe.android.core.networking.StripeNetworkClient
13+
import com.stripe.android.core.networking.StripeRequest
14+
import com.stripe.android.core.networking.StripeResponse
15+
import com.stripe.android.core.networking.responseJson
16+
import kotlinx.serialization.KSerializer
17+
import kotlinx.serialization.json.Json
18+
import java.net.HttpURLConnection
19+
import javax.inject.Inject
20+
21+
internal class FinancialConnectionsLiteRequestExecutor @Inject constructor(
22+
private val stripeNetworkClient: StripeNetworkClient,
23+
private val json: Json,
24+
private val logger: Logger,
25+
) {
26+
suspend fun <Response> execute(
27+
request: StripeRequest,
28+
responseSerializer: KSerializer<Response>
29+
): Result<Response> {
30+
return executeInternal(request) { body ->
31+
runCatching {
32+
json.decodeFromString(responseSerializer, body)
33+
}
34+
}
35+
}
36+
37+
private suspend fun <Response> executeInternal(
38+
request: StripeRequest,
39+
decodeResponse: (String) -> Result<Response>,
40+
): Result<Response> {
41+
logger.debug("Executing ${request.method.code} request to ${request.url}")
42+
return runCatching {
43+
stripeNetworkClient.executeRequest(request)
44+
}.mapCatching { response ->
45+
if (response.isError) {
46+
throw handleApiError(response)
47+
} else {
48+
decodeResponse(requireNotNull(response.body)).getOrThrow()
49+
}
50+
}.recoverCatching {
51+
throw APIConnectionException("Failed to execute $request", cause = it)
52+
}
53+
}
54+
55+
private fun handleApiError(response: StripeResponse<String>): Exception {
56+
val requestId = response.requestId?.value
57+
val responseCode = response.code
58+
val stripeError = StripeErrorJsonParser().parse(response.responseJson())
59+
60+
return when (responseCode) {
61+
HttpURLConnection.HTTP_ACCEPTED,
62+
HttpURLConnection.HTTP_BAD_REQUEST,
63+
HttpURLConnection.HTTP_NOT_FOUND -> InvalidRequestException(
64+
stripeError,
65+
requestId,
66+
responseCode
67+
)
68+
HttpURLConnection.HTTP_UNAUTHORIZED -> AuthenticationException(stripeError, requestId)
69+
HttpURLConnection.HTTP_FORBIDDEN -> PermissionException(stripeError, requestId)
70+
HTTP_TOO_MANY_REQUESTS -> RateLimitException(stripeError, requestId)
71+
else -> APIException(stripeError, requestId, responseCode)
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)