Skip to content

Settings: show price before PayPal (subscribe + resize) + merge size/subscription sections#36

Open
ClaydeCode wants to merge 5 commits into
mainfrom
feature/clayde/merge-size-subscription-price-preview
Open

Settings: show price before PayPal (subscribe + resize) + merge size/subscription sections#36
ClaydeCode wants to merge 5 commits into
mainfrom
feature/clayde/merge-size-subscription-price-preview

Conversation

@ClaydeCode

Copy link
Copy Markdown
Contributor

Closes #27.

Reworks the subscription/resize area of Settings. The subscribe-side price display is the launch blocker; the rest is the polish from the same issue.

What changed

1. Merged the Size and Subscription sections. They are now one card. A resize on a subscribed shard is a billing change, so splitting them across two sections split one decision across two places. Size comes first, so the card reads top-to-bottom as "pick a size → see the price → subscribe". The card is titled Size alone when billing is disabled, and keeps the #size anchor that AppIcon/AppStoreEntry deep-link to.

2. The monthly price is now shown before the buyer leaves for PayPal, on both flows:

  • Subscribe (trial + grace/reactivate): the current size and its monthly gross price, above the PayPal button.
  • Resize: the new monthly price plus the delta vs. current, before confirming.

Gross incl. 19% VAT, with the VAT share broken out, matching how the active-subscription card already renders it.

3. The PayPal revise button now has a deliberate slot under the size buttons with a width cap, instead of popping in fluid-width alongside them.

4. Resize approval hands off to the restart screen rather than polling on a busy Settings page. Since resize is the only thing that routes to /restart, that screen now returns to /settings instead of the app grid.

Two things worth knowing

The pricing mirror is in lockstep — verified, not assumed. The issue flagged the risk that pricing.js lagged controller #298 and would preview net while PayPal charges gross. It has not: origin/main of the controller computes (vm + disk*0.04) * 1.5 * 1.19, identical to pricing.js, and deliberately uses half-up rounding (int(eur*100 + 0.5)) to stay bit-identical to JS Math.round. No drift. (The local controller checkout was stale on an old branch and did show a divergent formula — worth knowing if you check this by hand.)

A pre-existing bug was blocking resize entirely for unsubscribed shards. e5d25e9 replaced the confirm button with the PayPal revise container, which only renders for an active subscription. That orphaned resizeShard() — it had zero template callers, so trial shards saw explanatory text, an empty div and a Cancel link, with no way to resize. The confirm button is restored for that path. This is why item 2's "before confirming" had nothing to attach to.

Known issues I did not fix (your call)

  • Downsize is selectable but always fails. sizeIsAvailable only excludes the current size, so a smaller size can be picked, while the controller rejects any non-increasing resize with 409. Pre-existing, but the new price preview makes it worse: it now advertises "New price: €21.78/month (-€15.71 vs. current)" for an operation that cannot succeed. One-line fix (upsize-only), but it changes which buttons are enabled and might pre-empt intended downsize support, so I left it.
  • A never-delivered BILLING.SUBSCRIPTION.UPDATED webhook now strands the user on /restart. The old subscribed path polled in place and surfaced a Refresh button after 60s; the restart screen has no such escape hatch, though it does escalate to "contact support" after 50s. This is the behaviour the issue asked for and matches the unsubscribed path, so I kept it — flagging in case the failure mode matters more than the consistency.

Verification

  • npm run test:unit — 33 passing (14 new).
  • npm run lint, npm run build — clean. Note CI only builds; it does not run the unit tests.
  • The price-preview computed props are tested directly as plain functions (no mount harness — this repo has no component tests, and adding @vue/test-utils felt like a separate decision). I confirmed the tests have teeth by mutating computeMonthlyPrice(p.vm_size, p.volume_size_gb) to || 0 and watching the null-volume test fail.
  • Not exercised against a live PayPal sandbox — the SDK paths (createSubscription/onApprove) are unchanged apart from the /restart handoff.

Recommended reading order

  1. src/lib/pricing.js — adds formatPriceDelta
  2. src/views/Restart.vue — one-line return target
  3. src/views/Settings.vue — the merged card (bulk of the diff)
  4. agents.md — documents the price-before-PayPal rule and the mirror lockstep trap
  5. tests/unit/pricing.spec.js, tests/unit/settings-pricing.spec.js

🤖 Generated with Claude Code

The resize flow needs to show how much the monthly price changes, not just
the new price. formatPrice cannot express the sign, so the caller would have
to hand-roll it.
…ice upfront

A resize on a subscribed shard is a billing change, so presenting the size
controls and the subscription card as separate sections split one decision
across two places. They are now one card, size first, so the flow reads
"pick a size -> see the price -> subscribe".

The monthly gross price was previously never shown in our UI: it first
appeared on PayPal's own screen, for both subscribe and resize. German
consumer law requires the price to be visible before purchase, so both flows
now render it from the client-side pricing mirror before the buyer leaves for
PayPal. The resize flow also shows the delta against the current price.

The price renders only when the shard's volume size is known; the controller
rejects subscribe with 422 in that case, so a price computed against a 0GB
volume would be wrong rather than merely missing.

Selecting a size on an unsubscribed shard rendered no confirm button at all:
e5d25e9 replaced it with the PayPal revise container, which only renders for
an active subscription, orphaning resizeShard() and leaving trial shards
unable to resize. The confirm button is restored for that path.

The revise button gets a deliberate slot under the size buttons with a width
cap, instead of popping in fluid-width next to them.
After the buyer approved a resize, the subscribed path set waitingForRestart
and polled in place, leaving the user on a busy Settings page while the shard
restarted to apply the new VM size. It now routes to the restart screen, which
already polls the shard until it comes back, matching the unsubscribed path.

Resize is the only thing that routes to /restart, so the screen now returns to
Settings instead of the app grid: the user came from Settings and wants to see
the applied size.
The price-before-PayPal rule is a legal constraint and the pricing.js/controller
lockstep is a silent-failure trap; neither is visible from the code alone.
Also adds the previously undocumented src/lib/ helpers.
…mid-selection

The PayPal revise button was only rendered from the selectedSize watcher, so a
size selected before subscribing left an empty approval slot once the shard
became subscribed: resizeNeedsApproval flipped, the container entered the DOM,
but nothing rendered into it. Both triggers now go through one sync method.
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.

Settings: show price before PayPal (subscribe + resize) + merge size/subscription sections

1 participant