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] Implement webview #9293

Draft
wants to merge 1 commit into
base: simond/connect-sdk-example-app-backend-integration
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AccountOnboardingExampleActivity : ComponentActivity() {
) {
if (sdkPublishableKey != null && accounts != null) {
val embeddedComponentManager = remember(sdkPublishableKey) {
EmbeddedComponentManager(
EmbeddedComponentManager.init(
activity = this@AccountOnboardingExampleActivity,
configuration = Configuration(sdkPublishableKey),
fetchClientSecret = viewModel::fetchClientSecret,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class PayoutsExampleActivity : ComponentActivity() {
) {
if (sdkPublishableKey != null && accounts != null) {
val embeddedComponentManager = remember(sdkPublishableKey) {
EmbeddedComponentManager(
EmbeddedComponentManager.init(
activity = this@PayoutsExampleActivity,
configuration = Configuration(sdkPublishableKey),
fetchClientSecret = viewModel::fetchClientSecret,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
Original file line number Diff line number Diff line change
@@ -1,17 +1,73 @@
package com.stripe.android.connectsdk

import StripeConnectWebViewClient
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Text

internal class EmbeddedComponentActivity internal constructor() : ComponentActivity() {
internal class EmbeddedComponentActivity : ComponentActivity() {

private lateinit var webView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.stripe_embedded_component_activity)

webView = findViewById(R.id.web_view)
setupWebView()
loadWebView()
}

@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
val stripeWebViewClient = StripeConnectWebViewClient()

webView.apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true

webViewClient = stripeWebViewClient
addJavascriptInterface(stripeWebViewClient.WebLoginJsInterfaceInternal(), "AndroidInternal")
addJavascriptInterface(stripeWebViewClient.WebLoginJsInterface(), "Android")
}
}

private fun loadWebView() {
val url = getUrl(intent)
webView.loadUrl(url)
}

@OptIn(PrivateBetaConnectSDK::class)
private fun getUrl(intent: Intent): String {
val component = intent.extras?.getSerializable(COMPONENT_EXTRA) as EmbeddedComponent
val configuration = intent.extras?.get(CONFIGURATION_EXTRA) as EmbeddedComponentManager.Configuration
return "https://connect-js.stripe.com/v1.0/android_webview.html#component=${component.urlName}" +
"&publicKey=${configuration.publishableKey}"
}

internal enum class EmbeddedComponent(val urlName: String) {
AccountOnboarding("account-onboarding"),
Payouts("payouts"),
}

companion object {
private const val COMPONENT_EXTRA = "component"
private const val CONFIGURATION_EXTRA = "configuration"

setContent {
Text("Not yet built...")
@OptIn(PrivateBetaConnectSDK::class)
internal fun newIntent(
activity: ComponentActivity,
component: EmbeddedComponent,
configuration: EmbeddedComponentManager.Configuration,
): Intent {
return Intent(activity, EmbeddedComponentActivity::class.java).apply {
putExtra(COMPONENT_EXTRA, component)
putExtra(CONFIGURATION_EXTRA, configuration)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,35 @@ import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.annotation.ColorInt
import androidx.annotation.RestrictTo
import com.stripe.android.connectsdk.EmbeddedComponentActivity.EmbeddedComponent
import kotlinx.parcelize.Parcelize

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class EmbeddedComponentManager internal constructor() {
constructor(
activity: ComponentActivity,
configuration: Configuration,
fetchClientSecret: FetchClientSecretCallback,
) : this() {
throw NotImplementedError("Not yet implemented")
}
class EmbeddedComponentManager private constructor(
private val activity: ComponentActivity,
private val configuration: Configuration,
internal val fetchClientSecret: FetchClientSecretCallback,
) {

@PrivateBetaConnectSDK
fun presentAccountOnboarding() {
throw NotImplementedError("Not yet implemented")
val intent = EmbeddedComponentActivity.newIntent(
activity,
EmbeddedComponent.AccountOnboarding,
configuration,
)
activity.startActivity(intent)
}

@PrivateBetaConnectSDK
fun presentPayouts() {
throw NotImplementedError("Not yet implemented")
val intent = EmbeddedComponentActivity.newIntent(
activity,
EmbeddedComponent.Payouts,
configuration,
)
activity.startActivity(intent)
}

@PrivateBetaConnectSDK
Expand All @@ -37,6 +45,23 @@ class EmbeddedComponentManager internal constructor() {
throw NotImplementedError("Not yet implemented")
}

companion object {
var instance: EmbeddedComponentManager? = null

fun init(
activity: ComponentActivity,
configuration: Configuration,
fetchClientSecret: FetchClientSecretCallback,
): EmbeddedComponentManager {
instance = EmbeddedComponentManager(
activity = activity,
configuration = configuration,
fetchClientSecret = fetchClientSecret
)
return instance!!
}
}

// Configuration

@Parcelize
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import android.graphics.Bitmap
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import com.stripe.android.connectsdk.EmbeddedComponentManager
import com.stripe.android.connectsdk.FetchClientSecretCallback
import com.stripe.android.connectsdk.PrivateBetaConnectSDK
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@OptIn(PrivateBetaConnectSDK::class)
internal class StripeConnectWebViewClient: WebViewClient() {

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
initJavascriptBridge(view!!)
}

private fun initJavascriptBridge(webView: WebView) {
webView.evaluateJavascript(
"""
Android.accountSessionClaimed = (message) => {
AndroidInternal.accountSessionClaimed(JSON.stringify(message));
};

Android.pageDidLoad = (message) => {
AndroidInternal.pageDidLoad(JSON.stringify(message));
};

Android.fetchClientSecret = () => {
return new Promise((resolve, reject) => {
try {
resolve(AndroidInternal.fetchClientSecret());
} catch (e) {
reject(e);
}
});
};

Android.onSetterFunctionCalled = (message) => {
AndroidInternal.onSetterFunctionCalled(JSON.stringify(message));
};

Android.openSecureWebView = (message) => {
AndroidInternal.openSecureWebView(JSON.stringify(message));
};
""".trimIndent(),
null
)
}

inner class WebLoginJsInterface {
@JavascriptInterface
fun debug(message: String) {
println("Debug log from JS: $message")
}

@JavascriptInterface
fun fetchInitParams() {
println("InitParams fetched")
}
}

inner class WebLoginJsInterfaceInternal {
@JavascriptInterface
fun log(message: String) {
println("Log from JS: $message")
}

@JavascriptInterface
fun onSetterFunctionCalled(message: String) {
val setterMessage = Json.decodeFromString<SetterMessage>(message)
println("Setter function called: $setterMessage")
}

@JavascriptInterface
fun openSecureWebView(message: String) {
val secureWebViewData = Json.decodeFromString<SecureWebViewMessage>(message)
println("Open secure web view with data: $secureWebViewData")
}

@JavascriptInterface
fun pageDidLoad(message: String) {
val pageLoadMessage = Json.decodeFromString<PageLoadMessage>(message)
println("Page did load: $pageLoadMessage")
}

@JavascriptInterface
fun accountSessionClaimed(message: String) {
val accountSessionClaimedMessage = Json.decodeFromString<AccountSessionClaimedMessage>(message)
println("Account session claimed: $accountSessionClaimedMessage")
}

@JavascriptInterface
fun fetchClientSecret(): String {
return runBlocking {
suspendCancellableCoroutine { continuation ->
EmbeddedComponentManager.instance!!.fetchClientSecret.fetchClientSecret(
object : FetchClientSecretCallback.ClientSecretResultCallback {
override fun onResult(secret: String) {
continuation.resume(secret)
}

override fun onError() {
continuation.resumeWithException(Exception("Failed to fetch client secret"))
}
}
)
}
}
}
}

@Serializable
data class AccountSessionClaimedMessage(
val merchantId: String
)

@Serializable
data class PageLoadMessage(
val pageViewId: String
)

@Serializable
data class SetterMessage(
val setter: String,
val value: String,
)

@Serializable
data class SecureWebViewMessage(
val id: String,
val url: String
)
}
Loading