Skip to content

ConfirmationPage - Composition over Configuration (full migration) #101224

Open

Description

@MelvinBot

Problem

ConfirmationPage is a configuration-style wrapper: 24 flat props on ConfirmationPage.tsx:25-77. Ten of them exist only to configure its two buttons, and seven more are *Style escape hatches that exist because callers can't reach the regions they need to restyle:

// buttons
buttonText, onButtonPress, shouldShowButton, isButtonDisabled, isButtonLoading,
secondaryButtonText, onSecondaryButtonPress, shouldShowSecondaryButton,
isSecondaryButtonDisabled, isSecondaryButtonLoading
// style patches
headingStyle, illustrationStyle, descriptionStyle, ctaStyle, footerStyle,
containerStyle, innerContainerStyle

Every new capability means another should* / is* / on*Press / *Text / *Style prop. The shape doesn't scale to a third button, to button content the wrapper never anticipated (an icon, a DotIndicator, a badge), or to any per-screen layout ConfirmationPage didn't predict. It also already carries two duplicate string-or-node pairs — description / descriptionComponent and cta / ctaComponent — which is the configuration API admitting it needs slots.

feat: add request access button to the domain access restricted screen added two of those props (isSecondaryButtonDisabled, isSecondaryButtonLoading) and ZhenjaHorbach flagged the prop count in review.

This is the same "composition over configuration" problem as Button - Composition over Configuration, one layer up: decomposing Button itself doesn't help callers if the wrapper in between re-flattens the API.

Scope note: this issue originally covered only the 10 button props. Per the follow-up discussion it now covers the full ConfirmationPage migration to a compound component.

Proposal

Migrate ConfirmationPage to a compound component. Each region it renders becomes a slot the caller composes, matching the Button.Icon / Button.Text / Button.KeyboardShortcut shape already shipped in Button/index.tsx:29-33.

<ConfirmationPage>
    <ConfirmationPage.Illustration src={LottieAnimations.Fireworks} />
    <ConfirmationPage.Heading>{translate('domain.accessRestricted.title')}</ConfirmationPage.Heading>
    <ConfirmationPage.Description>{translate('domain.accessRestricted.description')}</ConfirmationPage.Description>
    <ConfirmationPage.Footer>
        <ConfirmationPage.Button
            isDisabled={!isRequestPending && (!!hasPendingRequest || isOffline)}
            isLoading={isRequestPending}
            onPress={requestDomainAdminship}
        >
            <Button.Text>{translate('domain.accessRestricted.requestAdminAccess')}</Button.Text>
        </ConfirmationPage.Button>
    </ConfirmationPage.Footer>
</ConfirmationPage>

What this buys:

  • shouldShow* disappears. A slot renders when it's provided.
  • The *Style props disappear. style goes on the subcomponent that owns the region, so nothing has to be plumbed through the wrapper.
  • The string/node pairs collapse. description + descriptionComponent become one <ConfirmationPage.Description>; same for cta + ctaComponent.
  • Button content belongs to the caller — an icon, a badge, a DotIndicator, or a third button all become possible without touching ConfirmationPage.
  • No visual drift. ConfirmationPage.Button pre-applies CONST.BUTTON_VARIANT.SUCCESS / CONST.BUTTON_SIZE.LARGE / styles.mt3 so 30 screens stay consistent without each caller repeating them. This resolves the sizing/styling trade-off raised in the original write-up: sizing stays owned by the component, not the call site.

Layout logic stays inside ConfirmationPage — the ScrollView + centering container at ConfirmationPage.tsx:111-158 and the FixedFooter at ConfirmationPage.tsx:159-188. Only content and state move out.

Current props → target slots
Current prop(s) Target
illustration, illustrationStyle <ConfirmationPage.Illustration src style> (keeps the Lottie / SVG / reduced-motion fallback branch)
heading, headingStyle <ConfirmationPage.Heading style>
description, descriptionComponent, descriptionStyle <ConfirmationPage.Description style>
cta, ctaComponent, ctaStyle <ConfirmationPage.CTA style>
footerComponent <ConfirmationPage.FooterMessage> (the absolutely-positioned region above the footer)
buttonText, onButtonPress, shouldShowButton, isButtonDisabled, isButtonLoading <ConfirmationPage.Button> inside <ConfirmationPage.Footer>
secondaryButtonText, onSecondaryButtonPress, shouldShowSecondaryButton, isSecondaryButtonDisabled, isSecondaryButtonLoading a second <ConfirmationPage.Button>
footerStyle style on <ConfirmationPage.Footer>
containerStyle, innerContainerStyle style on <ConfirmationPage> / <ConfirmationPage.Content>

Exact subcomponent names are open to bikeshedding in the first PR.

Scope

Prop usage across the 30 call sites
shouldShowButton            30     descriptionComponent      5
buttonText                  30     innerContainerStyle       5
onButtonPress               30     secondaryButtonText       4
heading                     30     onSecondaryButtonPress    4
description                 28     headingStyle              3
illustration                19     cta                       3
containerStyle              18     ctaComponent              3
descriptionStyle            12     footerComponent           2
illustrationStyle           10     is(Secondary)Button*      1 each
shouldShowSecondaryButton    6

Two things stand out: containerStyle / descriptionStyle / illustrationStyle are used on a third to two-thirds of call sites (the configuration API is leaking), while the four is*Button* props are used exactly once each (they were added for one screen and every other caller pays for them in the type).

