Skip to content

Commit

Permalink
[Connect SDK] Add camera support to the Connect SDK (#9607)
Browse files Browse the repository at this point in the history
* Initial camera implementation

# Conflicts:
#	connect-example/src/main/java/com/stripe/android/connect/example/ui/features/payouts/PayoutsExampleActivity.kt
#	connect/src/main/java/com/stripe/android/connect/PayoutsView.kt

# Conflicts:
#	connect/src/main/java/com/stripe/android/connect/PayoutsView.kt
#	connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewClient.kt

# Conflicts:
#	connect-example/src/main/java/com/stripe/android/connect/example/ui/features/payouts/PayoutsExampleActivity.kt
#	connect/src/main/java/com/stripe/android/connect/EmbeddedComponentManager.kt
#	connect/src/main/java/com/stripe/android/connect/PayoutsView.kt

* Update request code

* Update example activity

* Refactor permission, handle within manager

* Fix leftover merge conflicts

# Conflicts:
#	connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt

* WIP

* Fix merge

* Fix build issues

* fix activity

* Fix suspend function

* refactor with activity oncreate

* Show promo badge in bank form (#9734)

* Show promo badge in bank form

* Address code review feedback

Fix layout issue with super-long bank name and validate with screenshot test.

* Add Embedded Appearance params to AppearanceBottomSheetDialogFragment (#9727)

* Add Embedded Appearance params to AppearanceBottomSheetDialogFragment

* [MOBILESDK-2480]update text style for bacs secondary button type (#9745)

* update text style for bacs secondary button type

* screenshots for screenshot tests

* Apply suggestions from code review

Capitalize comment and add period

Co-authored-by: Bella Koch <160939932+amk-stripe@users.noreply.github.com>

* crypto payment method (#9753)

* Better handle process death for all confirmation flow cases in `DefaultConfirmationHandler` (#9754)

* Fix lints, remove unneeded cancellation function

* Add tests

* Fix tests

* Add/fix logging

* Fix lint from log line

* Fix main thread

* Update connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt

Co-authored-by: lng-stripe <91862945+lng-stripe@users.noreply.github.com>

* Update connect/src/main/java/com/stripe/android/connect/EmbeddedComponentManager.kt

Co-authored-by: lng-stripe <91862945+lng-stripe@users.noreply.github.com>

* unsupported behavior to log warning

* Fix docstrings

* Update crashing behavior for camera permission request

* Move functions around for clarity

* Update tests

* Update and fix tests

* Fix detekt lints

* Fix main activity

* Remove unnecessary fragment, move logger out of constructor

* Clean up lib restriction

* Remove unnecessary unconfined test dispatcher

* Update api

* Lint fixes

---------

Co-authored-by: Till Hellmund <tillh@stripe.com>
Co-authored-by: tjclawson-stripe <163896025+tjclawson-stripe@users.noreply.github.com>
Co-authored-by: Tian Zhao <tianzhao@stripe.com>
Co-authored-by: Bella Koch <160939932+amk-stripe@users.noreply.github.com>
Co-authored-by: ericzhang-stripe <94195995+ericzhang-stripe@users.noreply.github.com>
Co-authored-by: Samer Alabi <141707240+samer-stripe@users.noreply.github.com>
Co-authored-by: lng-stripe <91862945+lng-stripe@users.noreply.github.com>
  • Loading branch information
8 people authored Dec 12, 2024
1 parent 82489c2 commit 3d66b3c
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.stripe.android.connect.EmbeddedComponentManager
import com.stripe.android.connect.PrivateBetaConnectSDK
import com.stripe.android.connect.example.core.safeNavigateUp
import com.stripe.android.connect.example.ui.common.ConnectSdkExampleTheme
import com.stripe.android.connect.example.ui.componentpicker.ComponentPickerContent
Expand All @@ -15,12 +17,15 @@ import com.stripe.android.connect.example.ui.settings.SettingsView
import com.stripe.android.connect.example.ui.settings.SettingsViewModel
import dagger.hilt.android.AndroidEntryPoint

@OptIn(PrivateBetaConnectSDK::class)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

EmbeddedComponentManager.onActivityCreate(this@MainActivity)

setContent {
val viewModel = hiltViewModel<EmbeddedComponentLoaderViewModel>()
val navController = rememberNavController()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class EmbeddedComponentService @Inject constructor(
* Returns the publishable key for use in the Stripe Connect SDK.
* Throws a [FuelError] exception on network issues and other errors.
*/
suspend fun loadPublishableKey(): GetAccountsResponse = getAccounts()
suspend fun loadPublishableKey(): String = getAccounts().publishableKey

/**
* Returns the client secret for the given merchant account to be used in the Stripe Connect SDK.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ abstract class BasicExampleComponentActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

EmbeddedComponentManager.onActivityCreate(this@BasicExampleComponentActivity)

setContent {
BackHandler(onBack = ::finish)
val viewModel = hiltViewModel<EmbeddedComponentLoaderViewModel>()
Expand Down
5 changes: 5 additions & 0 deletions connect/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
package com.stripe.android.connect

import android.Manifest
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.checkSelfPermission
import com.stripe.android.connect.appearance.Appearance
import com.stripe.android.connect.appearance.fonts.CustomFontSource
import com.stripe.android.connect.util.findActivity
import com.stripe.android.connect.webview.serialization.ConnectInstanceJs
import com.stripe.android.connect.webview.serialization.toJs
import com.stripe.android.core.Logger
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.parcelize.Parcelize
import kotlin.coroutines.resume
Expand All @@ -25,13 +39,27 @@ class EmbeddedComponentManager(
private val _appearanceFlow = MutableStateFlow(appearance)
internal val appearanceFlow: StateFlow<Appearance> get() = _appearanceFlow.asStateFlow()

private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG)
private val isDebugBuild: Boolean = BuildConfig.DEBUG
private val loggerTag = javaClass.simpleName

// Public functions

/**
* Create a new [AccountOnboardingView] for inclusion in the view hierarchy.
*/
@PrivateBetaConnectSDK
fun createAccountOnboardingView(
context: Context,
listener: AccountOnboardingListener? = null
listener: AccountOnboardingListener? = null,
): AccountOnboardingView {
val activity = checkNotNull(context.findActivity()) {
"You must create an AccountOnboardingView from an Activity"
}
checkNotNull(launcherMap[activity]) {
"You must call EmbeddedComponentManager.onActivityCreate in your Activity.onCreate function"
}

return AccountOnboardingView(
context = context,
embeddedComponentManager = this,
Expand All @@ -42,17 +70,37 @@ class EmbeddedComponentManager(
/**
* Create a new [PayoutsView] for inclusion in the view hierarchy.
*/
@PrivateBetaConnectSDK
fun createPayoutsView(
context: Context,
listener: PayoutsListener? = null
listener: PayoutsListener? = null,
): PayoutsView {
val activity = checkNotNull(context.findActivity()) {
"You must create a PayoutsView from an Activity"
}
checkNotNull(launcherMap[activity]) {
"You must call EmbeddedComponentManager.onActivityCreate in your Activity.onCreate function"
}

return PayoutsView(
context = context,
embeddedComponentManager = this,
listener = listener
listener = listener,
)
}

@PrivateBetaConnectSDK
fun update(appearance: Appearance) {
_appearanceFlow.value = appearance
}

@PrivateBetaConnectSDK
fun logout() {
throw NotImplementedError("Logout functionality is not yet implemented")
}

// Internal functions (not for public consumption)

internal fun getInitialParams(context: Context): ConnectInstanceJs {
return ConnectInstanceJs(
appearance = _appearanceFlow.value.toJs(),
Expand Down Expand Up @@ -85,24 +133,93 @@ class EmbeddedComponentManager(
}
}

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun update(appearance: Appearance) {
_appearanceFlow.value = appearance
}
/**
* Requests camera permissions for the EmbeddedComponents. Returns true if the user grants permission, false if the
* user denies permission, and null if the request cannot be completed.
*
* This function may result in a permissions pop-up being shown to the user (although this may not always
* happen, such as when the permission has already granted).
*/
internal suspend fun requestCameraPermission(context: Context): Boolean? {
if (checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
logger.debug("($loggerTag) Skipping permission request - CAMERA permission already granted")
return true
}

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun logout() {
throw NotImplementedError("Logout functionality is not yet implemented")
val activity = context.findActivity()
if (activity == null) {
logger.warning("($loggerTag) You must create the EmbeddedComponent view from an Activity")
if (isDebugBuild) {
// crash if in debug mode so that developers are more likely to catch this error.
error("You must create an AccountOnboardingView from an Activity")
}
}
val launcher = launcherMap[activity]
if (launcher == null) {
logger.warning(
"($loggerTag) Error launching camera permission request. " +
"Did you call EmbeddedComponentManager.onActivityCreate in your Activity.onCreate function?"
)
return null
}
launcher.launch(Manifest.permission.CAMERA)

return permissionsFlow.first()
}

// Configuration

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class Configuration(
val publishableKey: String,
) : Parcelable

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
companion object {
@VisibleForTesting
internal val permissionsFlow: MutableSharedFlow<Boolean> = MutableSharedFlow(extraBufferCapacity = 1)
private val launcherMap = mutableMapOf<Activity, ActivityResultLauncher<String>>()

/**
* Hooks the [EmbeddedComponentManager] into this activity's lifecycle.
*
* Must be called in [ComponentActivity.onCreate], passing in the instance of the
* activity as [activity]. This must be called in all activities where an EmbeddedComponent
* view is used.
*/
fun onActivityCreate(activity: ComponentActivity) {
val application = activity.application

application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityDestroyed(destroyedActivity: Activity) {
// ensure we remove the activity and its launcher from our map, and unregister
// this activity from future callbacks
launcherMap.remove(destroyedActivity)
if (destroyedActivity == activity) {
application.unregisterActivityLifecycleCallbacks(this)
}
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { /* no-op */ }

override fun onActivityStarted(activity: Activity) { /* no-op */ }

override fun onActivityResumed(activity: Activity) { /* no-op */ }

override fun onActivityPaused(activity: Activity) { /* no-op */ }

override fun onActivityStopped(activity: Activity) { /* no-op */ }

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { /* no-op */ }
})

launcherMap[activity] = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
permissionsFlow.tryEmit(isGranted)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.annotation.RestrictTo
fun interface FetchClientSecretCallback {

@PrivateBetaConnectSDK
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun fetchClientSecret(resultCallback: ClientSecretResultCallback)

@PrivateBetaConnectSDK
Expand Down
16 changes: 16 additions & 0 deletions connect/src/main/java/com/stripe/android/connect/util/CoreUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stripe.android.connect.util

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper

/**
* Returns the [Activity] that this [Context] is attached to, or null if none.
*/
internal fun Context.findActivity(): Activity? {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.graphics.Bitmap
import android.os.Build
import android.view.LayoutInflater
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
Expand Down Expand Up @@ -89,7 +90,7 @@ internal class StripeConnectWebViewContainerImpl<Listener : StripeEmbeddedCompon
internal val stripeWebViewClient = StripeConnectWebViewClient()

@VisibleForTesting
internal val stripeWebChromeClient = StripeWebChromeClient()
internal val stripeWebChromeClient = StripeConnectWebChromeClient()

private var controller: StripeConnectWebViewContainerController<Listener>? = null

Expand Down Expand Up @@ -230,13 +231,28 @@ internal class StripeConnectWebViewContainerImpl<Listener : StripeEmbeddedCompon
}

/**
* A [WebChromeClient] that provides additional functionality for Stripe Connect Embedded Component WebViews.
*
* This class is currently empty, but it could be used to add additional functionality in the future
* Setting a [WebChromeClient] (even an empty one) is necessary for certain functionality, like
* [WebViewClient.shouldOverrideUrlLoading] to work properly.
* A [WebChromeClient] that provides additional functionality for Stripe Connect Embedded Component WebViews,
* namely around permissions.
*/
inner class StripeWebChromeClient : WebChromeClient()
internal inner class StripeConnectWebChromeClient : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
val view = webView ?: return request.deny()

view.findViewTreeLifecycleOwner()?.lifecycleScope
?.launch {
controller?.onPermissionRequest(view.context, request)
}
?: return request.deny()
}

override fun onPermissionRequestCanceled(request: PermissionRequest) {
super.onPermissionRequestCanceled(request)

// currently a no-op since we don't hold any state from the permission
// request and delegate all the UI to the Android system, meaning
// there's no way for us to cancel any permissions UI
}
}

private inner class StripeJsInterface {
@JavascriptInterface
Expand Down
Loading

0 comments on commit 3d66b3c

Please sign in to comment.