mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #3028 from appwrite/feat/live-payment-create-upgrade
Confirm create-org and upgrade-plan payments on-session
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, teamStatusUpgrading } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id && $organization?.status === teamStatusUpgrading && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert title="Payment is processing" type="info">
|
||||
Your plan will activate within a few minutes. You can keep using {$organization.name} while we
|
||||
confirm the charge with your bank.
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -50,10 +50,10 @@
|
||||
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
|
||||
<Alert.Inline status="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
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.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<InputSelect
|
||||
|
||||
@@ -8,6 +8,7 @@ import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.
|
||||
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
|
||||
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
|
||||
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
|
||||
import PaymentProcessing from '$lib/components/billing/alerts/paymentProcessing.svelte';
|
||||
|
||||
import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
@@ -57,6 +58,7 @@ export const roles = [
|
||||
];
|
||||
|
||||
export const teamStatusReadonly = 'readonly';
|
||||
export const teamStatusUpgrading = 'upgrading';
|
||||
export const billingLimitOutstandingInvoice = 'outstanding_invoice';
|
||||
|
||||
export const paymentMethods = derived(
|
||||
@@ -587,6 +589,17 @@ export function checkForMarkedForDeletion(org: Models.Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
export function checkForUpgradingStatus(org: Models.Organization) {
|
||||
if (org?.status === teamStatusUpgrading) {
|
||||
headerAlert.add({
|
||||
id: 'paymentProcessing',
|
||||
component: PaymentProcessing,
|
||||
show: true,
|
||||
importance: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForMissingPaymentMethod() {
|
||||
const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
if (!starterPlan?.$id) {
|
||||
|
||||
@@ -186,25 +186,61 @@ export async function setPaymentMethod(providerMethodId: string, name: string, s
|
||||
}
|
||||
}
|
||||
|
||||
export type ConfirmPaymentOutcome =
|
||||
| { status: 'succeeded' | 'processing' | 'requires_action' }
|
||||
| { status: 'error'; message: string };
|
||||
|
||||
export async function confirmPayment(config: {
|
||||
clientSecret: string;
|
||||
paymentMethodId: string;
|
||||
orgId?: string;
|
||||
route?: string;
|
||||
}) {
|
||||
const { clientSecret, paymentMethodId, orgId, route } = config;
|
||||
redirectIfRequired?: boolean;
|
||||
}): Promise<ConfirmPaymentOutcome | undefined> {
|
||||
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'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user