@antelopejs/dms-saas 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +262 -13
- package/dist/auth/subscription-access-gate.d.ts +1 -1
- package/dist/billing-state/index.d.ts +1 -0
- package/dist/billing-state/index.js +1 -0
- package/dist/billing-state/index.js.map +1 -1
- package/dist/billing-state/past-due-banner.d.ts +4 -0
- package/dist/billing-state/past-due-banner.js +25 -0
- package/dist/billing-state/past-due-banner.js.map +1 -0
- package/dist/config/runtime.d.ts +6 -1
- package/dist/config/runtime.js +50 -1
- package/dist/config/runtime.js.map +1 -1
- package/dist/crons/recompute-billing-state.js +4 -1
- package/dist/crons/recompute-billing-state.js.map +1 -1
- package/dist/data-api/platformOwner/index.d.ts +1 -0
- package/dist/data-api/platformOwner/index.js +1 -0
- package/dist/data-api/platformOwner/index.js.map +1 -1
- package/dist/data-api/platformOwner/invitations.d.ts +22 -0
- package/dist/data-api/platformOwner/invitations.js +121 -0
- package/dist/data-api/platformOwner/invitations.js.map +1 -0
- package/dist/data-api/platformOwner/workspaces.d.ts +3 -0
- package/dist/data-api/platformOwner/workspaces.js +64 -12
- package/dist/data-api/platformOwner/workspaces.js.map +1 -1
- package/dist/data-api/tenant/tenant-invoices.js +2 -10
- package/dist/data-api/tenant/tenant-invoices.js.map +1 -1
- package/dist/hooks/register.js +2 -1
- package/dist/hooks/register.js.map +1 -1
- package/dist/implementations/dms-saas/billing.d.ts +2 -1
- package/dist/implementations/dms-saas/billing.js +6 -0
- package/dist/implementations/dms-saas/billing.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/invoice-line-items/injection.d.ts +10 -0
- package/dist/invoice-line-items/injection.js +7 -3
- package/dist/invoice-line-items/injection.js.map +1 -1
- package/dist/notifications/subjects.d.ts +19 -19
- package/dist/operator-actions/index.d.ts +1 -0
- package/dist/operator-actions/index.js +1 -0
- package/dist/operator-actions/index.js.map +1 -1
- package/dist/operator-actions/invitation-commands.d.ts +12 -0
- package/dist/operator-actions/invitation-commands.js +46 -0
- package/dist/operator-actions/invitation-commands.js.map +1 -0
- package/dist/operator-actions/types.d.ts +1 -1
- package/dist/pages/platform/categories.d.ts +6 -6
- package/dist/pages/platform/credit-notes.d.ts +1 -1
- package/dist/pages/platform/dashboard.d.ts +2 -2
- package/dist/pages/platform/dashboard.js +2 -4
- package/dist/pages/platform/dashboard.js.map +1 -1
- package/dist/pages/platform/features.d.ts +1 -1
- package/dist/pages/platform/invoices.d.ts +1 -1
- package/dist/pages/platform/plans/edit.d.ts +2 -2
- package/dist/pages/platform/plans/index.d.ts +1 -1
- package/dist/pages/platform/plans/migrations.d.ts +1 -1
- package/dist/pages/platform/segments/index.d.ts +1 -1
- package/dist/pages/platform/settings.d.ts +1 -1
- package/dist/pages/platform/users/detail.d.ts +1 -1
- package/dist/pages/platform/users/index.d.ts +1 -1
- package/dist/pages/platform/workspaces/detail.d.ts +1 -1
- package/dist/pages/platform/workspaces/detail.js +40 -1
- package/dist/pages/platform/workspaces/detail.js.map +1 -1
- package/dist/pages/platform/workspaces/index.d.ts +4 -10
- package/dist/pages/platform/workspaces/index.js +27 -111
- package/dist/pages/platform/workspaces/index.js.map +1 -1
- package/dist/pages/public/legal/privacy-policy.js +2 -1
- package/dist/pages/public/legal/privacy-policy.js.map +1 -1
- package/dist/pages/public/legal/terms-and-conditions.js +2 -1
- package/dist/pages/public/legal/terms-and-conditions.js.map +1 -1
- package/dist/pages/public/legal/terms-of-use.js +2 -1
- package/dist/pages/public/legal/terms-of-use.js.map +1 -1
- package/dist/pages/tenant/billing.d.ts +1 -1
- package/dist/plans/index.d.ts +1 -0
- package/dist/plans/index.js +1 -0
- package/dist/plans/index.js.map +1 -1
- package/dist/plans/platform-support.d.ts +2 -0
- package/dist/plans/platform-support.js +19 -0
- package/dist/plans/platform-support.js.map +1 -0
- package/dist/plans/seat-capacity.d.ts +9 -2
- package/dist/plans/seat-capacity.js +59 -12
- package/dist/plans/seat-capacity.js.map +1 -1
- package/dist/plans/seat-hooks.js +34 -13
- package/dist/plans/seat-hooks.js.map +1 -1
- package/dist/routes/platformOwner/billing-settings.d.ts +2 -2
- package/dist/routes/platformOwner/dashboard.d.ts +8 -6
- package/dist/routes/platformOwner/dashboard.js +31 -17
- package/dist/routes/platformOwner/dashboard.js.map +1 -1
- package/dist/routes/platformOwner/index.d.ts +1 -0
- package/dist/routes/platformOwner/index.js +1 -0
- package/dist/routes/platformOwner/index.js.map +1 -1
- package/dist/routes/platformOwner/plan-deletion.d.ts +1 -1
- package/dist/routes/platformOwner/plans.d.ts +5 -5
- package/dist/routes/platformOwner/segments.d.ts +1 -1
- package/dist/routes/platformOwner/workspace-invitations.d.ts +13 -0
- package/dist/routes/platformOwner/workspace-invitations.js +73 -0
- package/dist/routes/platformOwner/workspace-invitations.js.map +1 -0
- package/dist/routes/public/legal-documents.d.ts +2 -2
- package/dist/routes/public/register.d.ts +7 -3
- package/dist/routes/public/register.js +42 -27
- package/dist/routes/public/register.js.map +1 -1
- package/dist/routes/tenant/tenant-billing.d.ts +7 -11
- package/dist/routes/tenant/tenant-billing.js +34 -26
- package/dist/routes/tenant/tenant-billing.js.map +1 -1
- package/dist/routes/tenant/tenant-plan.d.ts +1 -0
- package/dist/routes/tenant/tenant-plan.js +27 -2
- package/dist/routes/tenant/tenant-plan.js.map +1 -1
- package/dist/routes/tenant/tenant-seats.d.ts +2 -4
- package/dist/routes/tenant/tenant-seats.js.map +1 -1
- package/dist/routes/tenant/workspaces.d.ts +3 -1
- package/dist/routes/tenant/workspaces.js +10 -4
- package/dist/routes/tenant/workspaces.js.map +1 -1
- package/dist/stripe/upcoming-invoice.d.ts +21 -0
- package/dist/stripe/upcoming-invoice.js +42 -0
- package/dist/stripe/upcoming-invoice.js.map +1 -0
- package/dist/stripe/webhook-dispatch.js +2 -0
- package/dist/stripe/webhook-dispatch.js.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -1
- package/dist/upcoming-invoice/db/preview-cache.model.d.ts +38 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js +57 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js.map +1 -0
- package/dist/upcoming-invoice/db/preview-cache.table.d.ts +10 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js +42 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js.map +1 -0
- package/dist/upcoming-invoice/eligibility.d.ts +17 -0
- package/dist/upcoming-invoice/eligibility.js +52 -0
- package/dist/upcoming-invoice/eligibility.js.map +1 -0
- package/dist/upcoming-invoice/invalidation.d.ts +2 -0
- package/dist/upcoming-invoice/invalidation.js +39 -0
- package/dist/upcoming-invoice/invalidation.js.map +1 -0
- package/dist/upcoming-invoice/mapping.d.ts +8 -0
- package/dist/upcoming-invoice/mapping.js +82 -0
- package/dist/upcoming-invoice/mapping.js.map +1 -0
- package/dist/upcoming-invoice/outcomes.d.ts +13 -0
- package/dist/upcoming-invoice/outcomes.js +15 -0
- package/dist/upcoming-invoice/outcomes.js.map +1 -0
- package/dist/upcoming-invoice/preview.d.ts +3 -0
- package/dist/upcoming-invoice/preview.js +80 -0
- package/dist/upcoming-invoice/preview.js.map +1 -0
- package/dist/upcoming-invoice/pricing.d.ts +3 -0
- package/dist/upcoming-invoice/pricing.js +84 -0
- package/dist/upcoming-invoice/pricing.js.map +1 -0
- package/dist/upcoming-invoice/usage.d.ts +14 -0
- package/dist/upcoming-invoice/usage.js +53 -0
- package/dist/upcoming-invoice/usage.js.map +1 -0
- package/dist/utils/invoice-lines-type.d.ts +1 -1
- package/dist/workspaces/billing-identity.d.ts +19 -0
- package/dist/workspaces/billing-identity.js +51 -0
- package/dist/workspaces/billing-identity.js.map +1 -0
- package/dist/workspaces/default-plan.d.ts +5 -0
- package/dist/workspaces/default-plan.js +109 -0
- package/dist/workspaces/default-plan.js.map +1 -0
- package/dist/workspaces/index.d.ts +2 -0
- package/dist/workspaces/index.js +2 -0
- package/dist/workspaces/index.js.map +1 -1
- package/dist/workspaces/invitations.d.ts +26 -0
- package/dist/workspaces/invitations.js +112 -0
- package/dist/workspaces/invitations.js.map +1 -0
- package/dist/workspaces/provisioning.d.ts +3 -3
- package/dist/workspaces/provisioning.js +39 -11
- package/dist/workspaces/provisioning.js.map +1 -1
- package/frontend-vue/app/components/BillingIdentityFields.vue +179 -0
- package/frontend-vue/app/components/PastDueBanner.vue +81 -0
- package/frontend-vue/app/components/PaymentMethodCard.vue +94 -38
- package/frontend-vue/app/components/PlanComparisonModal.vue +62 -26
- package/frontend-vue/app/components/PlanUpgradeModal.vue +181 -0
- package/frontend-vue/app/components/PlatformNotes.vue +4 -0
- package/frontend-vue/app/components/RefundButton.vue +10 -1
- package/frontend-vue/app/components/Register.vue +49 -92
- package/frontend-vue/app/components/SeatQuotaBanner.vue +28 -0
- package/frontend-vue/app/components/TenantBillingForm.vue +52 -128
- package/frontend-vue/app/components/TenantPlanCard.vue +68 -10
- package/frontend-vue/app/components/UserSummaryCard.vue +12 -10
- package/frontend-vue/app/components/WorkspaceAdminCreateModal.vue +202 -0
- package/frontend-vue/app/components/WorkspaceCreateModal.vue +46 -9
- package/frontend-vue/app/components/WorkspaceInvitationLink.vue +106 -0
- package/frontend-vue/app/components/WorkspaceSummaryCard.vue +39 -15
- package/frontend-vue/app/components/WorkspaceSuspended.vue +3 -5
- package/frontend-vue/app/components/WorkspaceSwitcherWidget.vue +8 -5
- package/frontend-vue/app/composables/useBillingCountries.ts +25 -5
- package/frontend-vue/app/composables/useBillingIdentity.ts +141 -0
- package/frontend-vue/app/composables/useInvitationLink.ts +69 -0
- package/frontend-vue/app/composables/usePastDueBannerAction.ts +20 -0
- package/frontend-vue/app/composables/usePlanComparison.ts +15 -0
- package/frontend-vue/app/composables/usePlanFeatureFormat.ts +312 -20
- package/frontend-vue/app/composables/usePlanFeatureLabel.ts +24 -0
- package/frontend-vue/app/composables/useSaasRegistration.ts +110 -223
- package/frontend-vue/app/composables/useSeatQuota.ts +8 -0
- package/frontend-vue/app/composables/useStripePaymentElement.ts +3 -3
- package/frontend-vue/app/composables/useWorkspaceAccessCache.ts +29 -2
- package/frontend-vue/app/composables/useWorkspaceFinalization.ts +7 -32
- package/frontend-vue/app/custom-pages/auth/no-workspace.vue +49 -194
- package/frontend-vue/app/plugins/auth-links.ts +0 -4
- package/frontend-vue/app/types/runtime-config.ts +13 -0
- package/frontend-vue/dms.frontend.ts +10 -4
- package/frontend-vue/i18n/locales/saas-en-GB.json +161 -36
- package/frontend-vue/i18n/locales/saas-fr-FR.json +162 -37
- package/frontend-vue/package.json +0 -1
- package/frontend-vue/pnpm-lock.yaml +1 -125
- package/package.json +15 -15
- package/frontend-vue/app/middleware/workspace-suspended.global.ts +0 -59
|
@@ -4,54 +4,24 @@ import {
|
|
|
4
4
|
onMounted,
|
|
5
5
|
reactive,
|
|
6
6
|
ref,
|
|
7
|
+
toRef,
|
|
7
8
|
toValue,
|
|
8
|
-
watch,
|
|
9
9
|
} from "vue";
|
|
10
|
-
import { formatMajorUnits } from "./useMoneyFormat";
|
|
11
|
-
import {
|
|
12
|
-
formatPlanIntervalLabel,
|
|
13
|
-
type PlanInterval,
|
|
14
|
-
} from "./usePlanIntervalLabel";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A plan as the public registration screen sees it.
|
|
18
|
-
*/
|
|
19
|
-
export interface RegistrationPlan {
|
|
20
|
-
_id: string;
|
|
21
|
-
name: string;
|
|
22
|
-
description: string;
|
|
23
|
-
price: number;
|
|
24
|
-
currency: string;
|
|
25
|
-
interval: PlanInterval;
|
|
26
|
-
trialDays: number;
|
|
27
|
-
audience: string;
|
|
28
|
-
borderColor: string | null;
|
|
29
|
-
borderLabel: string | null;
|
|
30
|
-
order: number;
|
|
31
|
-
}
|
|
32
10
|
|
|
33
11
|
export interface RegistrationForm {
|
|
34
|
-
customerType: "individual" | "business";
|
|
35
12
|
email: string;
|
|
36
13
|
password: string;
|
|
37
14
|
name: string;
|
|
38
|
-
workspaceName: string;
|
|
39
|
-
companyName: string;
|
|
40
|
-
vatNumber: string;
|
|
41
|
-
country: string;
|
|
42
|
-
addressLine1: string;
|
|
43
|
-
postalCode: string;
|
|
44
|
-
city: string;
|
|
45
|
-
selectedPlanId: string | null;
|
|
46
15
|
hasAcceptedLegal: boolean;
|
|
16
|
+
/** The visitor chose to add a card later. Only read under `optional`. */
|
|
17
|
+
skipsPaymentMethod: boolean;
|
|
47
18
|
}
|
|
48
19
|
|
|
49
20
|
/** Everything the submit guard looks at, independent of how it is bound. */
|
|
50
21
|
export interface RegistrationRequirements {
|
|
51
|
-
|
|
22
|
+
isPaymentRequired: boolean;
|
|
52
23
|
isPaymentReady: boolean;
|
|
53
24
|
hasAcceptedLegal: boolean;
|
|
54
|
-
country: string;
|
|
55
25
|
}
|
|
56
26
|
|
|
57
27
|
export type RegistrationTranslator = (
|
|
@@ -72,7 +42,7 @@ export interface UseSaasRegistrationOptions {
|
|
|
72
42
|
*/
|
|
73
43
|
paymentElementId?: string;
|
|
74
44
|
/**
|
|
75
|
-
* Whatever the consuming SaaS captures on top of the
|
|
45
|
+
* Whatever the consuming SaaS captures on top of the account fields, read
|
|
76
46
|
* when the visitor submits. It stays the consumer's data: dms-saas forwards
|
|
77
47
|
* it inside the provisioning transaction, to the consumer's own
|
|
78
48
|
* `TENANT_BEING_PROVISIONED` listener, and neither reads nor stores it. A
|
|
@@ -89,24 +59,12 @@ interface RegistrationRequirement {
|
|
|
89
59
|
isMet: (requirements: RegistrationRequirements) => boolean;
|
|
90
60
|
}
|
|
91
61
|
|
|
92
|
-
interface RegistrationAddress {
|
|
93
|
-
country: string;
|
|
94
|
-
line1?: string;
|
|
95
|
-
postalCode?: string;
|
|
96
|
-
city?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
62
|
interface RegistrationPayload {
|
|
100
63
|
email: string;
|
|
101
64
|
password: string;
|
|
102
65
|
name: string;
|
|
103
66
|
workspaceName: string;
|
|
104
|
-
|
|
105
|
-
customerType: string;
|
|
106
|
-
companyName?: string;
|
|
107
|
-
vatNumber?: string;
|
|
108
|
-
paymentMethodId: string;
|
|
109
|
-
address: RegistrationAddress;
|
|
67
|
+
paymentMethodId?: string;
|
|
110
68
|
extras?: Record<string, unknown>;
|
|
111
69
|
}
|
|
112
70
|
|
|
@@ -121,44 +79,41 @@ interface SetupIntentResponse {
|
|
|
121
79
|
clientSecret: string | null;
|
|
122
80
|
}
|
|
123
81
|
|
|
124
|
-
|
|
125
|
-
stripePublishableKey?: string;
|
|
126
|
-
admissionMode?: "open" | "invitation-only";
|
|
127
|
-
}
|
|
82
|
+
type PaymentStepRule = (skipsPaymentMethod: boolean) => boolean;
|
|
128
83
|
|
|
129
|
-
const PLANS_ENDPOINT = "/api/saas/plans/public";
|
|
130
84
|
const SETUP_INTENT_ENDPOINT = "/api/saas/register/setup-intent";
|
|
131
85
|
const REGISTER_ENDPOINT = "/api/saas/register";
|
|
132
86
|
const DEFAULT_PAYMENT_ELEMENT_ID = "dms-saas-register-payment-element";
|
|
133
87
|
const DEFAULT_REDIRECT = "/auth/login";
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
88
|
+
const DEFAULT_PAYMENT_POLICY: RegistrationPaymentMethodPolicy = "required";
|
|
89
|
+
const INVITATION_ONLY = "invitation-only";
|
|
90
|
+
const DEFAULT_WORKSPACE_NAME_KEY = "saas.register.default_workspace_name";
|
|
137
91
|
const NO_PAYMENT_ERROR_KEY = "saas.register.error.no_payment";
|
|
138
92
|
const LOAD_ERROR_KEY = "saas.register.error.load";
|
|
139
93
|
const SUBMIT_ERROR_KEY = "saas.register.error.failed";
|
|
94
|
+
const PASSWORD_POLICY_ERROR_KEY = "saas.errors.registration.password_policy";
|
|
95
|
+
|
|
96
|
+
const PAYMENT_STEP_RULES: Record<RegistrationPaymentMethodPolicy, PaymentStepRule> =
|
|
97
|
+
{
|
|
98
|
+
required: () => true,
|
|
99
|
+
optional: (skipsPaymentMethod) => !skipsPaymentMethod,
|
|
100
|
+
none: () => false,
|
|
101
|
+
};
|
|
140
102
|
|
|
141
103
|
/**
|
|
142
|
-
* Checked in the order the visitor can act on them
|
|
143
|
-
*
|
|
104
|
+
* Checked in the order the visitor can act on them: the card field comes
|
|
105
|
+
* before the legal checkbox on every screen.
|
|
144
106
|
*/
|
|
145
107
|
const REGISTRATION_REQUIREMENTS: RegistrationRequirement[] = [
|
|
146
|
-
{
|
|
147
|
-
errorKey: NO_PLAN_ERROR_KEY,
|
|
148
|
-
isMet: (requirements) => Boolean(requirements.selectedPlanId),
|
|
149
|
-
},
|
|
150
108
|
{
|
|
151
109
|
errorKey: NO_PAYMENT_ERROR_KEY,
|
|
152
|
-
isMet: (requirements) =>
|
|
110
|
+
isMet: (requirements) =>
|
|
111
|
+
!requirements.isPaymentRequired || requirements.isPaymentReady,
|
|
153
112
|
},
|
|
154
113
|
{
|
|
155
114
|
errorKey: "saas.register.error.legal_required",
|
|
156
115
|
isMet: (requirements) => requirements.hasAcceptedLegal,
|
|
157
116
|
},
|
|
158
|
-
{
|
|
159
|
-
errorKey: "saas.register.error.no_country",
|
|
160
|
-
isMet: (requirements) => Boolean(requirements.country),
|
|
161
|
-
},
|
|
162
117
|
];
|
|
163
118
|
|
|
164
119
|
/**
|
|
@@ -177,82 +132,44 @@ export function firstMissingRegistrationRequirement(
|
|
|
177
132
|
return missing?.errorKey ?? null;
|
|
178
133
|
}
|
|
179
134
|
|
|
180
|
-
function planIntervalLabel(
|
|
181
|
-
interval: PlanInterval,
|
|
182
|
-
translate: RegistrationTranslator,
|
|
183
|
-
): string {
|
|
184
|
-
return formatPlanIntervalLabel(interval, "saas.register.interval", translate);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
135
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* Takes its locale and translator instead of reading them from the component
|
|
192
|
-
* context, so a consumer can price a plan outside a `setup()` call.
|
|
136
|
+
* The card policy the deployment published, falling back to the backend's
|
|
137
|
+
* own default when it published none.
|
|
193
138
|
*
|
|
194
|
-
* @param
|
|
195
|
-
* @
|
|
196
|
-
* @param translate Translation function for the price pattern and the cadence
|
|
197
|
-
* @returns Localised price, e.g. `20 €/mois`
|
|
139
|
+
* @param config dms-saas public runtime config
|
|
140
|
+
* @returns The configured policy, or `required`
|
|
198
141
|
*/
|
|
199
|
-
export function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const price = formatMajorUnits(plan.price, plan.currency, locale, {
|
|
205
|
-
hideWholeAmountDecimals: true,
|
|
206
|
-
});
|
|
207
|
-
return translate(PLAN_PRICE_KEY, {
|
|
208
|
-
price,
|
|
209
|
-
interval: planIntervalLabel(plan.interval, translate),
|
|
210
|
-
});
|
|
142
|
+
export function resolveRegistrationPaymentPolicy(
|
|
143
|
+
config: DmsSaasPublicRuntimeConfig | undefined,
|
|
144
|
+
): RegistrationPaymentMethodPolicy {
|
|
145
|
+
const policy = config?.registrationPaymentMethod;
|
|
146
|
+
return policy && policy in PAYMENT_STEP_RULES ? policy : DEFAULT_PAYMENT_POLICY;
|
|
211
147
|
}
|
|
212
148
|
|
|
213
149
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* Sorted here because the endpoint returns plans in storage order, and the
|
|
217
|
-
* first plan on offer is the one that ends up pre-selected: it has to be the
|
|
218
|
-
* one the operator ranked first, not the one created first.
|
|
150
|
+
* Whether the registration still collects a card.
|
|
219
151
|
*
|
|
220
|
-
* @param
|
|
221
|
-
* @param
|
|
222
|
-
* @returns
|
|
152
|
+
* @param policy Configured card policy
|
|
153
|
+
* @param skipsPaymentMethod Whether the visitor chose to add a card later
|
|
154
|
+
* @returns True when the card step is shown and must be completed
|
|
223
155
|
*/
|
|
224
|
-
export function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
):
|
|
228
|
-
return
|
|
229
|
-
.filter(
|
|
230
|
-
(plan) => plan.audience === ANY_AUDIENCE || plan.audience === customerType,
|
|
231
|
-
)
|
|
232
|
-
.sort((left, right) => left.order - right.order);
|
|
156
|
+
export function isPaymentStepShown(
|
|
157
|
+
policy: RegistrationPaymentMethodPolicy,
|
|
158
|
+
skipsPaymentMethod: boolean,
|
|
159
|
+
): boolean {
|
|
160
|
+
return PAYMENT_STEP_RULES[policy](skipsPaymentMethod);
|
|
233
161
|
}
|
|
234
162
|
|
|
235
163
|
/**
|
|
236
|
-
*
|
|
164
|
+
* Whether the deployment has closed public registration to invitations.
|
|
237
165
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* falls back to the first plan they may actually buy.
|
|
241
|
-
*
|
|
242
|
-
* @param available Plans the current customer type may subscribe to
|
|
243
|
-
* @param current Plan id currently selected, if any
|
|
244
|
-
* @returns Plan id to select, or null when nothing is on offer
|
|
166
|
+
* @param config dms-saas public runtime config
|
|
167
|
+
* @returns True when only invited accounts may sign up
|
|
245
168
|
*/
|
|
246
|
-
export function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const isStillOffered = available.some((plan) => plan._id === current);
|
|
251
|
-
return isStillOffered ? current : (available[0]?._id ?? null);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function optionalField(value: string): string | undefined {
|
|
255
|
-
return value.trim() || undefined;
|
|
169
|
+
export function isRegistrationClosedBy(
|
|
170
|
+
config: DmsSaasPublicRuntimeConfig | undefined,
|
|
171
|
+
): boolean {
|
|
172
|
+
return config?.admissionMode === INVITATION_ONLY;
|
|
256
173
|
}
|
|
257
174
|
|
|
258
175
|
/**
|
|
@@ -276,12 +193,16 @@ export function snapshotRegistrationExtras(
|
|
|
276
193
|
/**
|
|
277
194
|
* The public registration flow, without a single line of presentation.
|
|
278
195
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
196
|
+
* Registration is short on purpose: an account, the legal acceptance and —
|
|
197
|
+
* when the deployment asks for one — a card. The workspace it opens lands on
|
|
198
|
+
* the free plan under a default name; plan, customer type and billing address
|
|
199
|
+
* are collected by the upgrade flow.
|
|
200
|
+
*
|
|
201
|
+
* dms-saas owns what is dangerous to re-derive — the card policy, the Stripe
|
|
202
|
+
* setup intent and card confirmation, the order the fields are validated in,
|
|
203
|
+
* the shape of the provisioning call, the mapping from API failures to a
|
|
204
|
+
* message — and hands the consumer plain state to bind and one action to
|
|
205
|
+
* call. Each SaaS then writes the markup its conversion funnel needs.
|
|
285
206
|
*
|
|
286
207
|
* The screen shipped in this package is one consumer among others: replace it
|
|
287
208
|
* by turning it off (`publicScreens.register: false` in the module config) and
|
|
@@ -289,12 +210,11 @@ export function snapshotRegistrationExtras(
|
|
|
289
210
|
* want on this composable rather than forking its logic.
|
|
290
211
|
*
|
|
291
212
|
* @param options Landing route and payment element id
|
|
292
|
-
* @returns Bindable form state,
|
|
213
|
+
* @returns Bindable form state, card policy, submission action and status
|
|
293
214
|
*/
|
|
294
215
|
export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
295
216
|
const { $authFetch } = useAuthFetch();
|
|
296
217
|
const { resolveApiError } = useApiErrorMessage();
|
|
297
|
-
const { countryItems } = useBillingCountries();
|
|
298
218
|
const { $i18n } = useDmsApp();
|
|
299
219
|
const config = useDmsRuntimeConfig();
|
|
300
220
|
|
|
@@ -304,24 +224,18 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
304
224
|
const paymentElementId =
|
|
305
225
|
options.paymentElementId ?? DEFAULT_PAYMENT_ELEMENT_ID;
|
|
306
226
|
const redirectTo = options.redirectTo ?? DEFAULT_REDIRECT;
|
|
227
|
+
const saasConfig = config.public.dmsSaas as
|
|
228
|
+
| DmsSaasPublicRuntimeConfig
|
|
229
|
+
| undefined;
|
|
307
230
|
|
|
308
231
|
const form = reactive<RegistrationForm>({
|
|
309
|
-
customerType: "individual",
|
|
310
232
|
email: "",
|
|
311
233
|
password: "",
|
|
312
234
|
name: "",
|
|
313
|
-
workspaceName: "",
|
|
314
|
-
companyName: "",
|
|
315
|
-
vatNumber: "",
|
|
316
|
-
country: "",
|
|
317
|
-
addressLine1: "",
|
|
318
|
-
postalCode: "",
|
|
319
|
-
city: "",
|
|
320
|
-
selectedPlanId: null,
|
|
321
235
|
hasAcceptedLegal: false,
|
|
236
|
+
skipsPaymentMethod: false,
|
|
322
237
|
});
|
|
323
238
|
|
|
324
|
-
const allPlans = ref<RegistrationPlan[]>([]);
|
|
325
239
|
const isLoading = ref(true);
|
|
326
240
|
const isSubmitting = ref(false);
|
|
327
241
|
const errorMessage = ref<string | null>(null);
|
|
@@ -329,19 +243,15 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
329
243
|
null,
|
|
330
244
|
);
|
|
331
245
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
/** Plans the current customer type may subscribe to, in the operator's order. */
|
|
339
|
-
const plans = computed(() =>
|
|
340
|
-
resolveOfferedPlans(allPlans.value, form.customerType),
|
|
341
|
-
);
|
|
246
|
+
/** Registration by invitation only: render the closed state, not the form. */
|
|
247
|
+
const isRegistrationClosed = isRegistrationClosedBy(saasConfig);
|
|
248
|
+
const paymentMethodPolicy = resolveRegistrationPaymentPolicy(saasConfig);
|
|
249
|
+
/** Whether the screen should offer to add the card later. */
|
|
250
|
+
const canSkipPaymentMethod = paymentMethodPolicy === "optional";
|
|
342
251
|
|
|
343
|
-
|
|
344
|
-
|
|
252
|
+
/** Bind the card field's visibility to this, with `v-show`, not `v-if`. */
|
|
253
|
+
const isPaymentStepVisible = computed(() =>
|
|
254
|
+
isPaymentStepShown(paymentMethodPolicy, form.skipsPaymentMethod),
|
|
345
255
|
);
|
|
346
256
|
|
|
347
257
|
/**
|
|
@@ -350,49 +260,37 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
350
260
|
*/
|
|
351
261
|
const isPaymentReady = computed(() => stripeHandle.value !== null);
|
|
352
262
|
|
|
263
|
+
/**
|
|
264
|
+
* DMS's password rules, scored for `<DmsPasswordStrength>`. The API holds
|
|
265
|
+
* the password to the same policy.
|
|
266
|
+
*/
|
|
267
|
+
const passwordStrength = usePasswordStrength(toRef(form, "password"));
|
|
268
|
+
|
|
353
269
|
const requirements = computed<RegistrationRequirements>(() => ({
|
|
354
|
-
|
|
270
|
+
isPaymentRequired: isPaymentStepVisible.value,
|
|
355
271
|
isPaymentReady: isPaymentReady.value,
|
|
356
272
|
hasAcceptedLegal: form.hasAcceptedLegal,
|
|
357
|
-
country: form.country,
|
|
358
273
|
}));
|
|
359
274
|
|
|
275
|
+
// The password comes first: a card confirmed for a registration the API
|
|
276
|
+
// then refuses would be confirmed for nothing.
|
|
360
277
|
const missingRequirement = computed(() =>
|
|
361
|
-
|
|
278
|
+
passwordSchema.safeParse(form.password).success
|
|
279
|
+
? firstMissingRegistrationRequirement(requirements.value)
|
|
280
|
+
: PASSWORD_POLICY_ERROR_KEY,
|
|
362
281
|
);
|
|
363
282
|
|
|
364
283
|
const canSubmit = computed(
|
|
365
284
|
() => !isSubmitting.value && missingRequirement.value === null,
|
|
366
285
|
);
|
|
367
286
|
|
|
368
|
-
function formatPlanPrice(plan: RegistrationPlan): string {
|
|
369
|
-
return formatRegistrationPlanPrice(plan, $i18n.locale.value, translate);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Repaired synchronously, not on the next flush: a consumer that switches
|
|
373
|
-
// customer type and submits in the same tick would otherwise send the plan
|
|
374
|
-
// the visitor can no longer buy.
|
|
375
|
-
watch(
|
|
376
|
-
plans,
|
|
377
|
-
(available) => {
|
|
378
|
-
form.selectedPlanId = resolveSelectedPlanId(
|
|
379
|
-
available,
|
|
380
|
-
form.selectedPlanId,
|
|
381
|
-
);
|
|
382
|
-
},
|
|
383
|
-
{ flush: "sync" },
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
async function loadPlans(): Promise<void> {
|
|
387
|
-
allPlans.value = await $authFetch<RegistrationPlan[]>(PLANS_ENDPOINT);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
287
|
async function mountPaymentElement(): Promise<void> {
|
|
391
|
-
|
|
288
|
+
const publishableKey = saasConfig?.stripePublishableKey;
|
|
289
|
+
if (!publishableKey) return;
|
|
392
290
|
const setup = await $authFetch<SetupIntentResponse>(SETUP_INTENT_ENDPOINT);
|
|
393
291
|
if (!setup.clientSecret) return;
|
|
394
292
|
stripeHandle.value = useStripePaymentElement({
|
|
395
|
-
publishableKey
|
|
293
|
+
publishableKey,
|
|
396
294
|
clientSecret: setup.clientSecret,
|
|
397
295
|
containerId: paymentElementId,
|
|
398
296
|
});
|
|
@@ -411,33 +309,23 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
411
309
|
*
|
|
412
310
|
* Taken whole, before the card confirmation: that step can hold a 3-D Secure
|
|
413
311
|
* challenge open for seconds while the fields stay editable, and a payload
|
|
414
|
-
* half read before and half after would
|
|
415
|
-
* the customer type chosen since — which the API rejects, once the card has
|
|
416
|
-
* already been confirmed.
|
|
312
|
+
* half read before and half after would not be the one the visitor sent.
|
|
417
313
|
*/
|
|
418
|
-
function captureSubmission(
|
|
419
|
-
const isBusiness = form.customerType === "business";
|
|
314
|
+
function captureSubmission(): RegistrationSubmission {
|
|
420
315
|
return {
|
|
421
316
|
email: form.email,
|
|
422
317
|
password: form.password,
|
|
423
318
|
name: form.name,
|
|
424
|
-
workspaceName:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
companyName: isBusiness ? form.companyName : undefined,
|
|
428
|
-
vatNumber: isBusiness ? optionalField(form.vatNumber) : undefined,
|
|
429
|
-
address: {
|
|
430
|
-
country: form.country,
|
|
431
|
-
line1: optionalField(form.addressLine1),
|
|
432
|
-
postalCode: optionalField(form.postalCode),
|
|
433
|
-
city: optionalField(form.city),
|
|
434
|
-
},
|
|
319
|
+
workspaceName: translate(DEFAULT_WORKSPACE_NAME_KEY, {
|
|
320
|
+
name: form.name.trim(),
|
|
321
|
+
}),
|
|
435
322
|
extras: snapshotRegistrationExtras(toValue(options.extras)),
|
|
436
323
|
};
|
|
437
324
|
}
|
|
438
325
|
|
|
439
326
|
/**
|
|
440
|
-
* Confirm the card, provision the workspace, land on
|
|
327
|
+
* Confirm the card when one is collected, provision the workspace, land on
|
|
328
|
+
* the configured route.
|
|
441
329
|
*
|
|
442
330
|
* Everything the caller needs to know travels through `errorMessage`:
|
|
443
331
|
* nothing throws, so a consumer template can bind the action directly.
|
|
@@ -449,11 +337,8 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
449
337
|
// the first is in flight would run both again.
|
|
450
338
|
if (isSubmitting.value) return null;
|
|
451
339
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
errorMessage.value = translate(
|
|
455
|
-
missingRequirement.value ?? NO_PLAN_ERROR_KEY,
|
|
456
|
-
);
|
|
340
|
+
if (missingRequirement.value) {
|
|
341
|
+
errorMessage.value = translate(missingRequirement.value);
|
|
457
342
|
return null;
|
|
458
343
|
}
|
|
459
344
|
|
|
@@ -463,9 +348,11 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
463
348
|
try {
|
|
464
349
|
// Inside the guard: freezing the capture reads the consumer's object,
|
|
465
350
|
// and a consumer's object is never guaranteed to be serialisable.
|
|
466
|
-
const submission = captureSubmission(
|
|
467
|
-
const paymentMethodId =
|
|
468
|
-
|
|
351
|
+
const submission = captureSubmission();
|
|
352
|
+
const paymentMethodId = isPaymentStepVisible.value
|
|
353
|
+
? await confirmPaymentMethod()
|
|
354
|
+
: undefined;
|
|
355
|
+
if (paymentMethodId === null) return null;
|
|
469
356
|
|
|
470
357
|
const result = await $authFetch<RegistrationResult>(REGISTER_ENDPOINT, {
|
|
471
358
|
method: "POST",
|
|
@@ -477,8 +364,8 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
477
364
|
return tenantId;
|
|
478
365
|
} catch (error) {
|
|
479
366
|
// Once the workspace exists, whatever failed after it is a landing
|
|
480
|
-
// problem: calling it a failed registration sends
|
|
481
|
-
//
|
|
367
|
+
// problem: calling it a failed registration sends the visitor back
|
|
368
|
+
// through a flow that would create a second account.
|
|
482
369
|
if (tenantId) return tenantId;
|
|
483
370
|
errorMessage.value = resolveApiError(error, SUBMIT_ERROR_KEY);
|
|
484
371
|
return null;
|
|
@@ -488,15 +375,14 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
488
375
|
}
|
|
489
376
|
|
|
490
377
|
onMounted(async () => {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
) {
|
|
378
|
+
// Closed registration and a card-less policy never reach Stripe: the
|
|
379
|
+
// setup intent would only answer 403 or 400.
|
|
380
|
+
if (isRegistrationClosed || paymentMethodPolicy === "none") {
|
|
495
381
|
isLoading.value = false;
|
|
496
382
|
return;
|
|
497
383
|
}
|
|
498
384
|
try {
|
|
499
|
-
await
|
|
385
|
+
await mountPaymentElement();
|
|
500
386
|
} catch (error) {
|
|
501
387
|
errorMessage.value = resolveApiError(error, LOAD_ERROR_KEY);
|
|
502
388
|
} finally {
|
|
@@ -506,17 +392,18 @@ export function useSaasRegistration(options: UseSaasRegistrationOptions = {}) {
|
|
|
506
392
|
|
|
507
393
|
return {
|
|
508
394
|
form,
|
|
509
|
-
plans,
|
|
510
|
-
selectedPlan,
|
|
511
|
-
countryItems,
|
|
512
395
|
paymentElementId,
|
|
396
|
+
paymentMethodPolicy,
|
|
397
|
+
canSkipPaymentMethod,
|
|
398
|
+
isPaymentStepVisible,
|
|
513
399
|
isPaymentReady,
|
|
400
|
+
passwordStrength,
|
|
401
|
+
isRegistrationClosed,
|
|
514
402
|
isLoading,
|
|
515
403
|
isSubmitting,
|
|
516
404
|
errorMessage,
|
|
517
405
|
missingRequirement,
|
|
518
406
|
canSubmit,
|
|
519
|
-
formatPlanPrice,
|
|
520
407
|
submit,
|
|
521
408
|
};
|
|
522
409
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const SEAT_QUOTA_ENDPOINT = "/api/saas/tenant/seats";
|
|
2
2
|
const SEAT_QUOTA_STATE_KEY = "saas-seat-quota";
|
|
3
3
|
|
|
4
|
+
export interface PlatformSupportMember {
|
|
5
|
+
userId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export interface SeatQuotaResponse {
|
|
5
11
|
members: number;
|
|
6
12
|
pendingInvites: number;
|
|
@@ -8,6 +14,8 @@ export interface SeatQuotaResponse {
|
|
|
8
14
|
/** `null` when no plan is in force, i.e. no seat ceiling to enforce. */
|
|
9
15
|
maxMembers: number | null;
|
|
10
16
|
isTenantOwner: boolean;
|
|
17
|
+
/** Platform owners with access to the workspace; they hold no seat. */
|
|
18
|
+
platformSupport: PlatformSupportMember[];
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export function useSeatQuota(): SharedRequest<SeatQuotaResponse> {
|
|
@@ -15,7 +15,9 @@ interface StripePaymentElementHandle {
|
|
|
15
15
|
confirmAndGetPaymentMethod: () => Promise<ConfirmResult>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Payment method types are fixed server-side on the SetupIntent: Stripe
|
|
19
|
+
// rejects `paymentMethodTypes` next to a `clientSecret`, since it only applies
|
|
20
|
+
// to an Elements instance created in deferred-intent `mode`.
|
|
19
21
|
const ALLOWED_CARD_BRANDS = ["visa", "mastercard"];
|
|
20
22
|
|
|
21
23
|
export function useStripePaymentElement(
|
|
@@ -32,10 +34,8 @@ export function useStripePaymentElement(
|
|
|
32
34
|
}
|
|
33
35
|
elementsInstance = stripeInstance.elements({
|
|
34
36
|
clientSecret: options.clientSecret,
|
|
35
|
-
paymentMethodTypes: ALLOWED_PAYMENT_METHODS,
|
|
36
37
|
});
|
|
37
38
|
const paymentElement = elementsInstance.create("payment", {
|
|
38
|
-
paymentMethodTypes: ALLOWED_PAYMENT_METHODS,
|
|
39
39
|
wallets: { applePay: "never", googlePay: "never" },
|
|
40
40
|
fields: {
|
|
41
41
|
billingDetails: { address: "auto", name: "auto", email: "never" },
|
|
@@ -14,8 +14,8 @@ export interface CachedWorkspaceAccess {
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Per-session cache of the current workspace's subscription access, shared
|
|
17
|
-
* between
|
|
18
|
-
*
|
|
17
|
+
* between its readers (the refund card) and the suspended screen (invalidator
|
|
18
|
+
* once the workspace unblocks).
|
|
19
19
|
*/
|
|
20
20
|
export function useWorkspaceAccessCache() {
|
|
21
21
|
return useDmsState<CachedWorkspaceAccess | null>(
|
|
@@ -23,3 +23,30 @@ export function useWorkspaceAccessCache() {
|
|
|
23
23
|
() => null,
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
const ACCESS_ENDPOINT = "/api/saas/billing/access";
|
|
28
|
+
const FALLBACK_TENANT_ID = "default";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The active workspace's access, served from the cache while it still belongs
|
|
32
|
+
* to the active tenant and fetched otherwise.
|
|
33
|
+
*
|
|
34
|
+
* @returns The access, or null on a transport error — callers fail open, the
|
|
35
|
+
* backend gate still denies every tenant surface
|
|
36
|
+
*/
|
|
37
|
+
export async function loadWorkspaceAccess(): Promise<WorkspaceAccess | null> {
|
|
38
|
+
const { session } = useUserSession();
|
|
39
|
+
const tenantId =
|
|
40
|
+
(session.value as { activeTenantId?: string } | null)?.activeTenantId ??
|
|
41
|
+
FALLBACK_TENANT_ID;
|
|
42
|
+
const cached = useWorkspaceAccessCache();
|
|
43
|
+
if (cached.value?.tenantId === tenantId) return cached.value.access;
|
|
44
|
+
const { $authFetch } = useAuthFetch();
|
|
45
|
+
try {
|
|
46
|
+
const access = await $authFetch<WorkspaceAccess>(ACCESS_ENDPOINT);
|
|
47
|
+
cached.value = { tenantId, access };
|
|
48
|
+
return access;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|