From 6c1b726f7d2da487e7db01b8675a73a1f9095d76 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 16 Jun 2026 17:26:40 +0200 Subject: [PATCH 1/2] phase 1: look up hub customer --- assets/js/hubsubscription.js | 115 ++++++++++++++++++++++++++++---- i18n/de.yaml | 9 +++ i18n/en.yaml | 11 ++- layouts/hub-billing/single.html | 64 ++++++++++++++---- 4 files changed, 174 insertions(+), 25 deletions(-) diff --git a/assets/js/hubsubscription.js b/assets/js/hubsubscription.js index 2edc6d4cf..85c3c4056 100644 --- a/assets/js/hubsubscription.js +++ b/assets/js/hubsubscription.js @@ -1,6 +1,7 @@ "use strict"; -const BILLING_PORTAL_SESSION_URL = LEGACY_STORE_URL + '/hub/billing-portal-session'; +const BILLING_SESSION_URL = API_BASE_URL + '/billing/session'; +const BILLING_CUSTOMER_URL = API_BASE_URL + '/billing/customers/by-hub-id'; const CUSTOM_BILLING_URL = LEGACY_STORE_URL + '/hub/custom-billing'; const GENERATE_PAY_LINK_URL = LEGACY_STORE_URL + '/hub/generate-pay-link'; const MANAGE_SUBSCRIPTION_URL = LEGACY_STORE_URL + '/hub/manage-subscription'; @@ -23,15 +24,26 @@ class HubSubscription { this._subscriptionData.oldLicense = null; } } - this._subscriptionData.hubId = this._subscriptionData.hubId ?? searchParams.get('hub_id'); - let returnUrl = fragmentParams.get('returnUrl') ?? searchParams.get('return_url'); + this._subscriptionData.hubId = this._subscriptionData.hubId ?? fragmentParams.get('hub_id') ?? searchParams.get('hub_id'); + // Record whether the returnUrl arrived in the URL fragment or the query string as `fragmentOrQuery` + // ('#' or '?'), so the billing API knows how to reconstruct the redirect later. If both carry it, the + // fragment wins. + let returnUrlInFragment = fragmentParams.get('returnUrl'); + let returnUrl = returnUrlInFragment ?? searchParams.get('return_url'); if (returnUrl) { this._subscriptionData.returnUrl = returnUrl; + this._subscriptionData.fragmentOrQuery = returnUrlInFragment ? '#' : '?'; } this._subscriptionData.session = searchParams.get('session'); - if (this._subscriptionData.hubId && this._subscriptionData.hubId.length > 0 && this._subscriptionData.returnUrl && this._subscriptionData.returnUrl.length > 0) { + if (this._subscriptionData.session) { + // We returned from the confirmation link (/hub/billing?session=): resolve the + // verified billing session and continue into the existing subscription flow. this._subscriptionData.state = 'LOADING'; - this.loadSubscription(); + this.loadBillingSession(); + } else if (this._subscriptionData.hubId && this._subscriptionData.hubId.length > 0 && this._subscriptionData.returnUrl && this._subscriptionData.returnUrl.length > 0) { + // Opened from the Hub without a verified session yet: ask the customer to request a + // confirmation link before we can manage their subscription. + this._subscriptionData.state = 'CREATE_SESSION'; } this._paddle = $.ajax({ url: 'https://cdn.paddle.com/paddle/paddle.js', @@ -94,6 +106,79 @@ class HubSubscription { this._subscriptionData.inProgress = false; } + loadBillingSession() { + this._subscriptionData.inProgress = true; + this._subscriptionData.errorMessage = ''; + $.ajax({ + url: BILLING_SESSION_URL + '/' + encodeURIComponent(this._subscriptionData.session), + type: 'GET' + }).done(data => { + this.onLoadBillingSessionSucceeded(data); + }).fail(xhr => { + this.onLoadBillingSessionFailed(xhr.status, xhr.responseJSON?.message || 'Loading billing session failed.'); + }); + } + + onLoadBillingSessionSucceeded(data) { + this._subscriptionData.hubId = data.hubId; + this._subscriptionData.email = data.email; + this._subscriptionData.returnUrl = data.returnUrl; + this._subscriptionData.fragmentOrQuery = data.fragmentOrQuery; + this._subscriptionData.errorMessage = ''; + // The session is verified; hand off to the existing subscription flow (store + Paddle). + this.loadSubscription(); + } + + onLoadBillingSessionFailed(status, error) { + if (status == 404) { + this._subscriptionData.state = 'LINK_EXPIRED'; + this._subscriptionData.errorMessage = ''; + } else { + this._subscriptionData.state = 'CREATE_SESSION'; + this._subscriptionData.errorMessage = error; + } + this._subscriptionData.inProgress = false; + } + + lookupCustomer() { + this._subscriptionData.inProgress = true; + this._subscriptionData.errorMessage = ''; + // First challenge (from /billing/customers/challenge) gates this lookup: it tells us whether + // the Hub is already linked to a customer before we ask for a confirmation link. + $.ajax({ + url: BILLING_CUSTOMER_URL + '/' + encodeURIComponent(this._subscriptionData.hubId) + '?captcha=' + encodeURIComponent(this._subscriptionData.captcha), + type: 'GET' + }).done(data => { + this.onLookupCustomerSucceeded(data); + }).fail(xhr => { + this.onLookupCustomerFailed(xhr.status, xhr.responseJSON?.message || 'Looking up your subscription failed.'); + }); + } + + onLookupCustomerSucceeded(data) { + // The Hub is already linked to a customer: the API returns their redacted email so we can + // show where the confirmation link will be sent without revealing the full address. + this._subscriptionData.redactedEmail = data.email; + this._subscriptionData.needsEmail = false; + this._subscriptionData.lookupDone = true; + this._subscriptionData.errorMessage = ''; + this._subscriptionData.inProgress = false; + } + + onLookupCustomerFailed(status, error) { + this._subscriptionData.inProgress = false; + if (status == 404) { + // The Hub is not linked to a customer yet: ask for the purchase email so the session + // request can be created for that address. + this._subscriptionData.needsEmail = true; + this._subscriptionData.redactedEmail = null; + this._subscriptionData.lookupDone = true; + this._subscriptionData.errorMessage = ''; + } else { + this._subscriptionData.errorMessage = error; + } + } + createSession() { if (!$(this._form)[0].checkValidity()) { $(this._form).find(':input').addClass('show-invalid'); @@ -103,18 +188,24 @@ class HubSubscription { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; + let body = { + hubId: this._subscriptionData.hubId, + returnUrl: this._subscriptionData.returnUrl, + fragmentOrQuery: this._subscriptionData.fragmentOrQuery, + captcha: this._subscriptionData.captcha + }; + if (this._subscriptionData.email) { + body.email = this._subscriptionData.email; + } $.ajax({ - url: BILLING_PORTAL_SESSION_URL, + url: BILLING_SESSION_URL, type: 'POST', - data: { - captcha: this._subscriptionData.captcha, - hub_id: this._subscriptionData.hubId, - return_url: this._subscriptionData.returnUrl - } + contentType: 'application/json', + data: JSON.stringify(body) }).done(_ => { this.onCreateSessionSucceeded(); }).fail(xhr => { - this.onCreateSessionFailed(xhr.responseJSON?.message || 'Creating billing portal session failed.'); + this.onCreateSessionFailed(xhr.responseJSON?.message || 'Requesting confirmation link failed.'); }); } diff --git a/i18n/de.yaml b/i18n/de.yaml index 5a1c9ca17..9e552a1d8 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -508,6 +508,15 @@ translation: "Bitte überprüfe deine E-Mails auf den Link zur Verwaltung deines Hub-Abonnements." - id: hub_billing_createsession_submit translation: "Zugang anfordern" +- id: hub_billing_createsession_email_description + translation: "Wir konnten diesem Hub kein bestehendes Abonnement zuordnen. Bitte gib die E-Mail-Adresse ein, die du für den Kauf verwendet hast, damit wir dir den Bestätigungslink senden können." +- id: hub_billing_createsession_email_placeholder + translation: "Deine E-Mail-Adresse" +- id: hub_billing_createsession_found_description + translation: "Wir haben ein bestehendes Abonnement für diesen Hub gefunden. Wir senden den Bestätigungslink an:" + +- id: hub_billing_linkexpired_description + translation: "Dieser Link ist nicht mehr gültig. Bestätigungslinks können nur einmal verwendet werden und verfallen nach einer Weile. Bitte fordere über deine Hub-Instanz einen neuen Link an." - id: hub_billing_manage_status_title translation: "Status" diff --git a/i18n/en.yaml b/i18n/en.yaml index b8955a1d3..4b7985988 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -503,11 +503,20 @@ translation: "Please wait while we're loading your subscription information." - id: hub_billing_createsession_description - translation: "To access your subscription information, a confirmation link will be sent to the email you used to make your purchase." + translation: "Looking up Hub instance details..." - id: hub_billing_createsession_success_description translation: "Please check your email for the link to manage your Hub subscription." - id: hub_billing_createsession_submit translation: "Request Access" +- id: hub_billing_createsession_email_description + translation: "This looks like a new Hub instance. Confirm your email address to proceed." +- id: hub_billing_createsession_email_placeholder + translation: "Your email address" +- id: hub_billing_createsession_found_description + translation: "We found an existing subscription for this Hub. We'll send the confirmation link to:" + +- id: hub_billing_linkexpired_description + translation: "This link is no longer valid. Confirmation links can only be used once and expire after a while. Please request a new link from your Hub instance." - id: hub_billing_manage_status_title translation: "Status" diff --git a/layouts/hub-billing/single.html b/layouts/hub-billing/single.html index b237b57d8..a8d19c7b8 100644 --- a/layouts/hub-billing/single.html +++ b/layouts/hub-billing/single.html @@ -3,7 +3,7 @@ {{ end }} {{ define "main" }}
-
+ @@ -60,6 +88,18 @@

+ +