diff --git a/src/lib/components/billing/alerts/paymentProcessing.svelte b/src/lib/components/billing/alerts/paymentProcessing.svelte
new file mode 100644
index 0000000000..0539ba2fad
--- /dev/null
+++ b/src/lib/components/billing/alerts/paymentProcessing.svelte
@@ -0,0 +1,13 @@
+
+
+{#if $organization?.$id && $organization?.status === teamStatusUpgrading && !hideBillingHeaderRoutes.includes(page.url.pathname)}
+
+ Your plan will activate within a few minutes. You can keep using {$organization.name} while we
+ confirm the charge with your bank.
+
+{/if}
diff --git a/src/lib/components/billing/selectPaymentMethod.svelte b/src/lib/components/billing/selectPaymentMethod.svelte
index 9682b99953..b6a09fc032 100644
--- a/src/lib/components/billing/selectPaymentMethod.svelte
+++ b/src/lib/components/billing/selectPaymentMethod.svelte
@@ -50,10 +50,10 @@
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
Indian credit or debit card-holders
- To comply with RBI regulations in India, Appwrite will ask for verification to charge
- up to $150 USD on your payment method. We will never charge more than the cost of your
- plan and the resources you use, or your budget cap limit. For higher usage limits, please
- contact us.
+ To comply with RBI regulations in India, you will be asked to authenticate the first charge
+ during checkout, and Appwrite will set up a mandate to charge up to $150 USD on your payment
+ method for future renewals. We will never charge more than the cost of your plan and the
+ resources you use, or your budget cap limit. For higher usage limits, please contact us.
{/if}
{
+ const { clientSecret, paymentMethodId, orgId, route, redirectIfRequired } = config;
try {
- const resolvedUrl = resolve('/(console)/organization-[organization]/billing', {
- organization: orgId
- });
-
- const url = window.location.origin + (route ? route : resolvedUrl);
+ const url =
+ window.location.origin +
+ (route ??
+ resolve('/(console)/organization-[organization]/billing', { organization: orgId }));
const paymentMethod = await sdk.forConsole.account.getPaymentMethod({ paymentMethodId });
+ if (redirectIfRequired) {
+ const { paymentIntent, error } = await get(stripe).confirmPayment({
+ clientSecret,
+ confirmParams: {
+ return_url: url,
+ payment_method: paymentMethod.providerMethodId
+ },
+ redirect: 'if_required'
+ });
+
+ if (error) {
+ addNotification({
+ title: 'Error',
+ message:
+ error.message ??
+ 'There was an error processing your payment, try again later. If the problem persists, please contact support.',
+ type: 'error'
+ });
+ return { status: 'error', message: error.message ?? 'Payment confirmation failed' };
+ }
+
+ const status = paymentIntent?.status;
+ if (status === 'succeeded' || status === 'processing' || status === 'requires_action') {
+ return { status };
+ }
+
+ return {
+ status: 'error',
+ message: `Unexpected payment status: ${status ?? 'unknown'}`
+ };
+ }
+
const { error } = await get(stripe).confirmPayment({
- clientSecret: clientSecret,
+ clientSecret,
confirmParams: {
return_url: url,
payment_method: paymentMethod.providerMethodId
@@ -215,12 +251,21 @@ export async function confirmPayment(config: {
throw error.message;
}
} catch (e) {
+ const underlying =
+ typeof e === 'string' ? e : ((e as { message?: string })?.message ?? null);
addNotification({
title: 'Error',
message:
+ underlying ??
'There was an error processing your payment, try again later. If the problem persists, please contact support.',
type: 'error'
});
+ if (redirectIfRequired) {
+ return {
+ status: 'error',
+ message: underlying ?? 'Payment confirmation failed'
+ };
+ }
}
}
diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte
index ea0830ac0c..8663437fb7 100644
--- a/src/routes/(console)/+layout.svelte
+++ b/src/routes/(console)/+layout.svelte
@@ -18,6 +18,7 @@
checkForMarkedForDeletion,
checkForMissingPaymentMethod,
checkForNewDevUpgradePro,
+ checkForUpgradingStatus,
checkForUsageLimit,
checkPaymentAuthorizationRequired,
paymentExpired,
@@ -295,6 +296,7 @@
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
+ checkForUpgradingStatus(org);
await checkForNewDevUpgradePro(org);
if (org?.billingPlanDetails.requiresPaymentMethod) {
diff --git a/src/routes/(console)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte
index bf5a4a1098..297b94489d 100644
--- a/src/routes/(console)/create-organization/+page.svelte
+++ b/src/routes/(console)/create-organization/+page.svelte
@@ -11,7 +11,8 @@
import {
billingIdToPlan,
getBasePlanFromGroup,
- isPaymentAuthenticationRequired
+ isPaymentAuthenticationRequired,
+ teamStatusUpgrading
} from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -106,11 +107,21 @@
});
if (!isPaymentAuthenticationRequired(org)) {
+ await invalidate(Dependencies.ACCOUNT);
await preloadAndNavigate(org.$id);
- addNotification({
- type: 'success',
- message: `${org.name ?? 'Organization'} has been created`
- });
+
+ if (org.status === teamStatusUpgrading) {
+ addNotification({
+ type: 'info',
+ message:
+ 'Payment is processing — your plan will activate within a few minutes.'
+ });
+ } else {
+ addNotification({
+ type: 'success',
+ message: `${org.name ?? 'Organization'} has been created`
+ });
+ }
}
} catch (e) {
addNotification({
@@ -144,8 +155,8 @@
});
if (isPaymentAuthenticationRequired(org)) {
- let clientSecret = org.clientSecret;
- let params = new URLSearchParams();
+ const clientSecret = org.clientSecret;
+ const params = new URLSearchParams();
params.append('type', 'payment_confirmed');
params.append('id', org.organizationId);
for (const [key, value] of page.url.searchParams.entries()) {
@@ -156,12 +167,29 @@
params.append('invites', collaborators.join(','));
const resolvedUrl = resolve('/(console)/create-organization');
- await confirmPayment({
+ const outcome = await confirmPayment({
clientSecret,
paymentMethodId,
- route: `${resolvedUrl}?${params}`
+ route: `${resolvedUrl}?${params}`,
+ redirectIfRequired: true
});
+ if (!outcome || outcome.status === 'error') {
+ try {
+ await sdk.forConsole.organizations.validatePayment({
+ organizationId: org.organizationId,
+ invites: []
+ });
+ } catch {
+ // expected: backend throws BILLING_PAYMENT_FAILED after deleting the draft team
+ }
+ return;
+ }
+
+ if (outcome.status === 'requires_action') {
+ return;
+ }
+
await validate(org.organizationId, collaborators);
}
}
diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
index b353f5738f..a651561a93 100644
--- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
@@ -12,7 +12,8 @@
import {
billingIdToPlan,
getBasePlanFromGroup,
- isPaymentAuthenticationRequired
+ isPaymentAuthenticationRequired,
+ teamStatusUpgrading
} from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { currentPlan, organization } from '$lib/stores/organization';
@@ -220,10 +221,19 @@
await invalidate(Dependencies.ORGANIZATION);
await goto(previousPage);
- addNotification({
- type: 'success',
- message: 'Your organization has been upgraded'
- });
+
+ if (org.status === teamStatusUpgrading) {
+ addNotification({
+ type: 'info',
+ message:
+ 'Payment is processing — your plan will activate within a few minutes.'
+ });
+ } else {
+ addNotification({
+ type: 'success',
+ message: 'Your organization has been upgraded'
+ });
+ }
trackEvent(Submit.OrganizationUpgrade, {
plan: selectedPlan?.name
@@ -259,8 +269,8 @@
});
if (isPaymentAuthenticationRequired(org)) {
- let clientSecret = org.clientSecret;
- let params = new URLSearchParams();
+ const clientSecret = org.clientSecret;
+ const params = new URLSearchParams();
for (const [key, value] of page.url.searchParams.entries()) {
if (key !== 'type' && key !== 'id') {
params.append(key, value);
@@ -275,11 +285,29 @@
organization: org.organizationId
});
- await confirmPayment({
+ const outcome = await confirmPayment({
clientSecret,
paymentMethodId,
- route: `${resolvedUrl}?${params.toString()}`
+ route: `${resolvedUrl}?${params.toString()}`,
+ redirectIfRequired: true
});
+
+ if (!outcome || outcome.status === 'error') {
+ try {
+ await sdk.forConsole.organizations.validatePayment({
+ organizationId: org.organizationId,
+ invites: []
+ });
+ } catch {
+ // expected: backend throws BILLING_PAYMENT_FAILED and rolls back the upgrade
+ }
+ return;
+ }
+
+ if (outcome.status === 'requires_action') {
+ return;
+ }
+
await validate(org.organizationId, collaborators);
}
diff --git a/src/routes/(console)/organization-[organization]/settings/BAA.svelte b/src/routes/(console)/organization-[organization]/settings/BAA.svelte
index 71a32f4d2a..a4ba95ae3d 100644
--- a/src/routes/(console)/organization-[organization]/settings/BAA.svelte
+++ b/src/routes/(console)/organization-[organization]/settings/BAA.svelte
@@ -160,12 +160,48 @@
const settingsUrl = resolve('/(console)/organization-[organization]/settings', {
organization: $organization.$id
});
- await confirmPayment({
+ const outcome = await confirmPayment({
clientSecret: paymentAuth.clientSecret,
paymentMethodId: $organization.paymentMethodId,
orgId: $organization.$id,
- route: `${settingsUrl}?type=confirm-addon&addonId=${paymentAuth.addonId}`
+ route: `${settingsUrl}?type=confirm-addon&addonId=${paymentAuth.addonId}`,
+ redirectIfRequired: true
});
+
+ if (!outcome || outcome.status === 'error') {
+ trackError(
+ new Error(outcome?.status === 'error' ? outcome.message : 'Payment failed'),
+ Submit.BAAAddonEnable
+ );
+ return;
+ }
+
+ if (outcome.status === 'requires_action') {
+ return;
+ }
+
+ await sdk.forConsole.organizations.confirmAddonPayment({
+ organizationId: $organization.$id,
+ addonId: paymentAuth.addonId
+ });
+
+ await Promise.all([
+ invalidate(Dependencies.ADDONS),
+ invalidate(Dependencies.ORGANIZATION)
+ ]);
+
+ if (outcome.status === 'processing') {
+ addNotification({
+ message: "BAA addon payment is processing — we'll activate it shortly.",
+ type: 'info'
+ });
+ } else {
+ addNotification({
+ message: 'BAA addon has been enabled',
+ type: 'success'
+ });
+ }
+ trackEvent(Submit.BAAAddonEnable);
return;
}
diff --git a/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte b/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte
index 36881a73cf..33d68f7a9e 100644
--- a/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte
+++ b/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte
@@ -39,12 +39,49 @@
const settingsUrl = resolve('/(console)/organization-[organization]/settings', {
organization: $organization.$id
});
- await confirmPayment({
+ const outcome = await confirmPayment({
clientSecret: paymentAuth.clientSecret,
paymentMethodId: $organization.paymentMethodId,
orgId: $organization.$id,
- route: `${settingsUrl}?type=confirm-addon&addonId=${paymentAuth.addonId}`
+ route: `${settingsUrl}?type=confirm-addon&addonId=${paymentAuth.addonId}`,
+ redirectIfRequired: true
});
+
+ if (!outcome || outcome.status === 'error') {
+ if (outcome?.status === 'error') {
+ error = outcome.message;
+ trackError(new Error(outcome.message), Submit.BAAAddonEnable);
+ }
+ return;
+ }
+
+ if (outcome.status === 'requires_action') {
+ return;
+ }
+
+ await sdk.forConsole.organizations.confirmAddonPayment({
+ organizationId: $organization.$id,
+ addonId: paymentAuth.addonId
+ });
+
+ await Promise.all([
+ invalidate(Dependencies.ADDONS),
+ invalidate(Dependencies.ORGANIZATION)
+ ]);
+
+ if (outcome.status === 'processing') {
+ addNotification({
+ message: "BAA addon payment is processing — we'll activate it shortly.",
+ type: 'info'
+ });
+ } else {
+ addNotification({
+ message: 'BAA addon has been enabled',
+ type: 'success'
+ });
+ }
+ trackEvent(Submit.BAAAddonEnable);
+ show = false;
return;
}