mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/main' into feat-budget-nullable
This commit is contained in:
@@ -179,7 +179,7 @@
|
||||
{#if $isLoading || answer}
|
||||
<div class="content">
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<div class="avatar is-size-x-small">{getInitials($user.name)}</div>
|
||||
<div class="avatar is-size-x-small">{getInitials($user.name || $user.email)}</div>
|
||||
<p class="u-opacity-75">{previousQuestion}</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-24">
|
||||
@@ -229,7 +229,7 @@
|
||||
|
||||
<div class="footer" slot="footer">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<AvatarInitials size={32} name={$user.name} />
|
||||
<AvatarInitials size={32} name={$user.name || $user.email} />
|
||||
<form
|
||||
class="input-text-wrapper u-width-full-line"
|
||||
style="--amount-of-buttons: 1;"
|
||||
|
||||
@@ -12,13 +12,10 @@
|
||||
|
||||
export function getFlag(country: string, width: number, height: number, quality: number) {
|
||||
if (!isValueOfStringEnum(Flag, country)) return '';
|
||||
let flag = sdk.forConsole.avatars
|
||||
return sdk.forConsole.avatars
|
||||
.getFlag(country, width * 2, height * 2, quality)
|
||||
?.toString();
|
||||
flag?.includes('&project=')
|
||||
? (flag = flag.replace('&project=', '&mode=admin'))
|
||||
: flag + '&mode=admin';
|
||||
return flag;
|
||||
?.toString()
|
||||
?.replace('&project=console', '&mode=admin');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { app } from '$lib/stores/app';
|
||||
import type { Campaign } from '$lib/stores/campaigns';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
|
||||
|
||||
export const imgLight = LoginLight;
|
||||
export const imgDark = LoginDark;
|
||||
@@ -38,12 +38,6 @@
|
||||
return campaign.description;
|
||||
}
|
||||
}
|
||||
|
||||
function getImage(image: string) {
|
||||
const endpoint = getApiEndpoint();
|
||||
const url = new URL(image, endpoint);
|
||||
return url.toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="grid-1-1 is-full-page" id="main">
|
||||
@@ -86,7 +80,7 @@
|
||||
<section
|
||||
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line">
|
||||
<img
|
||||
src={getImage(campaign?.image[$app.themeInUse])}
|
||||
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
|
||||
class="u-block u-image-object-fit-cover side-bg-img"
|
||||
alt="promo" />
|
||||
|
||||
@@ -151,7 +145,7 @@
|
||||
<div class="u-margin-block-start-16 u-flex u-gap-16">
|
||||
{#if currentReview?.image}
|
||||
<Avatar
|
||||
src={getImage(currentReview?.image)}
|
||||
src={getCampaignImageUrl(currentReview?.image)}
|
||||
name={currentReview.name}
|
||||
size={40} />
|
||||
{:else}
|
||||
@@ -170,7 +164,7 @@
|
||||
<p class="u-bold" style:text-transform="uppercase">provided to you by</p>
|
||||
<img
|
||||
style:max-block-size="2.5rem"
|
||||
src={getImage(campaign?.image[$app.themeInUse])}
|
||||
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
|
||||
alt={coupon?.campaign ?? campaign.$id} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<svelte:fragment slot="image">
|
||||
<AvatarInitials size={48} name={$user.name} />
|
||||
<AvatarInitials size={48} name={$user.name || $user.email} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="u-bold u-trim-1" data-private>{$user.name}</span>
|
||||
<span class="u-bold u-trim-1" data-private>{$user.name || 'User'}</span>
|
||||
</svelte:fragment>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let name: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
name ??= $user.name;
|
||||
name ??= $user.name ?? '';
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Button, Form, FormList, InputSelect } from '$lib/elements/forms';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CreditsApplied, EstimatedTotal, SelectPaymentMethod } from '$lib/components/billing';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputSelect, InputTags, InputText } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter
|
||||
} from '$lib/layout';
|
||||
|
||||
import { type PaymentList } from '$lib/sdk/billing';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { isOrganization } from '$lib/stores/billing.js';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { BillingPlan, ID } from '@appwrite.io/console';
|
||||
import {
|
||||
organizationList,
|
||||
type Organization,
|
||||
type OrganizationError
|
||||
} from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe.js';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { CreditsApplied } from '$lib/components/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Helper } from '$lib/elements/forms/index.js';
|
||||
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
|
||||
|
||||
export let data;
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
let previousPage: string = base;
|
||||
let showExitModal = false;
|
||||
let canSelectOrg = true;
|
||||
@@ -41,7 +49,13 @@
|
||||
|
||||
let selectedOrgId: string = null;
|
||||
let formComponent: Form;
|
||||
let couponForm: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let paymentMethodId: string;
|
||||
let collaborators: string[];
|
||||
let taxId: string;
|
||||
let billingBudget: number;
|
||||
let newOrgId = ID.unique();
|
||||
let options = [
|
||||
...($organizationList?.teams?.map((team) => ({
|
||||
@@ -53,16 +67,14 @@
|
||||
label: 'Create new organization'
|
||||
}
|
||||
];
|
||||
|
||||
let name: string;
|
||||
let coupon: string;
|
||||
let couponData = data?.couponData;
|
||||
let campaign = data?.campaign;
|
||||
$: selectedAction = selectedOrgId === newOrgId ? 'create' : 'update';
|
||||
let billingPlan = BillingPlan.PRO;
|
||||
|
||||
onMount(async () => {
|
||||
if (!$organizationList?.total && couponData?.code) {
|
||||
goto(`${base}/create-organization?coupon=${couponData.code}&plan=${BillingPlan.Tier1}`);
|
||||
}
|
||||
|
||||
await loadPaymentMethods();
|
||||
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
|
||||
selectedOrgId = newOrgId;
|
||||
}
|
||||
@@ -70,59 +82,128 @@
|
||||
selectedOrgId = $page.url.searchParams.get('org');
|
||||
canSelectOrg = false;
|
||||
}
|
||||
if (campaign?.plan) {
|
||||
billingPlan = campaign.plan;
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.has('type')) {
|
||||
const type = $page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
const organizationId = $page.url.searchParams.get('id');
|
||||
collaborators = $page.url.searchParams.get('invites').split(',');
|
||||
|
||||
await sdk.forConsole.billing.validateOrganization(organizationId, collaborators);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedOrgId && $organizationList?.total) {
|
||||
selectedOrgId = $organizationList.teams[0].$id;
|
||||
}
|
||||
});
|
||||
|
||||
async function applyCouponCredit(org: Organization) {
|
||||
$isSubmitting = true;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
await sdk.forConsole.billing.addCredit(org.$id, couponData.code);
|
||||
trackEvent(Submit.CreditRedeem);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
$isSubmitting = false;
|
||||
trackError(error, Submit.CreditRedeem);
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: error ? 'error' : 'success',
|
||||
message: error ? error.message : 'Credit applied successfully'
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
await goto(`${base}/organization-${selectedOrgId}`);
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
const methodList = await sdk.forConsole.billing.listPaymentMethods();
|
||||
const filteredMethods = methodList.paymentMethods.filter((method) => !!method?.last4);
|
||||
methods = { paymentMethods: filteredMethods, total: filteredMethods.length };
|
||||
paymentMethodId =
|
||||
selectedOrg?.paymentMethodId ??
|
||||
methods.paymentMethods.find((method) => !!method?.last4)?.$id ??
|
||||
null;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (couponData) {
|
||||
const createOrganization = selectedAction === 'create';
|
||||
const isScalePlanUpgrade =
|
||||
campaign?.plan && selectedOrg?.billingPlan !== campaign?.plan;
|
||||
if (!couponForm.checkValidity()) return;
|
||||
isSubmitting.set(true);
|
||||
try {
|
||||
let org: Organization | OrganizationError;
|
||||
// Create new org
|
||||
if (selectedOrgId === newOrgId) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
name,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
taxId
|
||||
);
|
||||
}
|
||||
|
||||
// on create-org, its `coupon`
|
||||
if (createOrganization) {
|
||||
await goto(`${base}/create-organization?coupon=${couponData.code}`);
|
||||
return;
|
||||
}
|
||||
if (!campaign?.plan || campaign.plan === selectedOrg?.billingPlan) {
|
||||
await applyCouponCredit(selectedOrg);
|
||||
} else if (isScalePlanUpgrade) {
|
||||
await goto(
|
||||
`${base}/create-organization?coupon=${couponData.code}&plan=${campaign.plan}`
|
||||
);
|
||||
} else if (selectedAction === 'update') {
|
||||
// on change-plan, its `code`
|
||||
await goto(
|
||||
`${base}/organization-${selectedOrgId}/change-plan?code=${couponData.code}`
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== billingPlan) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// we might not reach here but still, just being safe.
|
||||
// Existing pro org
|
||||
else {
|
||||
org = selectedOrg;
|
||||
}
|
||||
|
||||
if (!isOrganization(org) && org.status === 402) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('org', org.teamId);
|
||||
for (const [key, value] of $page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
params.append('invites', collaborators.join(','));
|
||||
await confirmPayment(
|
||||
'',
|
||||
clientSecret,
|
||||
paymentMethodId,
|
||||
'/console/apply-credit?' + params.toString()
|
||||
);
|
||||
org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators);
|
||||
}
|
||||
|
||||
if (isOrganization(org)) {
|
||||
trackEvent(Submit.CreditRedeem, {
|
||||
coupon: couponData.code,
|
||||
campaign: couponData?.campaign
|
||||
});
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await goto(`${base}/organization-${org.$id}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
}
|
||||
} catch (e) {
|
||||
trackError(e, Submit.CreditRedeem);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Coupon code is not valid'
|
||||
message: e.message
|
||||
});
|
||||
} finally {
|
||||
isSubmitting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
couponData = response;
|
||||
coupon = null;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -130,6 +211,11 @@
|
||||
$: selectedOrg = $organizationList?.teams?.find(
|
||||
(team) => team.$id === selectedOrgId
|
||||
) as Organization;
|
||||
|
||||
$: billingPlan =
|
||||
selectedOrg?.billingPlan === BillingPlan.SCALE
|
||||
? BillingPlan.SCALE
|
||||
: (campaign?.plan ?? BillingPlan.PRO);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -138,9 +224,9 @@
|
||||
|
||||
<WizardSecondaryContainer href={previousPage} bind:showExitModal confirmExit>
|
||||
<svelte:fragment slot="title">Apply credits</svelte:fragment>
|
||||
<WizardSecondaryContent hideSidebar={!$organizationList?.total}>
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<FormList gap={8}>
|
||||
<FormList>
|
||||
{#if $organizationList?.total && !campaign?.onlyNewOrgs && canSelectOrg}
|
||||
<InputSelect
|
||||
bind:value={selectedOrgId}
|
||||
@@ -150,69 +236,44 @@
|
||||
placeholder="Select organization"
|
||||
id="organization" />
|
||||
{/if}
|
||||
|
||||
<!-- TEMP FIX -->
|
||||
{#if !$organizationList?.total}
|
||||
{#if campaign?.template === 'card'}
|
||||
<div
|
||||
class="box card-container u-position-relative"
|
||||
style:--box-border-radius="var(--border-radius-small)">
|
||||
<div class="card-bg"></div>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
|
||||
<img
|
||||
src={campaign?.image[$app.themeInUse]}
|
||||
class="u-block u-image-object-fit-cover card-img"
|
||||
alt="promo" />
|
||||
<p class="text">
|
||||
{#if couponData?.credits}
|
||||
{campaign.title.replace(
|
||||
'VALUE',
|
||||
couponData.credits.toString()
|
||||
)}
|
||||
{:else}
|
||||
{campaign.title}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedOrgId && (selectedOrg?.billingPlan === BillingPlan.FREE || !selectedOrg?.paymentMethodId)}
|
||||
{#if selectedOrgId === newOrgId}
|
||||
<InputText
|
||||
label="Organization name"
|
||||
placeholder="Enter organization name"
|
||||
id="name"
|
||||
required
|
||||
bind:value={name} />
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
{#if couponData?.code && couponData?.status === 'active'}
|
||||
{@const dateAvailable = selectedOrg?.billingNextInvoiceDate}
|
||||
<CreditsApplied
|
||||
bind:couponData
|
||||
fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
{#if !dateAvailable}
|
||||
Credits will automatically be applied to your next invoice.
|
||||
{:else}
|
||||
Credits will automatically be applied to your next invoice on <b
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}</b
|
||||
>.
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">
|
||||
Add a coupon code to apply credits to your organization.
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
tooltip="Invited members will have access to all services and payment data within your organization"
|
||||
placeholder="Enter email address(es)"
|
||||
validityRegex={emailRegex}
|
||||
validityMessage="Invalid email address"
|
||||
id="members" />
|
||||
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
|
||||
{/if}
|
||||
|
||||
{#if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
|
||||
<Helper type="neutral">
|
||||
This coupon code is only valid when upgrading to Scale plan.
|
||||
</Helper>
|
||||
</FormList>
|
||||
</Form>
|
||||
<Form bind:this={couponForm} onSubmit={addCoupon}>
|
||||
<FormList>
|
||||
{#if !data?.couponData?.code && selectedOrgId}
|
||||
<InputText
|
||||
required
|
||||
disabled={!!couponData?.credits}
|
||||
bind:value={coupon}
|
||||
placeholder="Enter coupon code"
|
||||
id="code"
|
||||
label="Coupon code">
|
||||
<Button submit secondary disabled={!!couponData?.credits}>
|
||||
<span class="text">Apply</span>
|
||||
</Button>
|
||||
</InputText>
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
|
||||
<!-- this is the side card and is okay -->
|
||||
<svelte:fragment slot="aside">
|
||||
{#if campaign?.template === 'card'}
|
||||
<div
|
||||
@@ -221,7 +282,7 @@
|
||||
<div class="card-bg"></div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
|
||||
<img
|
||||
src={campaign?.image[$app.themeInUse]}
|
||||
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
|
||||
class="u-block u-image-object-fit-cover card-img"
|
||||
alt="promo" />
|
||||
<p class="text">
|
||||
@@ -234,27 +295,44 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
{#if couponData?.code && couponData?.status === 'active'}
|
||||
{@const dateAvailable = selectedOrg?.billingNextInvoiceDate}
|
||||
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
{#if !dateAvailable}
|
||||
Credits will automatically be applied to your next invoice.
|
||||
{:else}
|
||||
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
{#if couponData?.code && couponData?.status === 'active'}
|
||||
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
Credits will automatically be applied to your next invoice on <b
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}</b
|
||||
>.
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}.</b>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">Add a coupon code to apply credits to your organization.</p>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if selectedOrgId}
|
||||
<div class:u-margin-block-start-24={campaign?.template === 'card'}>
|
||||
<EstimatedTotal
|
||||
{billingBudget}
|
||||
organizationId={selectedOrgId === newOrgId ? undefined : selectedOrgId}
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
{couponData}>
|
||||
{#if campaign?.template === 'review' && (campaign?.cta || campaign?.claimed || campaign?.unclaimed)}
|
||||
<div class="u-margin-block-end-24">
|
||||
<p class="body-text-1 u-bold">{campaign?.cta}</p>
|
||||
<p class="text u-margin-block-start-8">
|
||||
{#if couponData?.code && couponData?.status === 'active' && campaign?.claimed}
|
||||
{campaign?.claimed}
|
||||
{:else if campaign?.unclaimed}
|
||||
{campaign?.unclaimed}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">Add a coupon code to apply credits to your organization.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</EstimatedTotal>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
@@ -263,22 +341,19 @@
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
if (formComponent.checkValidity()) {
|
||||
if (formComponent.checkValidity() && couponForm.checkValidity()) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
disabled={$isSubmitting || !selectedOrgId}>
|
||||
disabled={$isSubmitting}>
|
||||
{#if $isSubmitting}
|
||||
<span class="loader is-small is-transparent u-line-height-1-5" aria-hidden="true" />
|
||||
{/if}
|
||||
<!-- {#if selectedOrgId === newOrgId}
|
||||
Create organization
|
||||
{:else if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
|
||||
Upgrade plan
|
||||
{#if selectedOrgId === newOrgId}
|
||||
Create Organization
|
||||
{:else}
|
||||
Apply credits
|
||||
{/if} -->
|
||||
Apply credits
|
||||
Apply credits
|
||||
{/if}
|
||||
</Button>
|
||||
</WizardSecondaryFooter>
|
||||
<svelte:fragment slot="exit">
|
||||
|
||||
@@ -3,9 +3,8 @@ import type { Coupon } from '$lib/sdk/billing.js';
|
||||
import type { Campaign } from '$lib/stores/campaigns.js';
|
||||
import { sdk } from '$lib/stores/sdk.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ url }) => {
|
||||
export const load = async ({ url }) => {
|
||||
// Has promo code
|
||||
if (url.searchParams.has('code')) {
|
||||
let couponData: Coupon;
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
const type = $page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
const organizationId = $page.url.searchParams.get('id');
|
||||
const invites = $page.url.searchParams.getAll('invites');
|
||||
const invites = $page.url.searchParams.get('invites').split(',');
|
||||
await validate(organizationId, invites);
|
||||
}
|
||||
}
|
||||
@@ -152,10 +152,12 @@
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('id', org.teamId);
|
||||
for (let index = 0; index < collaborators.length; index++) {
|
||||
const invite = collaborators[index];
|
||||
params.append('invites', invite);
|
||||
for (const [key, value] of $page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
params.append('invites', collaborators.join(','));
|
||||
await confirmPayment(
|
||||
'',
|
||||
clientSecret,
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
import { ID, Region } from '@appwrite.io/console';
|
||||
import { openImportWizard } from '../project-[project]/settings/migrations/(import)';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { canWriteProjects } from '$lib/stores/roles';
|
||||
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
|
||||
import { regions as regionsStore } from '$routes/(console)/organization-[organization]/store';
|
||||
|
||||
export let data;
|
||||
|
||||
let addOrganization = false;
|
||||
let showCreate = false;
|
||||
let addOrganization = false;
|
||||
|
||||
const getPlatformInfo = (platform: string) => {
|
||||
let name: string, icon: string;
|
||||
@@ -85,6 +85,7 @@
|
||||
if (isCloud) wizard.start(Create);
|
||||
else showCreate = true;
|
||||
}
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Create project',
|
||||
@@ -120,17 +121,16 @@
|
||||
}
|
||||
};
|
||||
|
||||
let regions: RegionList;
|
||||
onMount(async () => {
|
||||
if (isCloud) {
|
||||
regions = await sdk.forConsole.billing.listRegions();
|
||||
const regions = await sdk.forConsole.billing.listRegions();
|
||||
regionsStore.set(regions);
|
||||
checkPricingRefAndRedirect($page.url.searchParams);
|
||||
}
|
||||
});
|
||||
|
||||
function findRegion(project: Models.Project) {
|
||||
const region = regions.regions.find((region) => region.$id === project.region);
|
||||
return region;
|
||||
return $regionsStore?.regions?.find((region) => region.$id === project.region);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</Pill>
|
||||
{/if}
|
||||
<svelte:fragment slot="icons">
|
||||
{#if isCloud && regions}
|
||||
{#if isCloud && $regionsStore?.regions}
|
||||
{@const region = findRegion(project)}
|
||||
<span class="u-color-text-gray u-medium u-line-height-2">
|
||||
{region?.name}
|
||||
|
||||
@@ -99,6 +99,15 @@
|
||||
billingPlan = plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.has('type')) {
|
||||
const type = $page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
const organizationId = $page.url.searchParams.get('id');
|
||||
const invites = $page.url.searchParams.get('invites').split(',');
|
||||
await validate(organizationId, invites);
|
||||
}
|
||||
}
|
||||
if ($currentPlan?.$id === BillingPlan.SCALE) {
|
||||
billingPlan = BillingPlan.SCALE;
|
||||
} else {
|
||||
@@ -224,17 +233,20 @@
|
||||
if (!isOrganization(org) && org.status == 402) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
for (const [key, value] of $page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('id', org.teamId);
|
||||
for (let index = 0; index < collaborators.length; index++) {
|
||||
const invite = collaborators[index];
|
||||
params.append('invites', invite);
|
||||
}
|
||||
params.append('invites', collaborators.join(','));
|
||||
params.append('plan', billingPlan);
|
||||
await confirmPayment(
|
||||
'',
|
||||
clientSecret,
|
||||
paymentMethodId,
|
||||
'/console/create-organization?' + params.toString()
|
||||
'/console/change-plan?' + params.toString()
|
||||
);
|
||||
await validate(org.teamId, collaborators);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { logout } from '$lib/helpers/logout';
|
||||
import { checkForUsageLimit } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { checkForUsageLimit } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { logout } from '$lib/helpers/logout';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedMember.userName} was deleted from ${selectedMember.teamName}`
|
||||
message: `${selectedMember.userName || 'User'} was deleted from ${selectedMember.teamName}`
|
||||
});
|
||||
trackEvent(Submit.MemberDelete);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { AvatarInitials, DropList, DropListItem, PaginationWithLimit } from '$lib/components';
|
||||
import {
|
||||
AvatarInitials,
|
||||
Drop,
|
||||
DropList,
|
||||
DropListItem,
|
||||
PaginationWithLimit
|
||||
} from '$lib/components';
|
||||
import Upgrade from '$lib/components/roles/upgrade.svelte';
|
||||
import { Pill } from '$lib/elements';
|
||||
import {
|
||||
TableBody,
|
||||
@@ -13,18 +21,15 @@
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { getRoleLabel } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { newMemberModal, organization } from '$lib/stores/organization';
|
||||
import { isOwner } from '$lib/stores/roles';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import Delete from '../deleteMember.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { isOwner } from '$lib/stores/roles';
|
||||
import type { PageData } from './$types';
|
||||
import Edit from './edit.svelte';
|
||||
import { getRoleLabel } from '$lib/stores/billing';
|
||||
import { Drop } from '$lib/components';
|
||||
import Upgrade from '$lib/components/roles/upgrade.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -113,9 +118,11 @@
|
||||
<TableRow>
|
||||
<TableCell title="Name">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<AvatarInitials size={40} name={member.userName} />
|
||||
<AvatarInitials
|
||||
size={40}
|
||||
name={member.userName || member.userEmail} />
|
||||
<span class="text u-trim">
|
||||
{member.userName ? member.userName : 'n/a'}
|
||||
{member.userName || 'n/a'}
|
||||
</span>
|
||||
{#if member.invited && !member.joined}
|
||||
<Pill warning>Pending</Pill>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived } from 'svelte/store';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export const regions = writable<RegionList | undefined>(undefined);
|
||||
export const regionFlagUrls = derived(regions, ($regions) => {
|
||||
if (!$regions?.regions?.length) return [];
|
||||
|
||||
return $regions?.regions?.map((region) => {
|
||||
return `${sdk.forConsole.client.config.endpoint}/avatars/flags/${region.flag}?width=80&height=60&quality=100&mode=admin`;
|
||||
});
|
||||
});
|
||||
|
||||
export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList);
|
||||
|
||||
+4
-2
@@ -64,9 +64,11 @@
|
||||
<TableRow>
|
||||
<TableCell title="name">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<AvatarInitials size={32} name={member.userName} />
|
||||
<AvatarInitials
|
||||
size={32}
|
||||
name={member.userName || member.userEmail} />
|
||||
<span class="text u-trim">
|
||||
{member.userName ? member.userName : member.userEmail}
|
||||
{member.userName || member.userEmail}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
import { InputText, FormList } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createProject, regions } from './store';
|
||||
import { createProject } from './store';
|
||||
import { regionFlagUrls, regions } from '$routes/(console)/organization-[organization]/store';
|
||||
|
||||
let showCustomId = false;
|
||||
|
||||
sdk.forConsole.billing.listRegions().then(regions.set);
|
||||
if (!$regions?.regions) {
|
||||
sdk.forConsole.billing.listRegions().then(regions.set);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#each $regionFlagUrls as image}
|
||||
<link rel="preload" as="image" href={image} />
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Details</svelte:fragment>
|
||||
<FormList>
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { createProject, regions } from './store';
|
||||
import { createProject } from './store';
|
||||
import type { Region } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
import { regions } from '$routes/(console)/organization-[organization]/store';
|
||||
|
||||
let prefs: Models.Preferences;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const createProject = writable<{
|
||||
@@ -10,5 +9,3 @@ export const createProject = writable<{
|
||||
name: null,
|
||||
region: 'fra'
|
||||
});
|
||||
|
||||
export const regions = writable<RegionList | undefined>(undefined);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
@@ -39,7 +39,7 @@
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b>all of {$user.name}'s sessions?</b>
|
||||
Are you sure you want to delete <b>all of {$user.name || 'User'}'s sessions?</b>
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDeleteAll = false)}>Cancel</Button>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from './store';
|
||||
import { project } from '../../store';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { user } from './store';
|
||||
|
||||
export let showDelete = false;
|
||||
let error: string;
|
||||
@@ -37,7 +37,9 @@
|
||||
state="warning"
|
||||
headerDivider={false}
|
||||
bind:error>
|
||||
<p data-private>Are you sure you want to delete <b>{$user.name}</b> from '{$project.name}'?</p>
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b>{$user.name || 'User'}</b> from '{$project.name}'?
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
let userName: string = null;
|
||||
onMount(async () => {
|
||||
userName ??= $user.name;
|
||||
userName ??= $user.name ?? '';
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
|
||||
export function getCardImgUrls(userId: string, endpoint: string) {
|
||||
const resolved = endpoint;
|
||||
|
||||
@@ -7,3 +9,9 @@ export function getCardImgUrls(userId: string, endpoint: string) {
|
||||
|
||||
return { frontImg, backImg, ogImg };
|
||||
}
|
||||
|
||||
export function getCampaignImageUrl(image: string) {
|
||||
const endpoint = getApiEndpoint();
|
||||
const url = new URL(image, endpoint);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
.getCampaign(coupon.campaign)
|
||||
.catch<null>(() => null);
|
||||
if (campaign && $user) {
|
||||
goto(`${base}/apply-credit?code=${code}`);
|
||||
goto(`${base}/apply-credit?${$page.url.searchParams}`);
|
||||
loading.set(false);
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +59,8 @@
|
||||
.getCampaign(campaignId)
|
||||
.catch<null>(() => null);
|
||||
if (campaign) {
|
||||
goto(`${base}/apply-credit?campaign=${campaign.$id}`);
|
||||
goto(`${base}/apply-credit?${$page.url.searchParams}`);
|
||||
|
||||
loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
+3
-14
@@ -58,18 +58,7 @@ export const load: LayoutLoad = async ({ depends, url, route }) => {
|
||||
}
|
||||
};
|
||||
|
||||
function withParams(pathname: string, searchParams: URLSearchParams): string {
|
||||
const redirect = searchParams.get('redirect');
|
||||
|
||||
if (redirect) {
|
||||
const extras = new URLSearchParams(searchParams);
|
||||
extras.delete('redirect');
|
||||
const extraQuery = extras.toString();
|
||||
const finalRedirect = extraQuery
|
||||
? `${redirect}${redirect.includes('?') ? '&' : '?'}${extraQuery}`
|
||||
: redirect;
|
||||
return `${pathname}?redirect=${encodeURIComponent(finalRedirect)}`;
|
||||
}
|
||||
const query = searchParams.toString();
|
||||
return query ? `${pathname}?${query}` : pathname;
|
||||
function withParams(pathname: string, searchParams: URLSearchParams) {
|
||||
if (searchParams.size > 0) return `${pathname}?${searchParams.toString()}`;
|
||||
return pathname;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 416 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
Reference in New Issue
Block a user