The 30 call sites, grouped for batching

domain (4)

  • src/pages/domain/BaseDomainVerifiedPage.tsx
  • src/pages/domain/DomainAccessRestrictedPage.tsx
  • src/pages/domain/DomainAddedPage.tsx
  • src/pages/domain/DomainAlreadyExistsPage.tsx

settings/Wallet (9)

  • src/pages/settings/Wallet/AddCardToDigitalWalletPage.tsx
  • src/pages/settings/Wallet/InternationalDepositAccount/subPages/Success.tsx
  • src/pages/settings/Wallet/PersonalCards/PersonalCardsErrorConfirmation.tsx
  • src/pages/settings/Wallet/PersonalCards/steps/SuccessStep.tsx
  • src/pages/settings/Wallet/PersonalCards/upgrade/UpgradeConfirmation.tsx
  • src/pages/settings/Wallet/ShareBankAccount/ShareBankAccount.tsx
  • src/pages/settings/Wallet/SuccessReportCardLost.tsx
  • src/pages/settings/Wallet/TransferBalancePage.tsx
  • src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx

settings/Security (4)

  • src/pages/settings/Security/LockAccount/FailedToLockAccountPage.tsx
  • src/pages/settings/Security/LockAccount/UnlockAccountPage.tsx
  • src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx
  • src/pages/settings/Security/TwoFactorAuth/SuccessPageBase.tsx

workspace (7)

  • src/pages/workspace/WorkspaceConfirmationSuccessPage.tsx
  • src/pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation.tsx
  • src/pages/workspace/downgrade/DowngradeConfirmation.tsx
  • src/pages/workspace/members/DynamicWorkspaceOwnerChangeSuccessPage.tsx
  • src/pages/workspace/receiptPartners/DynamicInviteReceiptPartnerPolicyPage.tsx
  • src/pages/workspace/upgrade/UpgradeConfirmation.tsx
  • src/pages/workspace/workflows/WorkspaceWorkflowsPayerSuccessPage.tsx

bank accounts / payments (4)

  • src/pages/AddPersonalBankAccountPage/substeps/SuccessStep.tsx
  • src/pages/EnablePayments/Pay/Activate/ActivateStep.tsx
  • src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
  • src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx

misc (2)

  • src/pages/TransactionDuplicate/DynamicReviewPage.tsx
  • src/pages/inbox/report/DynamicReportDetailsExportPage.tsx

Migration strategy

Incremental, mirroring the parent issue's approach so nothing lands as one 30-file PR:

  1. Add the subcomponents alongside the existing props. ConfirmationPage renders children when present and falls back to the flat-prop rendering otherwise, so main stays green at every step.
  2. Mark all 24 flat props @deprecated with a pointer to the slot that replaces each one.
  3. Migrate the call sites in batches by area (the groups above are a reasonable split — roughly 5 PRs).
  4. Delete the deprecated props and the fallback branch once no call site uses them.

Once the shape is agreed for ConfirmationPage, run the same audit over the other Button wrappers that flatten button state into their own props.

Context

Issue OwnerCurrent Issue Owner: @Guccio163

Activity

  1. Guccio163 commented on Sep 16, 2026

    @Guccio163
    Contributor

    Hi @mountiny and @ZhenjaHorbach, I created a proposal for the full ConfirmationPage composition, which would also handle this case; If it gets accepted we could close this issue ✅

  2. Guccio163 commented on Sep 16, 2026

    @Guccio163
    Contributor

    Basing on this comment, we'll handle the whole ConfirmationPage migration (proposed here) in this issue - @MelvinBot rename it and update the description then

  3. changed the title [-]ConfirmationPage - Composition over Configuration for its button props[/-] [+]ConfirmationPage - Composition over Configuration (full migration)[/+] on Sep 16, 2026
  4. MelvinBot commented on Sep 16, 2026

    @MelvinBot
    ContributorAuthor

    Renamed to ConfirmationPage - Composition over Configuration (full migration) and rewrote the description to cover the whole component, not just the two buttons.

    ⚠️ I can't read the Slack thread you linked (private workspace), so I reconstructed the target API from the code. If your proposal differs, reply with the shape and I'll correct the body.

    What changed in the description:

    • Problem now covers all 24 flat props, not just the 10 button ones — including the 7 *Style escape hatches and the two duplicate string-or-node pairs (description/descriptionComponent, cta/ctaComponent).
    • Proposal is a full compound component: ConfirmationPage.Illustration / .Heading / .Description / .CTA / .Footer / .Button, with a prop → slot mapping table. ConfirmationPage.Button keeps CONST.BUTTON_SIZE.LARGE + margin, which settles the sizing trade-off the old description left open.
    • Scope now lists all 30 call sites grouped into 5 batches, plus measured prop usage. Two findings worth noting: containerStyle (18), descriptionStyle (12) and illustrationStyle (10) are used widely — the configuration API is already leaking — while each of the four is*Button* props is used exactly once.
    • Migration strategy spelled out: add slots alongside the existing props with a fallback branch → @deprecated all 24 → migrate in ~5 area PRs → delete the props and the fallback.
  5. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    This issue has not been updated in over 15 days. @mountiny, @ZhenjaHorbach, @Guccio163 eroding to Monthly issue.

    P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!

  6. Guccio163 commented on Oct 13, 2026

    @Guccio163
    Contributor

    The PR just got review by C+, I'll be addressing the comments soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions