-
Notifications
You must be signed in to change notification settings - Fork 657
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] Add camera support to the Connect SDK #9607
Conversation
69616a3
to
16db6f3
Compare
Diffuse output:
APK
|
9e85b34
to
3603b54
Compare
# 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
# Conflicts: # connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt
3603b54
to
45f9c94
Compare
* Show promo badge in bank form * Address code review feedback Fix layout issue with super-long bank name and validate with screenshot test.
…#9727) * Add Embedded Appearance params to AppearanceBottomSheetDialogFragment
* 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>
…ltConfirmationHandler` (#9754)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool to see this working!
connect/src/main/java/com/stripe/android/connect/EmbeddedComponentManager.kt
Show resolved
Hide resolved
/** | ||
* | ||
*/ | ||
suspend fun onPermissionRequest(context: Context, request: PermissionRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix docstring
.../src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt
Outdated
Show resolved
Hide resolved
logger.debug( | ||
"($loggerTag) Denying permission - ${request.resources.joinToString()} are not supported" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be warning
log level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. We'll want an analytic event too now that I think about it so that we know if an embedded component is expecting any unsupported permissions. I've added a TODO for when we add analytics (hopefully tomorrow or Friday)
internal suspend fun requestCameraPermission(context: Context): Boolean? { | ||
val activity = context.findActivity() ?: error("You must create an AccountOnboardingView from an Activity") | ||
launcherMap[activity]?.launch(android.Manifest.permission.CAMERA) ?: return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned throwing here would be too user-unfriendly since it's pretty late in the UX flow to be crashing. I think instead we should return null and log warnings if either the activity or launcher are null.
Separately, the log message should be generalized and not reference a specific view, "AccountOnboardingView" in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's a worse bug to crash than it is to let the user proceed but with broken permissions. What do you think about crashing if in debug (since this really is a programmer error), but then following the behavior you outlined otherwise?
activity.registerForActivityResult( | ||
ActivityResultContracts.RequestPermission() | ||
) { isGranted -> | ||
MainScope().launch { | ||
permissionsFlow.emit(isGranted) | ||
} | ||
}.also { | ||
launcherMap[activity] = it | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could simplify the assignment and maybe use tryEmit
instead of launching. We might need to configure the MutableSharedFlow
with extraBufferCapacity = 1
though.
activity.registerForActivityResult( | |
ActivityResultContracts.RequestPermission() | |
) { isGranted -> | |
MainScope().launch { | |
permissionsFlow.emit(isGranted) | |
} | |
}.also { | |
launcherMap[activity] = it | |
} | |
launcherMap[activity] = | |
activity.registerForActivityResult( | |
ActivityResultContracts.RequestPermission() | |
) { isGranted -> | |
permissionsFlow.tryEmit(isGranted) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this suggestion. Is there any risk of race conditions where multiple tryEmit
s are called before the consumers from permissionFlow
.first get to them? I think the risk is low and acceptable, curious on your thoughts @lng-stripe
connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt
Outdated
Show resolved
Hide resolved
return | ||
} | ||
|
||
if (checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make EmbeddedComponentManager
fully responsible for the native camera permission and change this to
if (checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { | |
if (embeddedComponentManager.hasCameraPermission(context)) { |
otherwise this code risks becoming out of sync with embeddedComponentManager.requestCameraPermission(context)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. @lng-stripe Thoughts on having a single call to EmbeddedComponentManager
and inlining this into requestCameraPermission()
? ie:
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
}
// proceed with explicit check...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth having a hasCameraPermission()
function and your change above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like abstract the above if
statement into a hasCameraPermission
function, or keep what I have in that comment and also add a separate hasCameraPermission()
call in the Controller? The latter feels duplicative to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think arguments could be made for either. What you have now is fine 👍
request.grant(permissionsRequested) | ||
} else { | ||
val isGranted = embeddedComponentManager.requestCameraPermission(context) ?: return | ||
withContext(Dispatchers.Main) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading the code correctly, either setting the dispatcher here isn't necessary, or we'd need to do so for the other request.deny()
/ grant()
calls above (I think it's the former). Could you check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, we do need this because the request/deny call crashes if not done on the Main thread. I re-confirmed by wrapping the parent call in withContext(Dispatchers.IO) { ... }
- the reason I didn't catch the one on line 153 is that I moved the parent call to use the view lifecycle scope which is the main thread. For safety it's important this function guarantees it's done on the main thread - updated!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed -- good point that it's safer to be explicit here
@@ -58,6 +66,10 @@ class StripeConnectWebViewContainerControllerTest { | |||
|
|||
@Before | |||
fun setup() { | |||
Dispatchers.setMain(Dispatchers.Unconfined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't be necessary if we remove setting the dispatcher as suggested in my other comment. Otherwise, we'll need to Dispatchers.resetMain()
in an @After
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still necessary - see comment above. I've added an @After
as well now
…eConnectWebViewContainer.kt Co-authored-by: lng-stripe <91862945+lng-stripe@users.noreply.github.com>
…nentManager.kt Co-authored-by: lng-stripe <91862945+lng-stripe@users.noreply.github.com>
private val logger: Logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG), | ||
private val isDebugBuild: Boolean = BuildConfig.DEBUG, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't add this to the public API, which EmbeddedComponentManager
and this constructor will be part of (we should remove the @RestrictTo
above). I do think we should add a debug: Boolean
property to Configuration
, but logger
should not be exposed and instead should be instantiated internally.
val application = activity.application | ||
|
||
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why register on the Application? I think it was better before to register on the Activity. Reading the docs, it turns out you actually don't need to unregister the callback:
As this callback is associated with only this Activity, it is not usually necessary to unregister it unless you specifically do not want to receive further lifecycle callbacks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also prefer the callbacks on the activity, but that API is only available on API 29+ (whereas the Application version is available on 14+): https://developer.android.com/reference/android/app/Activity#registerActivityLifecycleCallbacks(android.app.Application.ActivityLifecycleCallbacks)
|
||
@Before | ||
fun setup() { | ||
Dispatchers.setMain(Dispatchers.Unconfined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to resetMain()
in this test class, too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I don't even need this dispatcher and forgot to remove it 😅
|
||
/** | ||
* Create a new [AccountOnboardingView] for inclusion in the view hierarchy. | ||
*/ | ||
@PrivateBetaConnectSDK | ||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these functions should not be restricted to the library -- they will be part of the public API. Also, isn't @PrivateBetaConnectSDK
redundant when it's already on the class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I didn't realize that this is only needed at the class level, so I cleaned it up throughout the library.
We're adding @RestrictTo
until we're ready to go into private beta, at which point we'll remove it and external developers will be able to use these classes (with the right opt-in). See here: #9203 (comment)
@@ -42,17 +70,40 @@ class EmbeddedComponentManager( | |||
/** | |||
* Create a new [PayoutsView] for inclusion in the view hierarchy. | |||
*/ | |||
@PrivateBetaConnectSDK | |||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto -- see other comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 👍
Summary
Hooks the webview's camera permission into the Android app's camera permission so that the user is shown a permissions modal when the connect SDK requests a camera permission.
MXMOBILE-2913
Motivation
Certain embedded components (particularly any that use Identity) make use of the camera, and the SDK needs to properly handle camera permissions to make sure these features work.
Testing
The easiest way I found to test was:
Screenshots
Granting permission
demo-1733780574.mp4
Rejecting permission
demo-1733780639.mp4