Migrate Hub billing page to the api's session-first flow#179
Migrate Hub billing page to the api's session-first flow#179tobihagemann wants to merge 5 commits into
Conversation
…g-api-migration Conflicts in assets/js/hubsubscription.js and layouts/hub-billing/single.html were resolved by keeping this branch's versions wholesale: both sides rewrote the same billing-page flow (this branch targets the new api manage/checkout endpoints, develop's store-migration phases 1+2 kept the legacy store flow with an api billing-session bootstrap), so a line-level merge is not meaningful. Dropped from develop as superseded: customer lookup with redacted email, the /billing/session + ?session= confirmation-link bootstrap, and token_transfer=session delivery back to the Hub (this branch currently only delivers the license via ?token=).
…sion-authorized manage and checkout
WalkthroughThis PR reworks Hub billing to support card and invoice checkout, hub-linked manage-subscription actions, and updated billing UI state. Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
data/de/eu_countries.yaml (1)
1-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider alphabetical ordering for the country dropdown.
Entries are listed in EU accession/treaty order rather than alphabetically by name (e.g.,
Frankreich,Deutschland,Griechenlandappear out of alphabetic sequence). Since this list feeds a<select>populated via{{ range (index $.Site.Data $.Site.Language.Lang).eu_countries }}inlayouts/hub-billing/single.html, sorting by localized name would improve usability for users scanning the dropdown.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/de/eu_countries.yaml` around lines 1 - 54, The eu_countries list is not sorted alphabetically by localized country name, which makes the dropdown harder to scan. Reorder the entries in eu_countries so the names appear in alphabetical sequence while keeping the same code/name pairs, and ensure the list consumed by the range in layouts/hub-billing/single.html remains alphabetically sorted for the German locale.data/en/eu_countries.yaml (1)
1-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSame ordering concern as the German counterpart.
The English list mirrors the same non-alphabetical accession order (e.g.,
France,Germany,Greeceinterleaved with earlier entries). Sorting both locale files alphabetically by displayed name would make the country picker easier to navigate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@data/en/eu_countries.yaml` around lines 1 - 54, The English EU country list in eu_countries.yaml should be reordered to match the alphabetical-by-name sorting requested for the German counterpart. Update the entries in the existing country list so the displayed names are in alphabetical order while keeping the same code/name pairs, and keep the ordering consistent with the locale file’s country-picker data structure.layouts/hub-billing/single.html (2)
467-474: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider
aria-pressedon the payment-method toggle buttons.These act as a two-state switcher but don't expose pressed/selected state to assistive tech.
♿ Suggested addition
-<button type="button" ... :class="{...: subscriptionData.paymentMethod === 'card'}" `@click.prevent`="subscriptionData.paymentMethod = 'card'"> +<button type="button" ... :class="{...: subscriptionData.paymentMethod === 'card'}" :aria-pressed="subscriptionData.paymentMethod === 'card'" `@click.prevent`="subscriptionData.paymentMethod = 'card'">🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@layouts/hub-billing/single.html` around lines 467 - 474, The payment-method toggle buttons in the hub billing checkout act like a two-state switch, but their selected state is not exposed to assistive technologies. Update the button markup in the payment method toggle section to include aria-pressed on both buttons, binding it to whether subscriptionData.paymentMethod equals 'card' or 'invoice' so screen readers can announce the active choice.
127-197: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueOptional: guard
subscriptionData.details.*accesses against null.
detailsis initialized tonull(line 6) and thesex-show/x-textbindings (status, seats, current_period_end, processor) dereference it directly. If this block is ever reactive beforedetailsis populated (e.g. via a parentx-showrather thanx-if), Alpine will log console errors. Optional chaining would make this resilient regardless of the parent gating mechanism.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@layouts/hub-billing/single.html` around lines 127 - 197, Guard the direct subscriptionData.details dereferences in the hub billing template so Alpine does not evaluate them while details is still null. Update the bindings in the status, seats, current_period_end, and processor checks within the single.html subscription section to use null-safe access or an equivalent guard, especially around the x-show, x-text, and disabled expressions tied to subscriptionData.details. Use the existing subscriptionData object and its details field as the locating symbols when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@data/de/eu_countries.yaml`:
- Around line 1-54: The eu_countries list is not sorted alphabetically by
localized country name, which makes the dropdown harder to scan. Reorder the
entries in eu_countries so the names appear in alphabetical sequence while
keeping the same code/name pairs, and ensure the list consumed by the range in
layouts/hub-billing/single.html remains alphabetically sorted for the German
locale.
In `@data/en/eu_countries.yaml`:
- Around line 1-54: The English EU country list in eu_countries.yaml should be
reordered to match the alphabetical-by-name sorting requested for the German
counterpart. Update the entries in the existing country list so the displayed
names are in alphabetical order while keeping the same code/name pairs, and keep
the ordering consistent with the locale file’s country-picker data structure.
In `@layouts/hub-billing/single.html`:
- Around line 467-474: The payment-method toggle buttons in the hub billing
checkout act like a two-state switch, but their selected state is not exposed to
assistive technologies. Update the button markup in the payment method toggle
section to include aria-pressed on both buttons, binding it to whether
subscriptionData.paymentMethod equals 'card' or 'invoice' so screen readers can
announce the active choice.
- Around line 127-197: Guard the direct subscriptionData.details dereferences in
the hub billing template so Alpine does not evaluate them while details is still
null. Update the bindings in the status, seats, current_period_end, and
processor checks within the single.html subscription section to use null-safe
access or an equivalent guard, especially around the x-show, x-text, and
disabled expressions tied to subscriptionData.details. Use the existing
subscriptionData object and its details field as the locating symbols when
making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c864dee-8dad-4a11-9584-0c965e44e55e
📒 Files selected for processing (7)
assets/js/hubsubscription.jsdata/de/eu_countries.yamldata/en/eu_countries.yamli18n/de.yamli18n/en.yamllayouts/hub-billing/single.htmllayouts/partials/hub-license-block.html
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/production/params.yaml`:
- Around line 26-29: Populate the EspoCRM product ID placeholders in the
production params so `invoiceProductId()` in `hubsubscription.js` always
receives valid values for both self-hosted and managed plans. Update
`espocrmHubSelfHostedProductId` and `espocrmHubManagedProductId` in the
production configuration with the real production product IDs, and verify the
values used by `loadInvoicePrice()` and `invoiceCheckout()` are non-empty before
deploying.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7eebd041-d444-4318-aa65-83e3522d99e5
📒 Files selected for processing (8)
assets/js/const.template.jsassets/js/hubsubscription.jsconfig/development/params.yamlconfig/production/params.yamlconfig/staging/params.yamli18n/de.yamli18n/en.yamllayouts/hub-billing/single.html
✅ Files skipped from review due to trivial changes (2)
- config/staging/params.yaml
- assets/js/const.template.js
🚧 Files skipped from review as they are similar to previous changes (3)
- i18n/de.yaml
- i18n/en.yaml
- assets/js/hubsubscription.js
| # ESPOCRM | ||
| espocrmHubSelfHostedProductId: # TODO: set production EspoCRM Self-Hosted Standard product id | ||
| espocrmHubManagedProductId: # TODO: set production EspoCRM Managed Standard product id | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fill in production EspoCRM product IDs before deploy.
These placeholders are empty with TODO markers. hubsubscription.js's invoiceProductId() returns this value directly and feeds it into both loadInvoicePrice() (which silently no-ops on falsy/empty values, so the invoice price never loads) and invoiceCheckout() (which POSTs it as product_id regardless of emptiness). Deploying with these unset will silently break the invoice checkout flow in production.
Ensure these are populated with the actual EspoCRM Self-Hosted/Managed product IDs before this PR ships to production.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/production/params.yaml` around lines 26 - 29, Populate the EspoCRM
product ID placeholders in the production params so `invoiceProductId()` in
`hubsubscription.js` always receives valid values for both self-hosted and
managed plans. Update `espocrmHubSelfHostedProductId` and
`espocrmHubManagedProductId` in the production configuration with the real
production product IDs, and verify the values used by `loadInvoicePrice()` and
`invoiceCheckout()` are non-empty before deploying.
Important
Requires cryptomator/api#34 (and its base cryptomator/api#33) to be deployed. The page hard depends on the new endpoints, so both sides need to ship together.
Moves the Hub billing page off the legacy store's manage endpoints and onto the api, with the billing session as the single credential. Opening the page from a Hub now starts with a customer lookup (redacted email for known Hubs, email entry for new ones) and a confirmation link. The emailed
?session=link resolves the session and routes on itsbillingId: linked sessions go to the manage view, new customers to checkout (card via Paddle, invoice via the new EspoCRM checkout). Manage calls authorize withAuthorization: Bearer <session>, checkouts carry the session so the purchase gets linked back, and the license comes fromGET /licenses/hub?session=&legacy=without a captcha. Delivery back to the Hub honorstoken_transfer:?token=for older Hubs,?session=for newer ones. Expired or reused links get their own screen.Invoice checkout got the same cost transparency as the Paddle overlay: picking "Pay by Invoice" locks the interval to yearly, loads the exact price from
GET /billing/espocrm/checkout/price(the same price-book lookup the api freezes on the subscription, so the shown total is the invoiced total), and puts an order summary in front of the binding buy button: product, seats, unit price, total with VAT or reverse-charge hint, billing address, and email. Seat changes for invoice customers show the prorated charge (or credit) and the new yearly total fromPOST /billing/manage/subscription/{hubId}/seats/previewin the confirm dialog. The two self-checkout products (Managed and Self-Hosted Standard) are configured viaespocrmHubManagedProductId/espocrmHubSelfHostedProductIdparams; production values are still TODO.State Machine