Skip to content

Introduce CheckoutController state holders and state loader with tests.#13398

Merged
jaynewstrom-stripe merged 1 commit into
masterfrom
jaynewstrom/add-checkoutcontroller-state-holders
Jul 13, 2026
Merged

Introduce CheckoutController state holders and state loader with tests.#13398
jaynewstrom-stripe merged 1 commit into
masterfrom
jaynewstrom/add-checkoutcontroller-state-holders

Conversation

@jaynewstrom-stripe

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces new internal state holder classes (CheckoutControllerStateHolder, CheckoutConfirmationStateHolder) and a CheckoutStateLoader for the new CheckoutController. These enable atomic, process-death-safe state commits and improved testability for the confirmation flow. Dependency injection modules, DI qualifiers, and supporting classes are included. A full suite of unit tests is added to verify correct behavior and transitions.

Motivation

As part of work to introduce and solidify the new CheckoutController, we need robust state management that survives process death and cleanly separates responsibilities for state mutation and observation. Extracting this logic into holders and a loader enables isolated unit testing and better separation of concerns, unlocking easier future maintenance and features.

Testing

  • Added tests
  • Modified tests

Changelog

  • [Added] Introduced state holder and loader classes for CheckoutController with comprehensive unit test coverage.

@jaynewstrom-stripe
jaynewstrom-stripe force-pushed the jaynewstrom/add-checkoutcontroller-state-holders branch from 96b14c2 to 799744c Compare July 10, 2026 20:00
@jaynewstrom-stripe
jaynewstrom-stripe force-pushed the jaynewstrom/add-checkoutcontroller-state-holders branch from 799744c to 788bd12 Compare July 10, 2026 22:04
@jaynewstrom-stripe
jaynewstrom-stripe marked this pull request as ready for review July 10, 2026 22:46
@jaynewstrom-stripe
jaynewstrom-stripe requested review from a team as code owners July 10, 2026 22:46
fun bindPaymentElementLoader(loader: DefaultPaymentElementLoader): PaymentElementLoader

@Binds
fun bindsElementsSessionRepository(impl: RealElementsSessionRepository): ElementsSessionRepository

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need this? Don't we get the Elements Session from the Checkout Session?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's injected into the loader. So it's not really used in this case. We could probably return a throwing instance to make that more obvious. But realistically, it's probably better to leave as is, ideally model it differently so it's not required here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah I feel like better modeling might be for PaymentElementLoader to take a LoadSession parameter which is a fun interface, like RetrieveCustomerEmail. It would do the work in this function: code and in CheckoutSessions implementations we inject the CheckoutSessionLoader and otherwise we inject the ElementsSessionLoader.

Doesn't seem hugely important but I think it'd be better

configuration: CheckoutController.Configuration.State,
checkoutSessionResponse: CheckoutSessionResponse,
): CheckoutControllerState = CheckoutControllerState(
key = UUID.randomUUID().toString(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OOC why do we not use the checkout session response ID for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I can't think of a great reason not to use it.


// Preserve the customer's existing selection across reloads when it's still valid, rather
// than blindly adopting the loader's recomputed selection (reuses the embedded logic).
val selection = selectionChooser.choose(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should this logic live in PaymentElementLoader?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We could probably inject it. But it does seem logical that it would go there 🤔 I'll attempt this in a follow up!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sounds good! Right now I think PaymentElementLoader is doing some work and then we throw out the results of that work

Comment on lines +36 to +40
val resolvedState = state.copy(
// [state] carries the previously resolved flag images forward (a mutation copies them
// from the committed state), so they're reused when the currencies haven't changed.
flagImages = flagImageResolver.resolve(response, cached = state.flagImages),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it feels surprising that we have to make a copy of state here. Does this mean that the state in CheckoutController is out of sync with what we use in load?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For configure this is a little silly. But I built it to be prepared for mutations, which is where this is actually useful. During a mutation, we'll have the previously configured response + flags, etc -- and we want all the same state, except for the new checkout sessions response + the updated flags (if they're needed).

Comment on lines +39 to +41
val ruleChain: RuleChain = RuleChain
.outerRule(networkRule)
.around(PaymentConfigurationTestRule(applicationContext))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should we re-use TestRules here so that other rules are pulled in automatically? E.g. I wonder if we want to use RetryRule for this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Seems reasonable. I've got a few follow ups already queued, so I'm going to merge this as is to unblock those -- but I'll follow up!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There's a lot of instrumentation test specific stuff in TestRules, so not sure it makes sense to use here -- given this is a unit test with robolectric instead of running on an emulator.

@jaynewstrom-stripe
jaynewstrom-stripe merged commit 2e5f6ab into master Jul 13, 2026
52 checks passed
@jaynewstrom-stripe
jaynewstrom-stripe deleted the jaynewstrom/add-checkoutcontroller-state-holders branch July 13, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants