-
Notifications
You must be signed in to change notification settings - Fork 664
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] Improve WebView state handling #10268
Conversation
Diffuse output:
APK
|
3faf73b
to
563468d
Compare
@@ -284,14 +284,6 @@ public final class com/stripe/android/connect/appearance/fonts/CustomFontSource$ | |||
public final fun serializer ()Lkotlinx/serialization/KSerializer; | |||
} | |||
|
|||
public final class com/stripe/android/connect/databinding/StripeConnectWebviewBinding : androidx/viewbinding/ViewBinding { |
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 XML layout was used by StripeConnectWebViewContainer
, but now it constructs the view programmatically now that it's getting the WebView from the ViewModel.
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.
Do you know if this was part included in the API review? If so, I think you need to amend the API review and have someone re-approve 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.
This was only used internally and was not meant to be exposed in the public API.
internal object AccountOnboardingListenerDelegate : ComponentListenerDelegate<AccountOnboardingListener> { | ||
override fun AccountOnboardingListener.delegate(message: SetterFunctionCalledMessage) { | ||
internal object AccountOnboardingListenerDelegate : ComponentListenerDelegate<AccountOnboardingListener>() { | ||
override fun delegate(listener: AccountOnboardingListener, message: SetterFunctionCalledMessage) { |
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.
👀 Minor refactoring to be less fancy, more approachable.
"You must call EmbeddedComponentManager.onActivityCreate in your Activity.onCreate function" | ||
} | ||
|
||
checkContextDuringCreate(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.
👀 Extracted helper function.
internal suspend fun requestCameraPermission(activity: Activity): Boolean? { | ||
if (checkSelfPermission(activity, 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.
👀 Note the change from Context
to Activity
here and in other places, since using application context would be a runtime error in many cases.
@OptIn(PrivateBetaConnectSDK::class) | ||
internal fun interface ComponentListenerDelegate<Listener : StripeEmbeddedComponentListener> { | ||
fun Listener.delegate(message: SetterFunctionCalledMessage) | ||
internal open class ComponentListenerDelegate<Listener : StripeEmbeddedComponentListener> { |
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.
👀 Refactored to be an open class
in order to move handling of common component events here. Before, the delegation to the listener was done in the Controller, which is no longer feasible. The Controller has been replaced by a ViewModel, which may not keep a reference to the listener otherwise we risk leaking the Activity during a config change.
val eventFlow: Flow<ComponentEvent> get() = _eventFlow.asSharedFlow() | ||
|
||
@VisibleForTesting | ||
internal val delegate = StripeConnectWebViewDelegate() |
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 extracted WebView -> VM interactions to this inner class for better code organization and encapsulation, e.g. the Container view should not be able to call onPageStarted
on the VM.
@SuppressLint("StaticFieldLeak") // Should be safe because we're using application. | ||
val webView: StripeConnectWebView = | ||
createWebView( | ||
application = application, | ||
delegate = delegate, |
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.
👀
// don't send errors for requests that aren't for the main page load | ||
if (isMainPageLoad) { | ||
// TODO - wrap error better | ||
_eventFlow.tryEmit(ComponentEvent.LoadError(RuntimeException(errorString))) |
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.
👀 Emitting an event object instead of directly invoking the Listener before.
|
||
val BASE_DEPENDENCIES_KEY = object : CreationExtras.Key<BaseDependencies> {} | ||
|
||
val Factory: ViewModelProvider.Factory = viewModelFactory { |
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.
👀 Extra scrutiny here, please.
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.
what are you concerned about here? Anything in particular I should be looking out for?
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.
Relates to my other comment:
👀 I'd appreciate extra scrutiny here since these ViewModel APIs are new to me. Relevant docs: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories
import org.robolectric.RuntimeEnvironment | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
class StripeConnectWebViewTest { |
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.
👀 Tests in here aren't comprehensive, but they weren't before either. I added more to largely sanity check testability.
@@ -284,14 +284,6 @@ public final class com/stripe/android/connect/appearance/fonts/CustomFontSource$ | |||
public final fun serializer ()Lkotlinx/serialization/KSerializer; | |||
} | |||
|
|||
public final class com/stripe/android/connect/databinding/StripeConnectWebviewBinding : androidx/viewbinding/ViewBinding { |
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.
Do you know if this was part included in the API review? If so, I think you need to amend the API review and have someone re-approve it
@@ -31,11 +32,14 @@ class AccountOnboardingView private constructor( | |||
embeddedComponentManager: EmbeddedComponentManager? = null, | |||
listener: AccountOnboardingListener? = null, | |||
props: AccountOnboardingProps? = null, | |||
cacheKey: String? = 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.
Does this need to be included in an updated API review?
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.
Yes. This change will be bundled in an upcoming API review that will also include presenting Account Onboarding full screen as opposed to returning a View.
|
||
val BASE_DEPENDENCIES_KEY = object : CreationExtras.Key<BaseDependencies> {} | ||
|
||
val Factory: ViewModelProvider.Factory = viewModelFactory { |
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.
what are you concerned about here? Anything in particular I should be looking out for?
Summary
Sorry for the big PR. It's a big refactoring that took quite a bit of design-by-doing to get right.
tl;dr:
Quoting my kdocs for
StripeConnectWebViewContainerViewModel
:WebView
created and configured by the container view viaStripeConnectWebViewContainerImpl
StripeConnectWebView
is created byStripeConnectWebViewContainerViewModel
.StripeConnectWebViewContainerViewModel
, leveraging platform persistence mechanismsStripeConnectWebViewContainerController
, which did not survive configuration changesStripeConnectWebViewContainerViewModel
, which stays alive during configuration change to handle WebView events, e.g. loading state changesSee inline comments for more detailed implementation changes.
Top research links:
Motivation
Part of https://jira.corp.stripe.com/browse/CAX-3737
Testing
Demos
Before
webview-before.mov
After
webview-after.mov
After (smoke testing)
webview-after-regression-test.mov
After (XML layout support)
webview-after-xml.mov