diff --git a/assets/js/const.template.js b/assets/js/const.template.js index f845d1351..32725b6df 100644 --- a/assets/js/const.template.js +++ b/assets/js/const.template.js @@ -13,6 +13,8 @@ const PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID = {{ .Site.Params.paddleHubManagedMonth const PADDLE_PRICES_URL = '{{ .Site.Params.paddlePricesUrl }}'; const PADDLE_DESKTOP_SALE_PRICE_ID = '{{ .Site.Params.paddleDesktopSalePriceId }}'; const PADDLE_ANDROID_SALE_PRICE_ID = '{{ .Site.Params.paddleAndroidSalePriceId }}'; +const ESPOCRM_HUB_SELF_HOSTED_PRODUCT_ID = '{{ .Site.Params.espocrmHubSelfHostedProductId }}'; +const ESPOCRM_HUB_MANAGED_PRODUCT_ID = '{{ .Site.Params.espocrmHubManagedProductId }}'; const LEGACY_STORE_URL = '{{ .Site.Params.legacyStoreUrl }}'; const HUB_MANAGED_DOMAIN = '{{ .Site.Params.hubManagedDomain }}'; const STRIPE_PK = '{{ .Site.Params.stripePk }}'; diff --git a/assets/js/hubsubscription.js b/assets/js/hubsubscription.js index b5a3dd6a7..fd9351ad0 100644 --- a/assets/js/hubsubscription.js +++ b/assets/js/hubsubscription.js @@ -2,10 +2,11 @@ const BILLING_SESSION_URL = API_BASE_URL + '/billing/session'; const BILLING_CUSTOMER_URL = API_BASE_URL + '/billing/customers/by-hub-id'; +const CARD_CHECKOUT_URL = API_BASE_URL + '/billing/paddle-classic/checkout'; +const INVOICE_CHECKOUT_URL = API_BASE_URL + '/billing/espocrm/checkout'; +const INVOICE_PRICE_URL = API_BASE_URL + '/billing/espocrm/checkout/price'; +const MANAGE_SUBSCRIPTION_BASE_URL = API_BASE_URL + '/billing/manage/subscription'; 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'; -const UPDATE_PAYMENT_METHOD_URL = LEGACY_STORE_URL + '/hub/update-payment-method'; const GET_LICENSE_URL = API_BASE_URL + '/licenses/hub'; class HubSubscription { @@ -15,11 +16,8 @@ class HubSubscription { this._subscriptionData = subscriptionData; this._subscriptionData.oldLicense = searchParams.get('oldLicense'); if (this._subscriptionData.oldLicense) { - try { - let base64 = this._subscriptionData.oldLicense.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); - this._subscriptionData.hubId = JSON.parse(atob(base64)).jti; - } catch (e) { - console.error('Failed to parse hub token:', e); + this._subscriptionData.hubId = this.extractHubId(this._subscriptionData.oldLicense); + if (!this._subscriptionData.hubId) { this._subscriptionData.oldLicense = null; } } @@ -29,17 +27,17 @@ class HubSubscription { this._subscriptionData.returnUrl = returnUrl; } // Capture the Hub's `token_transfer` value (how the license should be delivered) so it can be stored in - // the billing session; default to delivering it as a query parameter. + // the billing session. this._subscriptionData.tokenTransfer = searchParams.get('token_transfer') ?? 'queryParam'; this._subscriptionData.session = searchParams.get('session'); 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. + // billing session and continue into the manage or checkout flow. this._subscriptionData.state = 'LOADING'; 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. + // confirmation link before we can manage their subscription or check out. this._subscriptionData.state = 'CREATE_SESSION'; } this._paddle = $.ajax({ @@ -55,46 +53,74 @@ class HubSubscription { }); } - loadSubscription() { + extractHubId(license) { + try { + let base64 = license.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(base64)).jti; + } catch (e) { + console.error('Failed to parse hub token:', e); + return null; + } + } + + authHeaders() { + return { Authorization: 'Bearer ' + this._subscriptionData.session }; + } + + loadCheckoutPrerequisites() { this.loadCustomBilling(() => { + if (this._subscriptionData.customBilling?.manual_invoice) { + this._subscriptionData.state = 'MANUAL_INVOICE'; + return; + } this.loadPrice(() => { - this._subscriptionData.inProgress = true; + this._subscriptionData.state = 'NEW_CUSTOMER'; this._subscriptionData.errorMessage = ''; - $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'GET', - data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session - } - }).done(data => { - this.onLoadSubscriptionSucceeded(data); - }).fail(xhr => { - this.onLoadSubscriptionFailed(xhr.status, xhr.responseJSON?.message || 'Loading subscription failed.'); - }); + this._subscriptionData.inProgress = false; + }); + }); + } + + loadManageSubscription() { + this.loadCustomBilling(() => { + this._subscriptionData.inProgress = true; + this._subscriptionData.errorMessage = ''; + $.ajax({ + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}`, + type: 'GET', + headers: this.authHeaders() + }).done(data => { + this.onLoadSubscriptionSucceeded(data); + }).fail(xhr => { + this.onLoadSubscriptionFailed(xhr.status, xhr.responseJSON?.message || 'Loading subscription failed.'); }); }); } onLoadSubscriptionSucceeded(data) { - this._subscriptionData.details = data.subscription; - if (data.subscription.quantity) { - this._subscriptionData.quantity = data.subscription.quantity; - } + this._subscriptionData.details = { + processor: data.processor, + status: data.status, + seats: data.seats, + current_period_end: data.current_period_end + }; + this._subscriptionData.quantity = data.seats; this._subscriptionData.state = 'EXISTING_CUSTOMER'; this._subscriptionData.errorMessage = ''; this._subscriptionData.inProgress = false; - this._subscriptionData.needsTokenRefresh = true; + this.refreshToken(); } onLoadSubscriptionFailed(status, error) { - if (status == 404) { - this._subscriptionData.state = 'NEW_CUSTOMER'; - this._subscriptionData.errorMessage = ''; - } else if (status == 400) { - // Assuming that the error is due to the session being missing. + if (status == 401) { this._subscriptionData.state = 'CREATE_SESSION'; this._subscriptionData.errorMessage = ''; + } else if (status == 404 && this._subscriptionData.returnUrl) { + this.loadCheckoutPrerequisites(); + return; + } else if (status == 404) { + this._subscriptionData.state = 'MISSING_PARAMS'; + this._subscriptionData.errorMessage = ''; } else { this._subscriptionData.state = 'CREATE_SESSION'; this._subscriptionData.errorMessage = error; @@ -120,18 +146,29 @@ class HubSubscription { this._subscriptionData.email = data.email; this._subscriptionData.returnUrl = data.returnUrl; this._subscriptionData.tokenTransfer = data.tokenTransfer; + if (!this._subscriptionData.invoice.contact_email) { + this._subscriptionData.invoice.contact_email = data.email; + } this._subscriptionData.errorMessage = ''; - // The session is verified; hand off to the existing subscription flow (store + Paddle). - this.loadSubscription(); + // The session is verified; a session already linked to a billing manages it (the manage endpoints + // only accept linked sessions), an unlinked one belongs to a new customer heading into checkout. + if (data.billingId) { + this.loadManageSubscription(); + } else { + this.loadCheckoutPrerequisites(); + } } onLoadBillingSessionFailed(status, error) { if (status == 404) { this._subscriptionData.state = 'LINK_EXPIRED'; this._subscriptionData.errorMessage = ''; - } else { + } else if (this._subscriptionData.hubId) { this._subscriptionData.state = 'CREATE_SESSION'; this._subscriptionData.errorMessage = error; + } else { + this._subscriptionData.state = 'MISSING_PARAMS'; + this._subscriptionData.errorMessage = ''; } this._subscriptionData.inProgress = false; } @@ -142,8 +179,11 @@ class HubSubscription { // 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' + url: BILLING_CUSTOMER_URL + '/' + encodeURIComponent(this._subscriptionData.hubId), + type: 'GET', + data: { + captcha: this._subscriptionData.captcha + } }).done(data => { this.onLookupCustomerSucceeded(data); }).fail(xhr => { @@ -162,17 +202,15 @@ class HubSubscription { } onLookupCustomerFailed(status, error) { + // 404 means the Hub is not linked to a customer yet: ask for the purchase email. Any other + // failure falls back to the same manual entry (the lookup captcha solves only once, so a + // transient failure cannot re-trigger the lookup, and for a known hub the server ignores the + // entered address and mails the one on file) — but keeps the error visible. 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; - } + this._subscriptionData.needsEmail = true; + this._subscriptionData.redactedEmail = null; + this._subscriptionData.lookupDone = true; + this._subscriptionData.errorMessage = status == 404 ? '' : error; } createSession() { @@ -227,11 +265,7 @@ class HubSubscription { } }).done(data => { this.onLoadCustomBillingSucceeded(data); - if (data.custom_billing.manual_invoice) { - this._subscriptionData.state = 'MANUAL_INVOICE'; - } else { - continueHandler(); - } + continueHandler(); }).fail(xhr => { this.onLoadCustomBillingFailed(xhr.status, xhr.responseJSON?.message || 'Loading custom billing options failed.'); if (xhr.status == 404 && xhr.responseJSON?.status == 'error') { @@ -329,6 +363,16 @@ class HubSubscription { this._subscriptionData.inProgress = false; } + selectedPlanId() { + let isManaged = this._subscriptionData.customBilling?.managed; + let isMonthly = this._subscriptionData.billingInterval === 'monthly'; + if (isManaged) { + return isMonthly ? PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID : PADDLE_HUB_MANAGED_YEARLY_PLAN_ID; + } else { + return isMonthly ? PADDLE_HUB_SELF_HOSTED_MONTHLY_PLAN_ID : PADDLE_HUB_SELF_HOSTED_YEARLY_PLAN_ID; + } + } + checkout(locale) { if (!$(this._form)[0].checkValidity()) { $(this._form).find(':input').addClass('show-invalid'); @@ -338,30 +382,24 @@ class HubSubscription { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; - let isManaged = this._subscriptionData.customBilling?.managed; - let isMonthly = this._subscriptionData.billingInterval === 'monthly'; - let planId; - if (isManaged) { - planId = isMonthly ? PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID : PADDLE_HUB_MANAGED_YEARLY_PLAN_ID; - } else { - planId = isMonthly ? PADDLE_HUB_SELF_HOSTED_MONTHLY_PLAN_ID : PADDLE_HUB_SELF_HOSTED_YEARLY_PLAN_ID; - } - this.customCheckout(planId, locale); + this.customCheckout(this.selectedPlanId(), locale); } customCheckout(productId, locale) { $.ajax({ - url: GENERATE_PAY_LINK_URL, + url: CARD_CHECKOUT_URL, type: 'POST', data: { + captcha: this._subscriptionData.cardCaptcha, hub_id: this._subscriptionData.hubId, product_id: productId, - quantity: this._subscriptionData.quantity + quantity: this._subscriptionData.quantity, + session: this._subscriptionData.session } }).done(data => { this.openPaddleCheckout(data.pay_link, locale); }).fail(xhr => { - this.onPostFailed(xhr.responseJSON?.message || 'Generating pay link failed.'); + this.onPostFailed(xhr.responseJSON?.message || 'Checkout failed.'); }); } @@ -385,7 +423,7 @@ class HubSubscription { paddle.Order.details(checkoutId, data => { let subscriptionId = data.order.subscription_id; if (subscriptionId) { - this.post(subscriptionId); + this.onCheckoutSucceeded(); } else { this._subscriptionData.errorMessage = 'Retrieving subscription failed. Please check your emails instead.'; } @@ -393,29 +431,78 @@ class HubSubscription { }); } - post(subscriptionId) { + invoiceProductId() { + return this._subscriptionData.customBilling?.managed ? ESPOCRM_HUB_MANAGED_PRODUCT_ID : ESPOCRM_HUB_SELF_HOSTED_PRODUCT_ID; + } + + loadInvoicePrice() { + if (this._subscriptionData.invoicePrice || !this.invoiceProductId()) { + return; + } + $.ajax({ + url: INVOICE_PRICE_URL, + type: 'GET', + data: { + product_id: this.invoiceProductId() + } + }).done(data => { + this._subscriptionData.invoicePrice = data; + }); + } + + askForInvoiceConfirmation() { + if (!$(this._form)[0].checkValidity()) { + $(this._form).find(':input').addClass('show-invalid'); + this._subscriptionData.errorMessage = 'Please fill in all required fields.'; + return; + } + this._subscriptionData.errorMessage = ''; + this._subscriptionData.invoiceConfirmModal.open = true; + } + + invoiceCheckout() { + if (!$(this._form)[0].checkValidity()) { + $(this._form).find(':input').addClass('show-invalid'); + this._subscriptionData.errorMessage = 'Please fill in all required fields.'; + return; + } + + this._subscriptionData.inProgress = true; + this._subscriptionData.errorMessage = ''; + let invoice = this._subscriptionData.invoice; $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, + url: INVOICE_CHECKOUT_URL, type: 'POST', data: { + captcha: this._subscriptionData.invoiceCaptcha, hub_id: this._subscriptionData.hubId, + product_id: this.invoiceProductId(), + quantity: this._subscriptionData.quantity, session: this._subscriptionData.session, - subscription_id: subscriptionId + account_name: invoice.account_name, + vat_id: invoice.vat_id, + address_street: invoice.address_street, + address_postal_code: invoice.address_postal_code, + address_city: invoice.address_city, + address_country: invoice.address_country, + contact_first_name: invoice.contact_first_name, + contact_last_name: invoice.contact_last_name, + contact_email: invoice.contact_email } - }).done(data => { - this.onPostSucceeded(data); + }).done(_ => { + this.onCheckoutSucceeded(); }).fail(xhr => { - this.onPostFailed(xhr.responseJSON?.message || 'Adding subscription failed.'); + this.onPostFailed(xhr.responseJSON?.message || 'Creating subscription failed.'); }); } - onPostSucceeded(data) { - this._subscriptionData.state = 'EXISTING_CUSTOMER'; - this._subscriptionData.details = data.subscription; + onCheckoutSucceeded() { + this._subscriptionData.state = 'CHECKOUT_SUCCESS'; + this._subscriptionData.invoiceConfirmModal.open = false; this._subscriptionData.errorMessage = ''; this._subscriptionData.inProgress = false; - this._subscriptionData.shouldTransferToHub = true; - this._subscriptionData.needsTokenRefresh = true; + this._subscriptionData.shouldTransferToHub = !!this._subscriptionData.returnUrl; + this.refreshToken(); } onPostFailed(error) { @@ -426,20 +513,17 @@ class HubSubscription { updatePaymentMethod(locale) { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; + this._subscriptionData.shouldTransferToHub = false; $.ajax({ - url: UPDATE_PAYMENT_METHOD_URL, + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}/payment-method`, type: 'GET', - data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, - subscription_id: this._subscriptionData.details.subscription_id - } + headers: this.authHeaders() }).done(data => { this._paddle.then(paddle => { paddle.Checkout.open({ override: data.url, locale: locale, - successCallback: _ => this.loadSubscription(), + successCallback: _ => this.loadManageSubscription(), closeCallback: () => { this._subscriptionData.inProgress = false; } @@ -453,69 +537,43 @@ class HubSubscription { pause() { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; + // a stale transfer intent from an earlier action must not redirect after this refresh + this._subscriptionData.shouldTransferToHub = false; $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'PUT', - data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, - pause: true - } - }).done(data => { - this.onPutSucceeded(data, false); + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}/pause`, + type: 'POST', + headers: this.authHeaders() + }).done(_ => { + this.loadManageSubscription(); }).fail(xhr => { this.onPutFailed(xhr.status, xhr.responseJSON?.message || 'Updating subscription failed.'); }); } askForRestartConfirmation() { - this._subscriptionData.restartModal.nextPayment = null; this._subscriptionData.restartModal.open = true; - this.previewRestart(); - } - - previewRestart() { - this._subscriptionData.inProgress = true; - this._subscriptionData.errorMessage = ''; - $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'PUT', - data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, - pause: false, - preview: true - } - }).done(data => { - this._subscriptionData.restartModal.nextPayment = data.subscription.next_payment; - this._subscriptionData.errorMessage = ''; - this._subscriptionData.inProgress = false; - }).fail(xhr => { - this.onPutFailed(xhr.status, xhr.responseJSON?.message || 'Calculating price failed.'); - }); } restart() { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'PUT', - data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, - pause: false - } - }).done(data => { - this.onPutSucceeded(data, this._subscriptionData.details.state == 'paused'); + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}/resume`, + type: 'POST', + headers: this.authHeaders() + }).done(_ => { + this._subscriptionData.restartModal.open = false; + this._subscriptionData.shouldTransferToHub = !!this._subscriptionData.returnUrl; + this.loadManageSubscription(); }).fail(xhr => { this.onPutFailed(xhr.status, xhr.responseJSON?.message || 'Updating subscription failed.'); }); } openChangeSeatsModal() { - this._subscriptionData.quantity = this._subscriptionData.details.quantity; - this._subscriptionData.changeSeatsModal.immediatePayment = null; + this._subscriptionData.quantity = this._subscriptionData.details.seats; + this._subscriptionData.changeSeatsModal.nextPayment = null; + this._subscriptionData.changeSeatsModal.invoicePreview = null; this._subscriptionData.changeSeatsModal.confirmation = false; this._subscriptionData.changeSeatsModal.open = true; } @@ -528,23 +586,24 @@ class HubSubscription { } this._subscriptionData.changeSeatsModal.confirmation = true; - this.previewChangeQuantity(); + if (this._subscriptionData.details.processor == 'PADDLE_CLASSIC' || this._subscriptionData.details.processor == 'ESPOCRM') { + this.previewChangeQuantity(); + } } previewChangeQuantity() { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'PUT', + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}/seats/preview`, + type: 'POST', + headers: this.authHeaders(), data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, - quantity: this._subscriptionData.quantity, - preview: true + quantity: this._subscriptionData.quantity } }).done(data => { - this._subscriptionData.changeSeatsModal.immediatePayment = data.subscription.immediate_payment; + this._subscriptionData.changeSeatsModal.nextPayment = data.next_payment; + this._subscriptionData.changeSeatsModal.invoicePreview = data.prorated_amount != null ? data : null; this._subscriptionData.errorMessage = ''; this._subscriptionData.inProgress = false; }).fail(xhr => { @@ -556,31 +615,21 @@ class HubSubscription { this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; $.ajax({ - url: MANAGE_SUBSCRIPTION_URL, - type: 'PUT', + url: `${MANAGE_SUBSCRIPTION_BASE_URL}/${this._subscriptionData.hubId}/seats`, + type: 'POST', + headers: this.authHeaders(), data: { - hub_id: this._subscriptionData.hubId, - session: this._subscriptionData.session, quantity: this._subscriptionData.quantity } - }).done(data => { + }).done(_ => { this._subscriptionData.changeSeatsModal.open = false; - this.onPutSucceeded(data, true); + this._subscriptionData.shouldTransferToHub = !!this._subscriptionData.returnUrl; + this.loadManageSubscription(); }).fail(xhr => { this.onPutFailed(xhr.status, xhr.responseJSON?.message || 'Updating subscription failed.'); }); } - onPutSucceeded(data, shouldOpenReturnUrl) { - this._subscriptionData.details = data.subscription; - this._subscriptionData.errorMessage = ''; - this._subscriptionData.inProgress = false; - if (shouldOpenReturnUrl) { - this._subscriptionData.shouldTransferToHub = true; - this._subscriptionData.needsTokenRefresh = true; - } - } - onPutFailed(status, error) { if (status == 401) { this._subscriptionData.state = 'CREATE_SESSION'; @@ -590,6 +639,7 @@ class HubSubscription { } refreshToken() { + this._subscriptionData.needsTokenRefresh = true; this._subscriptionData.inProgress = true; this._subscriptionData.errorMessage = ''; $.ajax({ @@ -608,6 +658,8 @@ class HubSubscription { this.transferTokenToHub(); } }).fail(xhr => { + // Expected for a card checkout until Paddle's payment webhook links the session to the new + // billing; the license block then offers a retry. this._subscriptionData.errorMessage = xhr.responseJSON?.message || 'Refreshing license failed.'; this._subscriptionData.needsTokenRefresh = false; this._subscriptionData.inProgress = false; @@ -616,7 +668,6 @@ class HubSubscription { transferTokenToHub() { if (this._subscriptionData.tokenTransfer === 'queryParam') { - // Deliver the refreshed license to the Hub directly as a query parameter. location.href = this._subscriptionData.returnUrl + '?token=' + encodeURIComponent(this._subscriptionData.token); } else if (this._subscriptionData.tokenTransfer === 'session') { // Hand the Hub the billing session id instead; it resolves the license itself. diff --git a/config/development/params.yaml b/config/development/params.yaml index b616dd930..5bd6cf92b 100644 --- a/config/development/params.yaml +++ b/config/development/params.yaml @@ -23,5 +23,9 @@ paddlePricesUrl: https://sandbox-checkout.paddle.com/api/2.0/prices paddleDesktopSalePriceId: pri_01kk1z8f8cb5f6fnf9x7bv4xg9 paddleAndroidSalePriceId: pri_01kk243hedea8mbabs8q22ygdn +# ESPOCRM +espocrmHubSelfHostedProductId: 69bd302d5c65eabaa +espocrmHubManagedProductId: 69bd302d521a70103 + # STRIPE stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF diff --git a/config/production/params.yaml b/config/production/params.yaml index a7b353c00..42f477823 100644 --- a/config/production/params.yaml +++ b/config/production/params.yaml @@ -23,5 +23,9 @@ paddlePricesUrl: https://checkout.paddle.com/api/2.0/prices paddleDesktopSalePriceId: pri_01kk4gejs06jg0m0n9tghk0ktr paddleAndroidSalePriceId: pri_01kk4gg6c3pj0tq0vvpf4x4vw2 +# ESPOCRM +espocrmHubSelfHostedProductId: # TODO: set production EspoCRM Self-Hosted Standard product id +espocrmHubManagedProductId: # TODO: set production EspoCRM Managed Standard product id + # STRIPE stripePk: pk_live_eSasX216vGvC26GdbVwA011V diff --git a/config/staging/params.yaml b/config/staging/params.yaml index fa0b63b11..8be0ea28b 100644 --- a/config/staging/params.yaml +++ b/config/staging/params.yaml @@ -23,5 +23,9 @@ paddlePricesUrl: https://sandbox-checkout.paddle.com/api/2.0/prices paddleDesktopSalePriceId: pri_01kk1z8f8cb5f6fnf9x7bv4xg9 paddleAndroidSalePriceId: pri_01kk243hedea8mbabs8q22ygdn +# ESPOCRM +espocrmHubSelfHostedProductId: 69bd302d5c65eabaa +espocrmHubManagedProductId: 69bd302d521a70103 + # STRIPE stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF diff --git a/data/de/eu_countries.yaml b/data/de/eu_countries.yaml new file mode 100644 index 000000000..fd975739e --- /dev/null +++ b/data/de/eu_countries.yaml @@ -0,0 +1,54 @@ +- code: AT + name: Österreich +- code: BE + name: Belgien +- code: BG + name: Bulgarien +- code: HR + name: Kroatien +- code: CY + name: Zypern +- code: CZ + name: Tschechien +- code: DK + name: Dänemark +- code: EE + name: Estland +- code: FI + name: Finnland +- code: FR + name: Frankreich +- code: DE + name: Deutschland +- code: GR + name: Griechenland +- code: HU + name: Ungarn +- code: IE + name: Irland +- code: IT + name: Italien +- code: LV + name: Lettland +- code: LT + name: Litauen +- code: LU + name: Luxemburg +- code: MT + name: Malta +- code: NL + name: Niederlande +- code: PL + name: Polen +- code: PT + name: Portugal +- code: RO + name: Rumänien +- code: SK + name: Slowakei +- code: SI + name: Slowenien +- code: ES + name: Spanien +- code: SE + name: Schweden diff --git a/data/en/eu_countries.yaml b/data/en/eu_countries.yaml new file mode 100644 index 000000000..c0f1063df --- /dev/null +++ b/data/en/eu_countries.yaml @@ -0,0 +1,54 @@ +- code: AT + name: Austria +- code: BE + name: Belgium +- code: BG + name: Bulgaria +- code: HR + name: Croatia +- code: CY + name: Cyprus +- code: CZ + name: Czechia +- code: DK + name: Denmark +- code: EE + name: Estonia +- code: FI + name: Finland +- code: FR + name: France +- code: DE + name: Germany +- code: GR + name: Greece +- code: HU + name: Hungary +- code: IE + name: Ireland +- code: IT + name: Italy +- code: LV + name: Latvia +- code: LT + name: Lithuania +- code: LU + name: Luxembourg +- code: MT + name: Malta +- code: NL + name: Netherlands +- code: PL + name: Poland +- code: PT + name: Portugal +- code: RO + name: Romania +- code: SK + name: Slovakia +- code: SI + name: Slovenia +- code: ES + name: Spain +- code: SE + name: Sweden diff --git a/i18n/de.yaml b/i18n/de.yaml index f4658eaf1..3bca13188 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -522,8 +522,6 @@ translation: "Status" - id: hub_billing_manage_status_active translation: "Aktiv" -- id: hub_billing_manage_status_pastdue - translation: "Überfällig" - id: hub_billing_manage_status_trialing translation: "Testphase" - id: hub_billing_manage_status_paused @@ -546,12 +544,10 @@ - id: hub_billing_manage_payment_info_title translation: "Zahlungsinformationen" -- id: hub_billing_manage_payment_info_credit_card - translation: "Kreditkarte" -- id: hub_billing_manage_payment_info_credit_card_last_four_digits_description - translation: "Endet mit" -- id: hub_billing_manage_payment_info_paypal - translation: "PayPal" +- id: hub_billing_manage_payment_info_card + translation: "Karte / PayPal" +- id: hub_billing_manage_payment_info_invoice + translation: "Rechnung" - id: hub_billing_manage_payment_info_update_action translation: "Zahlungsmethode aktualisieren" @@ -564,8 +560,6 @@ - id: hub_billing_manage_license_key_retry_action translation: "Erneut versuchen" -- id: hub_billing_manage_modal_charge_amount_description - translation: "Rechnungsbetrag" - id: hub_billing_manage_modal_continue translation: "Weiter" - id: hub_billing_manage_modal_confirm @@ -588,8 +582,18 @@ translation: "Neue Anzahl der Sitze" - id: hub_billing_manage_change_quantity_confirmation_increase_warning translation: "Du bist dabei, das Sitzplatzlimit zu erhöhen. Wenn du dies bestätigst, wird die die Differenz sofort in Rechnung gestellt." +- id: hub_billing_manage_change_quantity_confirmation_increase_warning_invoice + translation: "Du bist dabei, das Sitzplatzlimit zu erhöhen. Wenn du dies bestätigst, werden die zusätzlichen Sitze per Rechnung abgerechnet." - id: hub_billing_manage_change_quantity_confirmation_decrease_warning translation: "Du bist dabei, das Sitzplatzlimit zu verringern. Wenn du dies bestätigst, wird deine nächste Zahlung um die Differenz reduziert." +- id: hub_billing_manage_change_quantity_confirmation_decrease_warning_invoice + translation: "Du bist dabei, das Sitzplatzlimit zu verringern. Wenn du dies bestätigst, wird die Reduzierung auf deiner nächsten Rechnung berücksichtigt." +- id: hub_billing_manage_change_quantity_confirmation_prorated_invoice + translation: "Anteiliger Betrag für den laufenden Zeitraum (wird jetzt in Rechnung gestellt)" +- id: hub_billing_manage_change_quantity_confirmation_prorated_credit_invoice + translation: "Anteilige Gutschrift für den laufenden Zeitraum" +- id: hub_billing_manage_change_quantity_confirmation_new_recurring_invoice + translation: "Neuer Jahresbetrag (netto)" - id: hub_billing_checkout_description translation: "Schöpfe das volle Potenzial deiner Hub-Instanz aus und hol dein Team mit der clientseitigen Verschlüsselung für deinen Cloud-Speicher an Bord." @@ -597,6 +601,8 @@ translation: "Für Unternehmen" - id: hub_billing_checkout_audience_consumer translation: "Für zu Hause" +- id: hub_billing_checkout_manage_existing_action + translation: "Ein bestehendes Abonnement verwalten" - id: hub_billing_checkout_standard_title translation: "Standard" @@ -628,6 +634,67 @@ - id: hub_billing_checkout_standard_submit translation: "Zur Zahlung" +- id: hub_billing_checkout_payment_method + translation: "Zahlungsmethode" +- id: hub_billing_checkout_payment_method_card + translation: "Per Karte zahlen" +- id: hub_billing_checkout_payment_method_invoice + translation: "Kauf auf Rechnung" + +- id: hub_billing_checkout_invoice_contact_first_name + translation: "Vorname" +- id: hub_billing_checkout_invoice_contact_last_name + translation: "Nachname" +- id: hub_billing_checkout_invoice_contact_email + translation: "E-Mail" +- id: hub_billing_checkout_invoice_contact_email_hint + translation: "Mit dieser E-Mail-Adresse verwaltest du später dein Abonnement." +- id: hub_billing_checkout_invoice_account_name + translation: "Firmenname" +- id: hub_billing_checkout_invoice_address_street + translation: "Straße und Hausnummer" +- id: hub_billing_checkout_invoice_address_postal_code + translation: "Postleitzahl" +- id: hub_billing_checkout_invoice_address_city + translation: "Stadt" +- id: hub_billing_checkout_invoice_address_country + translation: "Land" +- id: hub_billing_checkout_invoice_address_country_placeholder + translation: "Bitte auswählen" +- id: hub_billing_checkout_invoice_non_eu_hint + translation: "Kauf auf Rechnung ist nur innerhalb der EU möglich. Außerhalb der EU? Kontaktiere uns bitte über die Enterprise-Option." +- id: hub_billing_checkout_invoice_vat_id + translation: "USt-IdNr." +- id: hub_billing_checkout_invoice_vat_id_hint + translation: "Erforderlich für EU-Länder außerhalb Deutschlands." +- id: hub_billing_checkout_invoice_instruction + translation: "Eine Rechnung wird ausgestellt und an deine E-Mail-Adresse gesendet." +- id: hub_billing_checkout_invoice_submit + translation: "Auf Rechnung kaufen" +- id: hub_billing_checkout_invoice_total + translation: "Gesamtbetrag" +- id: hub_billing_checkout_invoice_total_suffix + translation: "pro Jahr (netto)" +- id: hub_billing_checkout_invoice_total_vat_hint + translation: "zzgl. 19 % USt." +- id: hub_billing_checkout_invoice_total_reverse_charge_hint + translation: "Reverse-Charge-Verfahren – Steuerschuldnerschaft des Leistungsempfängers" +- id: hub_billing_checkout_invoice_confirm_title + translation: "Bestellübersicht" +- id: hub_billing_checkout_invoice_confirm_product + translation: "Produkt" +- id: hub_billing_checkout_invoice_confirm_unit_price + translation: "Preis pro Seat" +- id: hub_billing_checkout_invoice_confirm_billing_address + translation: "Rechnungsadresse" + +- id: hub_billing_checkout_success_description + translation: "Vielen Dank für deinen Kauf! Dein Abonnement ist jetzt aktiv." +- id: hub_billing_checkout_success_invoice_description + translation: "Eine Rechnung wird ausgestellt und an deine E-Mail-Adresse gesendet." +- id: hub_billing_checkout_success_relicense_description + translation: "Um deine Lizenz zu erhalten, kehre bitte zu deiner Hub-Instanz zurück und starte den Abonnementvorgang erneut." + - id: hub_billing_checkout_community_title translation: "Community" - id: hub_billing_checkout_community_statement diff --git a/i18n/en.yaml b/i18n/en.yaml index 26b5fd25b..e0f953513 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -522,8 +522,6 @@ translation: "Status" - id: hub_billing_manage_status_active translation: "Active" -- id: hub_billing_manage_status_pastdue - translation: "Past Due" - id: hub_billing_manage_status_trialing translation: "Trialing" - id: hub_billing_manage_status_paused @@ -546,12 +544,10 @@ - id: hub_billing_manage_payment_info_title translation: "Payment Information" -- id: hub_billing_manage_payment_info_credit_card - translation: "Credit Card" -- id: hub_billing_manage_payment_info_credit_card_last_four_digits_description - translation: "Ending with" -- id: hub_billing_manage_payment_info_paypal - translation: "PayPal" +- id: hub_billing_manage_payment_info_card + translation: "Card / PayPal" +- id: hub_billing_manage_payment_info_invoice + translation: "Invoice" - id: hub_billing_manage_payment_info_update_action translation: "Update Payment Method" @@ -564,8 +560,6 @@ - id: hub_billing_manage_license_key_retry_action translation: "Retry" -- id: hub_billing_manage_modal_charge_amount_description - translation: "Charge Amount" - id: hub_billing_manage_modal_continue translation: "Continue" - id: hub_billing_manage_modal_confirm @@ -588,8 +582,18 @@ translation: "New Number of Seats" - id: hub_billing_manage_change_quantity_confirmation_increase_warning translation: "You are about to increase the seats limit. By confirming, you will be immediately charged for the difference." +- id: hub_billing_manage_change_quantity_confirmation_increase_warning_invoice + translation: "You are about to increase the seats limit. By confirming, the additional seats will be billed by invoice." - id: hub_billing_manage_change_quantity_confirmation_decrease_warning translation: "You are about to decrease the seats limit. By confirming, your next payment will be reduced by the difference." +- id: hub_billing_manage_change_quantity_confirmation_decrease_warning_invoice + translation: "You are about to decrease the seats limit. By confirming, the reduction will be reflected on your next invoice." +- id: hub_billing_manage_change_quantity_confirmation_prorated_invoice + translation: "Prorated amount for the current period (invoiced now)" +- id: hub_billing_manage_change_quantity_confirmation_prorated_credit_invoice + translation: "Prorated credit for the current period" +- id: hub_billing_manage_change_quantity_confirmation_new_recurring_invoice + translation: "New yearly total (net)" - id: hub_billing_checkout_description translation: "Unlock the full potential of your Hub instance and get your team on board with client-side encryption for your cloud storage." @@ -597,6 +601,8 @@ translation: "For Business" - id: hub_billing_checkout_audience_consumer translation: "For Home" +- id: hub_billing_checkout_manage_existing_action + translation: "Manage an existing subscription" - id: hub_billing_checkout_standard_title translation: "Standard" @@ -628,6 +634,67 @@ - id: hub_billing_checkout_standard_submit translation: "Checkout" +- id: hub_billing_checkout_payment_method + translation: "Payment Method" +- id: hub_billing_checkout_payment_method_card + translation: "Pay by Card" +- id: hub_billing_checkout_payment_method_invoice + translation: "Pay by Invoice" + +- id: hub_billing_checkout_invoice_contact_first_name + translation: "First Name" +- id: hub_billing_checkout_invoice_contact_last_name + translation: "Last Name" +- id: hub_billing_checkout_invoice_contact_email + translation: "Email" +- id: hub_billing_checkout_invoice_contact_email_hint + translation: "You'll use this email address to manage your subscription later." +- id: hub_billing_checkout_invoice_account_name + translation: "Company Name" +- id: hub_billing_checkout_invoice_address_street + translation: "Street and Number" +- id: hub_billing_checkout_invoice_address_postal_code + translation: "Postal Code" +- id: hub_billing_checkout_invoice_address_city + translation: "City" +- id: hub_billing_checkout_invoice_address_country + translation: "Country" +- id: hub_billing_checkout_invoice_address_country_placeholder + translation: "Please select" +- id: hub_billing_checkout_invoice_non_eu_hint + translation: "Invoice payment is available within the EU only. Outside the EU? Please contact us via the Enterprise option." +- id: hub_billing_checkout_invoice_vat_id + translation: "VAT ID" +- id: hub_billing_checkout_invoice_vat_id_hint + translation: "Required for EU countries outside Germany." +- id: hub_billing_checkout_invoice_instruction + translation: "An invoice will be issued and sent to your email address." +- id: hub_billing_checkout_invoice_submit + translation: "Buy on Invoice" +- id: hub_billing_checkout_invoice_total + translation: "Total" +- id: hub_billing_checkout_invoice_total_suffix + translation: "per year (net)" +- id: hub_billing_checkout_invoice_total_vat_hint + translation: "plus 19% German VAT" +- id: hub_billing_checkout_invoice_total_reverse_charge_hint + translation: "reverse charge – VAT to be accounted for by the recipient" +- id: hub_billing_checkout_invoice_confirm_title + translation: "Order Summary" +- id: hub_billing_checkout_invoice_confirm_product + translation: "Product" +- id: hub_billing_checkout_invoice_confirm_unit_price + translation: "Price per Seat" +- id: hub_billing_checkout_invoice_confirm_billing_address + translation: "Billing Address" + +- id: hub_billing_checkout_success_description + translation: "Thank you for your purchase! Your subscription is now active." +- id: hub_billing_checkout_success_invoice_description + translation: "An invoice will be issued and sent to your email address." +- id: hub_billing_checkout_success_relicense_description + translation: "To receive your license, please return to your Hub instance and start the subscription process again." + - id: hub_billing_checkout_community_title translation: "Community" - id: hub_billing_checkout_community_statement diff --git a/layouts/hub-billing/single.html b/layouts/hub-billing/single.html index 12f870120..851792f93 100644 --- a/layouts/hub-billing/single.html +++ b/layouts/hub-billing/single.html @@ -3,7 +3,7 @@ {{ end }} {{ define "main" }}
-
+