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

Draft proposal to update the Google Pay section of the sample application #1469

Merged
merged 21 commits into from
Feb 20, 2024

Conversation

JlUgia
Copy link

@JlUgia JlUgia commented Feb 8, 2024

Description

Proposal to update sample application to simplify the integration.
Note: This changes don't enforce API correctness, and are only meant to be used as guiding proposals. The code hasn't been run or tested. This review does not include thoughts from our Android counterparts, which we are still seeking.

The changes include:

  • Hoist state and view model to the parent activity, and free the composable screen from the dependency
  • [Tentative] Updated action related states to use sealed/abstract class. This should do without the need to nullify optional parameters after having taken action.
  • Proposed removing the round trip to the viewModel on payment start and completed.
  • Separate one off events from the UI state for increased clarity.
  • Proposed to instantiate the Google Pay component only once (if possible – ie.: the componentData does not change). In addition, the component could be created once at the activity level and send as a param to the composable screen. That would allow us to handle the intent right away and free the screen and viewModel from that additional logic and extra UDF cycle.

Feel free to comment and modify.

@jreij
Copy link
Collaborator

jreij commented Feb 8, 2024

Nice work @JlUgia, thanks a lot for the contribution 🚀 We will review this when we can.

) {
lateinit var googlePayComponent: GooglePayComponent
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be private?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question, is there any case where this field might not get initialized and causes a problem? Asking because you definitely know more than me about compose 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a question for us, since the component is only accessed in response to events and state changes under our control. In other words, if we can guarantee that the fetchSession action (triggering the SessionsGooglePayEvents.ComponentData event) completes before any of the following happens:

  • A SessionsGooglePayEvents.Action or SessionsGooglePayEvents.Intent event is issued.
  • The pay button is clicked
  • The state becomes SessionsGooglePayState.ShowButton or SessionsGooglePayState.ShowComponent.

...then the logic must be safe.
My understanding is that this is the case at least for all Google Pay related events, since Google Pay availability is only requested one the session call returns. Is that also true for Action, or Intent events sent by the component?

On that note, I see that we are handling the SessionsGooglePayState.ShowComponent state in the compose UI, but that value is never set elsewhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The var is function local, so access modifiers are not necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky part here is with Intent. This is usually triggered after a redirect (custom tabs, external browser, app switch) has completed and we returned to the app through a deep link. With Google Pay that's mostly after the Google Pay component is finished and a 3DS challenge was launched in the browser (which is rare).

Right now we have this code in the SessionsGooglePayActivity:

    override fun onNewIntent(intent: Intent) {
        ..
        sessionsGooglePayViewModel.onNewIntent(intent) // this triggers a `SessionsGooglePayEvents.Intent`
    }

The only case I can think of where this code executes before the SessionsGooglePayEvents.ComponentData event has been called is if the activity got destroyed in the background.

But then even without a redirect, I don't think this sample covers that scenario (app destroyed in the background and recreated), because we are fetching a new session when the view model is created so we lose the previous state of the component completely. I am not sure if any of our samples covers that use case, it's a tricky one to deal with.

Copy link
Author

@JlUgia JlUgia Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For compose, note that the view model can outlive the view if the view is recreated due to configuration changes (and hence the data there can be reused in subsequent activity recreations).

If the 3DS flow is added to the app, as a developer, I'd like to see one of the following (in order of preference):

  • A good practice sample that I can use in my app (including the session renewal)
  • A marked commented warning letting developers know about lifecycle challenges

If this is not deemed relevant for a simple sample application, I'd consider removing it otherwise.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the input! I think we definitely would like to provide an example that fully works with all edge case scenarios in mind.

and hence the data there can be reused in subsequent activity recreations

Yes and in this case our samples would work. However (correct me if I'm wrong) if the system decides to kill the app in the background to free up memory (just like when you switch on "Don't Keep Activities" in the developer options) then everything in memory will be destroyed including the view model. Which means the only thing we can use to recover the data is the SaveStateHandle. We already use it on the SDK side but not for all scenarios, so we need to investigate this more.

Copy link
Collaborator

@jreij jreij left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@JlUgia JlUgia marked this pull request as ready for review February 20, 2024 14:45
@JlUgia JlUgia requested a review from a team as a code owner February 20, 2024 14:45
@jreij jreij merged commit 2c5cfb5 into Adyen:test/google_pay_beta Feb 20, 2024
6 of 7 checks passed

package com.adyen.checkout.example.ui.googlepay.compose

internal abstract class SessionsGooglePayEvents internal constructor() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JlUgia I just noticed this line, is there any benefit from using this over internal sealed class SessionsGooglePayEvents {?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference is that sealed classes enforce exhaustive checking (eg.: a switch statement will be forced to include a case statement for each member in the class), which has backwards-compatibility implications (especially for lists that are not meant to be exhaustive).

My rule of thumb is to use sealed class for exhaustive lists that are always meant to be handled in full. For example view states in a layout (loading, complete, error), and abstract class for non-exhaustive lists with unrelated items like SesssionsGooglePayEvents (even more so if they are part of an API that is expected to change).

Let me know if that makes sense.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really good solution 💯 Especially in library development where adding something to a sealed class can technically cause a breaking change

SessionsGooglePayContent(
googlePayComponent = googlePayComponent,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw I just tested the branch locally and this lateinit field is causing a crash here (and a few other places) when the screen is being created:

kotlin.UninitializedPropertyAccessException: lateinit property googlePayComponent has not been initialized

I'm not sure anymore if this solution will work. I'll try to find a solution but let me know if you have any suggestions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have a client key implementation readily available, and haven't tested it. Let me take a look tomorrow. What's the quickest way to have a client key added?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry my bad, I should have tested it myself before merging but I didn't want to keep stalling this PR so I thought I'll fix any errors after merging 😅

If you have an Adyen test account already then you can just follow this step in the integration guide. Otherwise don't worry about it, I'll try to figure it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants