#291. A plan switch (Upgrade, Downgrade, or Pay for this plan on a granted tier's card) was confirmed in a panel at the top of the Plans section, above the cards and away from the button that opened it. It now confirms in the same dialog as Cancel plan and Resume plan. The button opens the dialog at once, titled with the panel's question ("Switch to Pro?", or "Pay for Pro?" on the granted tier's own card), and the dialog's body loads from the switch's preview route, because the charge it states comes from Stripe at that moment. Keep current plan closes the dialog and sends nothing. An answer that belongs in the dialog stays there: a declined card, a payment awaiting the bank's approval, stale figures; focus moves to the new form. An answer that lands outside closes it: the switch made or refused, or a plan withdrawn after the page loaded. A load that fails closes the dialog with the error toast; a commit that fails (403 or 5xx, which the pages' htmx config does not swap) leaves it open with the toast. Closing the dialog while its body loads aborts the load, and the aborted load shows no toast. The shared dialog gains a region the loaded form lives in; the forms library renders a preview that leaves by the dialog without a box of its own; the lint accepts `data-action-load` as a trigger's route. No string is added or reworded. docs/design-system.md, docs/operator-ux-conventions.md and docs/stripe.md say the same. Tests: TestSwitchAnswersNameTheirRegion, TestPlanMoveTitle, TestSwitchControlOpensTheSharedDialog, TestConfirmModalCarriesTheLoadedRegion, TestPreviewInTheDialogRendersNoBox, and the walkthrough TestSwitchDialogWalkthrough (seven steps in a browser, among them a load answered offline, a load closed mid-flight, and a commit answered 503 by the browser so nothing reaches Stripe). The walkthrough resets the application database, so it runs only with SWITCH_DIALOG_WALK=1, as the merchant-disclosure sheet runs only when asked; make test skips it. 35 goldens change: every member plans golden loses one blank line, the switch controls of 12 lose their hx-* attributes for the dialog's, and the plan move form's golden loses its box. Full suite on reset databases: 5249 passed, 16 skipped, none failed. Sandbox walk on this commit, all twelve steps passed: the dialog opens at once and loads Stripe's figure ($20.00 equal to the preview's amount_due); Keep current plan sends nothing; Cancel plan shows its own form; offline the dialog closes with the toast; stale figures, a declined card and a payment awaiting approval stay in the dialog with focus on the new form; the made switch closes it and Max is current; a plan withdrawn in another session refuses in the Plans region; Pay for this plan reads "Pay for Max?". make screens: no page this commit touches changed. Three operator captures differ for other reasons: person detail and invoice detail show new ids, because the demo snapshot was rebuilt when the previous landing added a FedWiki migration, and the domains list's age column counts one more day (#120). Not walked by the walkthrough, with the reason: a request in flight at a chosen moment (the dialog closed while the commit runs, another switch opened) and a page restored from the browser's cache; a load answered 400 and a commit the switch refuses answer through the same close rule as the withdrawn plan and the made switch that the walkthrough and the hand walk cover. Breaks, all CAUGHT (19 test records): - own card titled as a switch (title test) - own card titled as a switch (control test) - setMove leaves the title unset - control loads without the price (control test) - control loads without the price (render test) - control loads the switch's route - heading left in the message - current tier's name unescaped - refused preview left in the dialog - confirmation answered into the Plans region (a declined switch) - confirmation answered into the Plans region (a stale switch) - confirmation answered into the Plans region (a switch awaiting approval) - decline answered 200 - way out a link again - commit's description dropped - region renamed in the partial - box rule removed - box rule widened to every kind - box rule widened to every way out Breaks of the walkthrough, all CAUGHT (8 records, each building and booting the app from the edited tree): - the close not deferred - the abort on close dropped - the cancelled mark ignored by the toast - the load not sent - the loaded region left showing under a static trigger - the close on an answer outside the dialog removed - the failed commit closing the dialog - the way out a link again Seen by no test: renderConfirmation without its describeCommit call (the database tests' handlers carry no Stripe key fingerprint, so the line is never shown there; describeCommit itself is pinned); data-action-load dropped from the lint's condition (no template interpolates a route into it); the spinner and aria-busy not set, and the focus after a stay-open answer not moved, which only the hand walk reaches, because the walkthrough stops before any Stripe write.
132 lines
4.5 KiB
HTML
132 lines
4.5 KiB
HTML
|
|
===== member.plan.move unbound
|
|
|
|
|
|
|
|
<form id="form-member.plan.move" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="" ><input type="hidden" name="price_id" value="" ><input type="hidden" name="proration_date" value="" >
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
===== member.plan.move unbound edit side
|
|
|
|
|
|
<div class="alert alert-secondary app-form-box mb-3" role="status">
|
|
<form id="form-member.plan.move" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="" ><input type="hidden" name="price_id" value="" ><input type="hidden" name="proration_date" value="" >
|
|
</div>
|
|
|
|
|
|
</form>
|
|
</div>
|
|
|
|
|
|
===== member.plan.move create side
|
|
|
|
|
|
|
|
<form id="form-member.plan.move" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="" ><input type="hidden" name="price_id" value="" ><input type="hidden" name="proration_date" value="" >
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
===== member.plan.move unbound with values
|
|
|
|
|
|
|
|
<form id="form-member.plan.move" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="sample" ><input type="hidden" name="price_id" value="sample" ><input type="hidden" name="proration_date" value="sample" >
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
===== member.plan.move record
|
|
|
|
|
|
|
|
<form id="form-member.plan.move-i1" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-i1-error" class="app-form-error" role="alert"></div>
|
|
|
|
<div class="app-form-message mb-2"><p>msg</p></div>
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="sample" ><input type="hidden" name="price_id" value="sample" ><input type="hidden" name="proration_date" value="sample" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Confirm switch</button>
|
|
|
|
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Keep current plan</button>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
|
|
|
|
===== member.plan.move refused [ladder_id price_id proration_date]
|
|
|
|
|
|
|
|
<form id="form-member.plan.move" data-form="member.plan.move" data-form-kind="preview" class="app-form" novalidate hx-post="/partials/member/plans/switch" hx-target="#member-plans" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-member.plan.move-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<input type="hidden" name="ladder_id" value="" aria-describedby="form-member.plan.move-ladder_id-error" aria-invalid="true" autofocus><input type="hidden" name="price_id" value="" aria-describedby="form-member.plan.move-price_id-error" aria-invalid="true"><input type="hidden" name="proration_date" value="" aria-describedby="form-member.plan.move-proration_date-error" aria-invalid="true">
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
===== member.plan.move bad-values refusals
|
|
|