diff --git a/src/lib/components/billing/alerts/paymentProcessing.svelte b/src/lib/components/billing/alerts/paymentProcessing.svelte
new file mode 100644
index 000000000..0539ba2fa
--- /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 9682b9995..b6a09fc03 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', {
@@ -203,8 +208,40 @@ export async function confirmPayment(config: {
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
@@ -221,6 +258,12 @@ export async function confirmPayment(config: {
'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: typeof e === 'string' ? e : (e?.message ?? 'Payment confirmation failed')
+ };
+ }
}
}
diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte
index ea0830ac0..8663437fb 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 bf5a4a109..8966e2055 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,21 @@
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') {
+ 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 b353f5738..7c9c6923a 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,21 @@
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') {
+ return;
+ }
+
+ if (outcome.status === 'requires_action') {
+ return;
+ }
+
await validate(org.organizationId, collaborators);
}