Skip to content

Commit

Permalink
Refactor permission, handle within manager
Browse files Browse the repository at this point in the history
  • Loading branch information
simond-stripe committed Nov 18, 2024
1 parent 16db6f3 commit c7afc92
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.connect.example.data

import androidx.appcompat.app.AppCompatActivity
import com.github.kittinunf.fuel.core.FuelError
import com.stripe.android.connect.EmbeddedComponentManager
import com.stripe.android.connect.FetchClientSecretCallback.ClientSecretResultCallback
Expand Down Expand Up @@ -33,7 +34,7 @@ class EmbeddedComponentManagerProvider @Inject constructor(
* Provides the EmbeddedComponentManager instance, creating it if it doesn't exist.
* Throws [IllegalStateException] if an EmbeddedComponentManager cannot be created at this time.
*/
fun provideEmbeddedComponentManager(): EmbeddedComponentManager {
fun provideEmbeddedComponentManager(activity: AppCompatActivity): EmbeddedComponentManager {
if (embeddedComponentManager != null) {
return embeddedComponentManager!!
}
Expand All @@ -43,6 +44,7 @@ class EmbeddedComponentManagerProvider @Inject constructor(
?: throw IllegalStateException("Publishable key must be set before creating EmbeddedComponentManager")

return EmbeddedComponentManager(
activity = activity,
configuration = EmbeddedComponentManager.Configuration(
publishableKey = publishableKey,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand All @@ -29,30 +30,22 @@ import javax.inject.Inject

@OptIn(PrivateBetaConnectSDK::class)
@AndroidEntryPoint
class PayoutsExampleActivity : FragmentActivity() {
class PayoutsExampleActivity : AppCompatActivity() {

@Inject lateinit var embeddedComponentManagerProvider: EmbeddedComponentManagerProvider
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG)
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private val requestPermissionFlow: MutableSharedFlow<Boolean> = MutableSharedFlow()


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val embeddedComponentManager = try {
embeddedComponentManagerProvider.provideEmbeddedComponentManager()
embeddedComponentManagerProvider.provideEmbeddedComponentManager(this)
} catch (e: IllegalStateException) {
// TODO - handle app restoration more gracefully
logger.error("(PayoutsExampleActivity) Error retrieving EmbeddedComponentManager: $e")
finish() // we don't have an embedded component manager, so go back to MainActivity to get one
return
}
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
MainScope().launch {
requestPermissionFlow.emit(isGranted)
}
}


setContent {
ConnectSdkExampleTheme {
Expand All @@ -79,10 +72,7 @@ class PayoutsExampleActivity : FragmentActivity() {
) {
BackHandler(onBack = onDismiss)
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
embeddedComponentManager.createPayoutsView(context) { permission ->
requestPermissionLauncher.launch(permission)
requestPermissionFlow.first() // wait for the next result
}
embeddedComponentManager.createPayoutsView(this@PayoutsExampleActivity)
})
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
package com.stripe.android.connect

import android.content.Context
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.ColorInt
import androidx.annotation.RestrictTo
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.parcelize.Parcelize
import kotlin.coroutines.resume

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class EmbeddedComponentManager(
activity: AppCompatActivity,
private val configuration: Configuration,
private val fetchClientSecretCallback: FetchClientSecretCallback,
) {

private val permissionsFlow: MutableSharedFlow<Boolean> = MutableSharedFlow()
private val requestPermissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
MainScope().launch {
permissionsFlow.emit(isGranted)
}
}

internal suspend fun requestCameraPermission(): Boolean {
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
return permissionsFlow.first()
}

/**
* Create a new [PayoutsView] for inclusion in the view hierarchy.
*/
fun createPayoutsView(context: Context, requestPermissionFromUser: suspend (String) -> Boolean): PayoutsView {
fun createPayoutsView(activity: AppCompatActivity): PayoutsView {
return PayoutsView(
context = context,
context = activity,
embeddedComponentManager = this,
requestPermissionFromUser = requestPermissionFromUser,
)
}

Expand Down
16 changes: 4 additions & 12 deletions connect/src/main/java/com/stripe/android/connect/PayoutsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class PayoutsView @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
embeddedComponentManager: EmbeddedComponentManager? = null,
requestPermissionFromUser: (suspend (String) -> Boolean)? = null,
) : FrameLayout(context, attrs, defStyleAttr) {

private var stripeWebViewClient: StripeConnectWebViewClient? = null
Expand All @@ -23,7 +22,7 @@ class PayoutsView @JvmOverloads constructor(
inflate(getContext(), R.layout.stripe_connect_webview, this)

embeddedComponentManager?.let {
configureWebView(it, requestPermissionFromUser ?: { false })
configureWebView(it)
}
}

Expand All @@ -32,25 +31,18 @@ class PayoutsView @JvmOverloads constructor(
* Must be called when this view is created via XML.
* Cannot be called more than once per instance.
*/
fun setEmbeddedComponentManager(
embeddedComponentManager: EmbeddedComponentManager,
requestPermissionFromUser: suspend (String) -> Boolean = { false },
) {
fun setEmbeddedComponentManager(embeddedComponentManager: EmbeddedComponentManager) {
if (stripeWebViewClient != null) {
throw IllegalStateException("EmbeddedComponentManager already set")
}
configureWebView(embeddedComponentManager, requestPermissionFromUser)
configureWebView(embeddedComponentManager)
}

private fun configureWebView(
embeddedComponentManager: EmbeddedComponentManager,
requestPermissionFromUser: suspend (String) -> Boolean,
) {
private fun configureWebView(embeddedComponentManager: EmbeddedComponentManager) {
val webView = findViewById<WebView>(R.id.stripe_web_view)
stripeWebViewClient = StripeConnectWebViewClient(
embeddedComponentManager = embeddedComponentManager,
connectComponent = StripeEmbeddedComponent.PAYOUTS,
requestPermissionFromUser = requestPermissionFromUser,
).apply {
configureAndLoadWebView(webView)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,58 @@ import android.content.Context
import android.content.pm.PackageManager
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.checkSelfPermission
import kotlinx.coroutines.CoroutineScope
import androidx.lifecycle.LifecycleCoroutineScope
import com.stripe.android.connect.EmbeddedComponentManager
import com.stripe.android.connect.PrivateBetaConnectSDK
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* A [WebChromeClient] that handles permission requests for the Stripe Connect WebView.
* This should be used in conjunction with [StripeConnectWebViewClient].
*/
@OptIn(PrivateBetaConnectSDK::class)
internal class StripeConnectWebChromeClient(
private val context: Context,
private val requestPermissionFromUser: suspend (String) -> Boolean,
private val permissionScopeBuilder: () -> CoroutineScope = { CoroutineScope(Dispatchers.Default) },
private val embeddedComponentManager: EmbeddedComponentManager,
private val viewScope: () -> LifecycleCoroutineScope,
) : WebChromeClient() {

private val inProgressRequests: MutableMap<PermissionRequest, CoroutineScope> = mutableMapOf()
private val inProgressRequests: MutableMap<PermissionRequest, Job> = mutableMapOf()

override fun onPermissionRequest(request: PermissionRequest) {
if (!request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
request.deny()
// we only care about camera permissions at this time (video/audio)
val permissionsRequested = request.resources.filter {
it in listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE, PermissionRequest.RESOURCE_AUDIO_CAPTURE)
}.toTypedArray()
if (permissionsRequested.isEmpty()) {
request.deny() // no supported permissions were requested, so reject the request
return
}

if (checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
request.grant(permissionsRequested)
} else {
val scope = permissionScopeBuilder().also {
inProgressRequests[request] = it
}
scope.launch {
val isGranted = requestPermissionFromUser(Manifest.permission.CAMERA)
val job = viewScope().launch {
val isGranted = embeddedComponentManager.requestCameraPermission()
withContext(Dispatchers.Main) {
if (isGranted) {
request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
request.grant(permissionsRequested)
} else {
request.deny()
}
}
inProgressRequests.remove(request)
}
inProgressRequests[request] = job
}
}

override fun onPermissionRequestCanceled(request: PermissionRequest?) {
inProgressRequests[request]?.cancel()
if (request == null) return
inProgressRequests.remove(request)?.also { it.cancel() }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.stripe.android.connect.webview

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.stripe.android.connect.BuildConfig
import com.stripe.android.connect.EmbeddedComponentManager
import com.stripe.android.connect.PrivateBetaConnectSDK
Expand All @@ -22,7 +21,6 @@ import kotlinx.serialization.json.Json
internal class StripeConnectWebViewClient(
private val embeddedComponentManager: EmbeddedComponentManager,
private val connectComponent: StripeEmbeddedComponent,
private val requestPermissionFromUser: suspend (String) -> Boolean,
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG),
private val jsonSerializer: Json = Json {
ignoreUnknownKeys = true
Expand All @@ -37,7 +35,11 @@ internal class StripeConnectWebViewClient(
fun configureAndLoadWebView(webView: WebView) {
webView.apply {
webViewClient = this@StripeConnectWebViewClient
webChromeClient = StripeConnectWebChromeClient(context, requestPermissionFromUser)
webChromeClient = StripeConnectWebChromeClient(
context = context,
embeddedComponentManager = embeddedComponentManager,
viewScope = { webView.findViewTreeLifecycleOwner()?.lifecycleScope!! },
)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
Expand Down

0 comments on commit c7afc92

Please sign in to comment.