@cosmicdrift/kumiko-bundled-features 0.342.0 → 0.344.0
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/dist/audit/constants.d.ts +5 -1
- package/dist/audit/constants.js +5 -1
- package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
- package/dist/audit/escape-hatch-audit-sink.js +1 -0
- package/dist/audit/run-escape-hatch-retention.d.ts +3 -2
- package/dist/audit/run-escape-hatch-retention.js +71 -7
- package/dist/auth-email-password/email-templates.d.ts +4 -0
- package/dist/auth-email-password/email-templates.js +69 -16
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
- package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +2 -0
- package/dist/billing-foundation/checkout-core.d.ts +15 -4
- package/dist/billing-foundation/checkout-core.js +18 -6
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +13 -3
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +23 -20
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +6 -1
- package/dist/billing-foundation/consumer-protection/consent-gate.js +6 -1
- package/dist/billing-foundation/consumer-protection/consent-locale.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +24 -2
- package/dist/billing-foundation/events.d.ts +2 -0
- package/dist/billing-foundation/events.js +2 -0
- package/dist/billing-foundation/handlers/billing-plans.query.d.ts +1 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +5 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +46 -24
- package/dist/billing-foundation/index.d.ts +3 -1
- package/dist/billing-foundation/index.js +2 -0
- package/dist/billing-foundation/types.d.ts +16 -7
- package/dist/billing-foundation/types.js +3 -0
- package/dist/billing-foundation/validate-options.d.ts +1 -1
- package/dist/billing-foundation/validate-options.js +24 -2
- package/dist/billing-foundation/web/billing-plans-panel.js +1 -1
- package/dist/billing-foundation/web/cancel-contract-dialog.js +2 -2
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +1 -1
- package/dist/billing-foundation/web/checkout-consent-dialog.js +4 -4
- package/dist/billing-foundation/web/index.d.ts +3 -1
- package/dist/billing-foundation/web/index.js +2 -0
- package/dist/channel-discord/feature.js +1 -0
- package/dist/channel-email/feature.js +1 -0
- package/dist/channel-in-app/feature.js +1 -0
- package/dist/channel-push/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- package/dist/channel-teams/teams-channel.d.ts +2 -1
- package/dist/channel-teams/teams-channel.js +12 -0
- package/dist/channel-telegram/feature.js +1 -0
- package/dist/delivery/chat-webhook-channel.d.ts +2 -1
- package/dist/delivery/chat-webhook-channel.js +5 -2
- package/dist/delivery/chat-webhook-sender.d.ts +8 -1
- package/dist/delivery/chat-webhook-sender.js +45 -4
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/delivery-service.d.ts +2 -1
- package/dist/delivery/delivery-service.js +55 -4
- package/dist/delivery/events.d.ts +19 -1
- package/dist/delivery/events.js +11 -1
- package/dist/delivery/feature.js +29 -5
- package/dist/delivery/handlers/log.query.d.ts +4 -2
- package/dist/delivery/handlers/log.query.js +46 -18
- package/dist/delivery/i18n.js +36 -0
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/jobs.js +24 -10
- package/dist/delivery/notify-factory.d.ts +5 -0
- package/dist/delivery/notify-factory.js +6 -0
- package/dist/delivery/public-names.d.ts +4 -0
- package/dist/delivery/public-names.js +4 -0
- package/dist/delivery/tables.js +3 -0
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/testing.js +2 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/delivery/web/client-plugin.js +9 -1
- package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-channel-cell.js +10 -0
- package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-error-cell.js +16 -0
- package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
- package/dist/delivery/web/delivery-status-cell.js +11 -1
- package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-time-cell.js +6 -0
- package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-type-cell.js +17 -0
- package/dist/legal-pages/constants.d.ts +2 -0
- package/dist/legal-pages/feature.js +7 -2
- package/dist/renderer-simple/simple-renderer.js +8 -0
- package/dist/secrets/constants.d.ts +6 -0
- package/dist/secrets/constants.js +8 -0
- package/dist/secrets/feature.d.ts +3 -0
- package/dist/secrets/feature.js +3 -0
- package/dist/secrets/i18n.js +4 -3
- package/dist/secrets/index.d.ts +3 -2
- package/dist/secrets/index.js +3 -2
- package/dist/secrets/write-gate.d.ts +1 -0
- package/dist/secrets/write-gate.js +14 -6
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/redact.d.ts +4 -0
- package/dist/shared/redact.js +21 -0
- package/dist/step-dispatcher/feature.js +2 -1
- package/dist/step-dispatcher/mail-runner.d.ts +0 -1
- package/dist/step-dispatcher/mail-runner.js +0 -5
- package/dist/tenant/web/member-roles-cell.js +1 -1
- package/dist/tenant/web/member-status-cell.js +1 -1
- package/dist/tenant-lifecycle/feature.js +8 -2
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
- package/dist/user/index.d.ts +1 -0
- package/dist/user/index.js +1 -0
- package/dist/user/resolve-display-names.d.ts +2 -0
- package/dist/user/resolve-display-names.js +24 -0
- package/package.json +9 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-teams/changes.json +8 -0
- package/src/delivery/changes.json +32 -0
- package/src/jobs/changes.json +6 -0
- package/src/legal-pages/changes.json +6 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
- /package/dist/{tenant → shared}/web/translate-or-raw.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Crypto-free so the browser panel can share the locale rule with the server.
|
|
3
3
|
export const CONSENT_LOCALES = ["de", "en"];
|
|
4
|
-
export const FALLBACK_CONSENT_LOCALE = "
|
|
4
|
+
export const FALLBACK_CONSENT_LOCALE = "en";
|
|
5
5
|
function isConsentLocale(value) {
|
|
6
6
|
return CONSENT_LOCALES.some((locale) => locale === value);
|
|
7
7
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// § 312f contract confirmation: one mail per recorded consent, sent by the
|
|
2
2
|
// system-only issue-contract-confirmation handler that the event-triggered
|
|
3
3
|
// jobs (register-consent.ts) call.
|
|
4
|
-
import { access, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { access, createSystemUser, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
|
+
import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
|
|
7
|
+
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
6
8
|
import * as z from "zod";
|
|
7
9
|
import { requireTemplateResolver, TEXT_BLOCK_KIND } from "../../template-resolver/index.js";
|
|
8
10
|
import { UserQueries } from "../../user/index.js";
|
|
@@ -22,7 +24,17 @@ export const issueContractConfirmationSchema = z
|
|
|
22
24
|
const recipientRowSchema = z.object({
|
|
23
25
|
email: z.string().min(1),
|
|
24
26
|
isDeleted: z.boolean().optional(),
|
|
27
|
+
timezone: z.string().nullish(),
|
|
25
28
|
});
|
|
29
|
+
// createI18n returns the key itself for an unknown key.
|
|
30
|
+
function translatedOrUndefined(translation, key) {
|
|
31
|
+
return translation === key ? undefined : translation;
|
|
32
|
+
}
|
|
33
|
+
function pickTimeZone(userZone, tenantZone) {
|
|
34
|
+
if (userZone && isValidIanaTimeZone(userZone))
|
|
35
|
+
return userZone;
|
|
36
|
+
return isValidIanaTimeZone(tenantZone) ? tenantZone : "UTC";
|
|
37
|
+
}
|
|
26
38
|
export function createIssueContractConfirmationHandler(options, consumerProtection) {
|
|
27
39
|
return {
|
|
28
40
|
name: "issue-contract-confirmation",
|
|
@@ -100,7 +112,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
100
112
|
}
|
|
101
113
|
const locale = resolveConsentLocale(consent.locale);
|
|
102
114
|
const terms = await requireTemplateResolver(ctx, "billing-foundation").resolveTemplate({
|
|
103
|
-
tenantId,
|
|
115
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
104
116
|
slug: consumerProtection.termsTextBlock,
|
|
105
117
|
kind: TEXT_BLOCK_KIND,
|
|
106
118
|
locale,
|
|
@@ -108,9 +120,19 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
108
120
|
if (terms.version !== consent.termsTemplateVersion) {
|
|
109
121
|
ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
|
|
110
122
|
}
|
|
123
|
+
const i18n = createI18n(ctx.registry, { defaultLocale: "en" });
|
|
124
|
+
const tierLabel = consent.tier !== null && options.catalog
|
|
125
|
+
? translatedOrUndefined(i18n.t(options.catalog.tierLabelKey(consent.tier), locale), options.catalog.tierLabelKey(consent.tier))
|
|
126
|
+
: undefined;
|
|
127
|
+
const itemLabel = consent.itemLabelKey !== undefined
|
|
128
|
+
? translatedOrUndefined(i18n.t(consent.itemLabelKey, locale, consent.itemLabelParams), consent.itemLabelKey)
|
|
129
|
+
: undefined;
|
|
111
130
|
const issuedAtIso = options.now().toString();
|
|
112
131
|
const content = renderContractConfirmation({
|
|
113
132
|
consent,
|
|
133
|
+
timeZone: pickTimeZone(recipient.data.timezone, ctx.tz.tenant),
|
|
134
|
+
...(tierLabel !== undefined && { tierLabel }),
|
|
135
|
+
...(itemLabel !== undefined && { itemLabel }),
|
|
114
136
|
consentGivenAtIso: consentEvent.createdAt.toString(),
|
|
115
137
|
contractStartIso: issuedAtIso,
|
|
116
138
|
...(payload.currentPeriodEndIso !== undefined && {
|
|
@@ -65,6 +65,8 @@ export declare const checkoutConsentRecordedPayloadSchema: z.ZodObject<{
|
|
|
65
65
|
termsTemplateVersion: z.ZodNumber;
|
|
66
66
|
locale: z.ZodString;
|
|
67
67
|
actorUserId: z.ZodString;
|
|
68
|
+
itemLabelKey: z.ZodOptional<z.ZodString>;
|
|
69
|
+
itemLabelParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
68
70
|
}, z.core.$strip>;
|
|
69
71
|
export type CheckoutConsentRecordedPayload = z.infer<typeof checkoutConsentRecordedPayloadSchema>;
|
|
70
72
|
export declare const CONTRACT_CONFIRMATION_ISSUED_EVENT_SHORT: "contract-confirmation-issued";
|
|
@@ -101,6 +101,8 @@ export const checkoutConsentRecordedPayloadSchema = z.object({
|
|
|
101
101
|
termsTemplateVersion: z.number(),
|
|
102
102
|
locale: z.string().min(2).max(35),
|
|
103
103
|
actorUserId: z.string().min(1),
|
|
104
|
+
itemLabelKey: z.string().min(1).max(200).optional(),
|
|
105
|
+
itemLabelParams: z.record(z.string(), z.union([z.string().max(100), z.number()])).optional(),
|
|
104
106
|
});
|
|
105
107
|
// =============================================================================
|
|
106
108
|
// contract-confirmation-issued — § 312f confirmation mail sent
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import type
|
|
2
|
+
import { type BillingPlanCatalog, type ResolvedBillingFoundationOptions } from "../types.js";
|
|
3
3
|
export declare function createBillingPlansQuery(options: ResolvedBillingFoundationOptions, catalog: BillingPlanCatalog): QueryHandlerDef;
|
|
@@ -5,6 +5,7 @@ import * as z from "zod";
|
|
|
5
5
|
import { findCatalogProvider } from "../checkout-core.js";
|
|
6
6
|
import { CONSENT_TEXTS, consentTextVersion, } from "../consumer-protection/consent-text.js";
|
|
7
7
|
import { buildBillingPlans } from "../plan-catalog.js";
|
|
8
|
+
import { isLegalLinkSet, } from "../types.js";
|
|
8
9
|
const billingPlansSchema = z.object({}).strict();
|
|
9
10
|
export function createBillingPlansQuery(options, catalog) {
|
|
10
11
|
return {
|
|
@@ -25,12 +26,15 @@ export function createBillingPlansQuery(options, catalog) {
|
|
|
25
26
|
de: consentTextView("de"),
|
|
26
27
|
en: consentTextView("en"),
|
|
27
28
|
},
|
|
28
|
-
legalLinks: consumerProtection.legalLinks,
|
|
29
|
+
legalLinks: legalLinksPerLocale(consumerProtection.legalLinks),
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
function legalLinksPerLocale(legalLinks) {
|
|
36
|
+
return isLegalLinkSet(legalLinks) ? { de: legalLinks, en: legalLinks } : legalLinks;
|
|
37
|
+
}
|
|
34
38
|
function consentTextView(locale) {
|
|
35
39
|
const { earlyPerformance, withdrawalLoss } = CONSENT_TEXTS[locale];
|
|
36
40
|
return { earlyPerformance, withdrawalLoss, consentTextVersion: consentTextVersion(locale) };
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
// billing-foundation:write:switch-plan instead. Plan-driven callers should
|
|
22
22
|
// prefer start-plan-checkout/switch-plan over calling this handler with a
|
|
23
23
|
// hand-picked priceId.
|
|
24
|
+
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
24
25
|
import * as z from "zod";
|
|
25
|
-
import {
|
|
26
|
+
import { assertCheckoutAllowed, openCheckout, startProviderCheckout, } from "../checkout-core.js";
|
|
26
27
|
import { consentCheckoutFields, consentPayloadSchema, prepareConsent, recordConsent, } from "../consumer-protection/consent-gate.js";
|
|
27
28
|
const createCheckoutSessionSchema = z.object({
|
|
28
29
|
/** Welcher Provider — entityName eines registrierten subscription-
|
|
@@ -44,7 +45,7 @@ const createCheckoutSessionSchema = z.object({
|
|
|
44
45
|
export function createCheckoutSessionHandler(options) {
|
|
45
46
|
const consumerProtection = options.consumerProtection;
|
|
46
47
|
const schema = consumerProtection
|
|
47
|
-
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
48
|
+
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema.optional() }).strict()
|
|
48
49
|
: createCheckoutSessionSchema;
|
|
49
50
|
return {
|
|
50
51
|
name: "create-checkout-session",
|
|
@@ -56,27 +57,39 @@ export function createCheckoutSessionHandler(options) {
|
|
|
56
57
|
handler: async (event, ctx) => {
|
|
57
58
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
58
59
|
const payload = event.payload;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
? await prepareConsent(ctx, consumerProtection, payload.consent, mode)
|
|
62
|
-
: undefined;
|
|
63
|
-
const result = await openCheckout(ctx, { baseUrl: options.baseUrl, catalog: options.catalog, now: options.now }, {
|
|
60
|
+
const openOptions = { baseUrl: options.baseUrl, catalog: options.catalog, now: options.now };
|
|
61
|
+
const checkoutInput = {
|
|
64
62
|
providerName: payload.providerName,
|
|
65
63
|
priceId: payload.priceId,
|
|
66
64
|
successUrl: payload.successUrl,
|
|
67
65
|
cancelUrl: payload.cancelUrl,
|
|
68
66
|
...(payload.providerCustomerId && { providerCustomerId: payload.providerCustomerId }),
|
|
69
67
|
...(payload.mode && { mode: payload.mode }),
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
price: await lookupPrice(ctx, plugin, payload.priceId),
|
|
78
|
-
});
|
|
68
|
+
};
|
|
69
|
+
if (!consumerProtection) {
|
|
70
|
+
const result = await openCheckout(ctx, openOptions, checkoutInput);
|
|
71
|
+
return {
|
|
72
|
+
isSuccess: true,
|
|
73
|
+
data: { url: result.url, providerName: result.providerName },
|
|
74
|
+
};
|
|
79
75
|
}
|
|
76
|
+
const mode = payload.mode ?? "subscription";
|
|
77
|
+
const plugin = await assertCheckoutAllowed(ctx, openOptions, checkoutInput);
|
|
78
|
+
const price = await lookupPrice(ctx, plugin, payload.priceId);
|
|
79
|
+
const orderItem = mode === "payment"
|
|
80
|
+
? await consumerProtection.oneOffItemLabel?.(ctx, payload.priceId)
|
|
81
|
+
: undefined;
|
|
82
|
+
const consent = await prepareConsent(ctx, consumerProtection, payload.consent, mode);
|
|
83
|
+
const result = await startProviderCheckout(ctx, plugin, {
|
|
84
|
+
...checkoutInput,
|
|
85
|
+
...consentCheckoutFields(consent),
|
|
86
|
+
});
|
|
87
|
+
await recordConsent(ctx, consent, {
|
|
88
|
+
tier: mode === "subscription" ? (plugin.priceToTier?.[payload.priceId] ?? null) : null,
|
|
89
|
+
priceId: payload.priceId,
|
|
90
|
+
price,
|
|
91
|
+
...(orderItem && { orderItem }),
|
|
92
|
+
});
|
|
80
93
|
return {
|
|
81
94
|
isSuccess: true,
|
|
82
95
|
data: { url: result.url, providerName: result.providerName },
|
|
@@ -84,16 +97,25 @@ export function createCheckoutSessionHandler(options) {
|
|
|
84
97
|
},
|
|
85
98
|
};
|
|
86
99
|
}
|
|
87
|
-
//
|
|
88
|
-
//
|
|
100
|
+
// The price is part of the consent record, so it must be known before the
|
|
101
|
+
// buyer is sent to the provider.
|
|
89
102
|
async function lookupPrice(ctx, plugin, priceId) {
|
|
90
|
-
|
|
91
|
-
return null;
|
|
103
|
+
let price;
|
|
92
104
|
try {
|
|
93
|
-
const prices = await plugin.retrievePrices(ctx, [priceId]);
|
|
94
|
-
|
|
105
|
+
const prices = (await plugin.retrievePrices?.(ctx, [priceId])) ?? [];
|
|
106
|
+
price = prices.find((p) => p.priceId === priceId);
|
|
95
107
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
108
|
+
catch (cause) {
|
|
109
|
+
throw priceUnavailable(priceId, cause);
|
|
98
110
|
}
|
|
111
|
+
if (!price)
|
|
112
|
+
throw priceUnavailable(priceId);
|
|
113
|
+
return price;
|
|
114
|
+
}
|
|
115
|
+
function priceUnavailable(priceId, cause) {
|
|
116
|
+
return new UnprocessableError("price_unavailable", {
|
|
117
|
+
i18nKey: "billing-foundation.errors.priceUnavailable",
|
|
118
|
+
message: `billing-foundation: no live price resolved for priceId "${priceId}"`,
|
|
119
|
+
...(cause instanceof Error && { cause }),
|
|
120
|
+
});
|
|
99
121
|
}
|
|
@@ -2,6 +2,8 @@ export { paymentAggregateId, subscriptionAggregateId } from "./aggregate-id.js";
|
|
|
2
2
|
export { type BillingInfo, type BillingInfoQueryDeps, createBillingInfoQueryConfig, } from "./billing-info-query.js";
|
|
3
3
|
export { isBillingEnabled, type ResolvedProvider, resolveProviderPlugin, } from "./checkout-core.js";
|
|
4
4
|
export { BILLING_FOUNDATION_FEATURE, BILLING_PLANS_PANEL_COMPONENT, BILLING_PLANS_SCREEN_ID, type BillingEventKind, BillingEventKinds, type BillingPlanAction, BillingPlanActions, DEFAULT_PURCHASE_ROLES, isSwitchableSubscriptionStatus, isTerminalSubscriptionStatus, parseProviderConsentId, SUBSCRIPTION_PROVIDER_EXTENSION, type SubscriptionCancelTiming, SubscriptionCancelTimings, type SubscriptionEventType, SubscriptionEventTypes, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, type SubscriptionStatus, SubscriptionStatuses, } from "./constants.js";
|
|
5
|
+
export { type ConsentPayload, consentPayloadSchema, type OrderItem, } from "./consumer-protection/consent-gate.js";
|
|
6
|
+
export { CONSENT_LOCALES, type ConsentLocale, resolveConsentLocale, } from "./consumer-protection/consent-locale.js";
|
|
5
7
|
export { type ContractTerminationRoutesOptions, createContractTerminationRoutes, } from "./consumer-protection/termination-pages.js";
|
|
6
8
|
export { paymentEntity, subscriptionEntity } from "./entities.js";
|
|
7
9
|
export { INVOICE_PAID_EVENT_QN, INVOICE_PAID_EVENT_SHORT, INVOICE_PAYMENT_FAILED_EVENT_QN, INVOICE_PAYMENT_FAILED_EVENT_SHORT, PAYMENT_AGGREGATE_TYPE, PAYMENT_RECEIVED_EVENT_QN, PAYMENT_RECEIVED_EVENT_SHORT, type PaymentEventHeaders, type PaymentEventPayload, paymentEventPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, SUBSCRIPTION_CANCELED_EVENT_QN, SUBSCRIPTION_CANCELED_EVENT_SHORT, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_SHORT, SUBSCRIPTION_UPDATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_SHORT, type SubscriptionEventHeaders, type SubscriptionEventPayload, subscriptionEventPayloadSchema, } from "./events.js";
|
|
@@ -10,5 +12,5 @@ export { getSubscriptionForTenant, type SubscriptionView } from "./get-subscript
|
|
|
10
12
|
export { paymentsProjectionTable, subscriptionsProjectionTable } from "./projection.js";
|
|
11
13
|
export { billingPlansPanel, createBillingPlansScreen } from "./screens.js";
|
|
12
14
|
export { createSubscriptionTierSync, effectiveTierFromSubscription, SUBSCRIPTION_WEBHOOK_PATH, type SubscriptionTierSyncDeps, } from "./subscription-tier-sync.js";
|
|
13
|
-
export { type BillingFoundationOptions, type BillingPlanBenefit, type BillingPlanCatalog, type BillingPlanPrice, type BillingPlansResult, type BillingPlanView, type ConsumerProtectionOptions, KNOWN_RECURRING_INTERVALS, type PaymentEvent, type ProviderPrice, type ProviderSubscriptionSnapshot, type RecurringInterval, type SubscriptionEvent, type SubscriptionProviderPlugin, } from "./types.js";
|
|
15
|
+
export { type BillingFoundationOptions, type BillingPlanBenefit, type BillingPlanCatalog, type BillingPlanPrice, type BillingPlansResult, type BillingPlanView, type ConsumerProtectionOptions, KNOWN_RECURRING_INTERVALS, type LegalLinkSet, type PaymentEvent, type ProviderPrice, type ProviderSubscriptionSnapshot, type RecurringInterval, type SubscriptionEvent, type SubscriptionProviderPlugin, } from "./types.js";
|
|
14
16
|
export { createSubscriptionWebhookRoute, type SubscriptionWebhookRouteOptions, } from "./webhook-handler.js";
|
|
@@ -3,6 +3,8 @@ export { paymentAggregateId, subscriptionAggregateId } from "./aggregate-id.js";
|
|
|
3
3
|
export { createBillingInfoQueryConfig, } from "./billing-info-query.js";
|
|
4
4
|
export { isBillingEnabled, resolveProviderPlugin, } from "./checkout-core.js";
|
|
5
5
|
export { BILLING_FOUNDATION_FEATURE, BILLING_PLANS_PANEL_COMPONENT, BILLING_PLANS_SCREEN_ID, BillingEventKinds, BillingPlanActions, DEFAULT_PURCHASE_ROLES, isSwitchableSubscriptionStatus, isTerminalSubscriptionStatus, parseProviderConsentId, SUBSCRIPTION_PROVIDER_EXTENSION, SubscriptionCancelTimings, SubscriptionEventTypes, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, SubscriptionStatuses, } from "./constants.js";
|
|
6
|
+
export { consentPayloadSchema, } from "./consumer-protection/consent-gate.js";
|
|
7
|
+
export { CONSENT_LOCALES, resolveConsentLocale, } from "./consumer-protection/consent-locale.js";
|
|
6
8
|
export { createContractTerminationRoutes, } from "./consumer-protection/termination-pages.js";
|
|
7
9
|
export { paymentEntity, subscriptionEntity } from "./entities.js";
|
|
8
10
|
export { INVOICE_PAID_EVENT_QN, INVOICE_PAID_EVENT_SHORT, INVOICE_PAYMENT_FAILED_EVENT_QN, INVOICE_PAYMENT_FAILED_EVENT_SHORT, PAYMENT_AGGREGATE_TYPE, PAYMENT_RECEIVED_EVENT_QN, PAYMENT_RECEIVED_EVENT_SHORT, paymentEventPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, SUBSCRIPTION_CANCELED_EVENT_QN, SUBSCRIPTION_CANCELED_EVENT_SHORT, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_SHORT, SUBSCRIPTION_UPDATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_SHORT, subscriptionEventPayloadSchema, } from "./events.js";
|
|
@@ -3,6 +3,8 @@ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
import type { HandlerContext, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
5
|
import { type BillingEventKinds, type BillingPlanAction, SUBSCRIPTION_PROVIDER_EXTENSION, type SubscriptionCancelTiming, type SubscriptionEventType, type SubscriptionStatus, type TerminationScope } from "./constants.js";
|
|
6
|
+
import type { OrderItem } from "./consumer-protection/consent-gate.js";
|
|
7
|
+
import type { ConsentLocale } from "./consumer-protection/consent-locale.js";
|
|
6
8
|
export type SubscriptionEvent = {
|
|
7
9
|
/** Discriminator against PaymentEvent. Optional (instead of required) so
|
|
8
10
|
* existing plugins like subscription-mollie, which don't set this field,
|
|
@@ -279,6 +281,12 @@ export type BillingPlanCatalog<TTier extends string = string> = {
|
|
|
279
281
|
/** Defaults to the only registered provider exposing `priceToTier`. */
|
|
280
282
|
readonly providerName?: string;
|
|
281
283
|
};
|
|
284
|
+
export type LegalLinkSet = {
|
|
285
|
+
readonly terms: string;
|
|
286
|
+
readonly withdrawal: string;
|
|
287
|
+
readonly privacy: string;
|
|
288
|
+
};
|
|
289
|
+
export declare function isLegalLinkSet(links: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>): links is LegalLinkSet;
|
|
282
290
|
/** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
|
|
283
291
|
* `start-plan-checkout` and `create-checkout-session` require a `consent`
|
|
284
292
|
* payload and record it as an event before the buyer reaches the provider. */
|
|
@@ -294,12 +302,13 @@ export type ConsumerProtectionOptions = {
|
|
|
294
302
|
* host that resolves none (the platform apex) — the declaration then runs
|
|
295
303
|
* without a request tenant and finds the contract by the declarant's email. */
|
|
296
304
|
readonly terminationScope?: TerminationScope;
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
/** Names the item of a one-off payment (mode "payment") for the consent record
|
|
306
|
+
* and the confirmation mail; the label key is resolved through the registry
|
|
307
|
+
* i18n, so it must be qualified with the registering feature. */
|
|
308
|
+
readonly oneOffItemLabel?: (ctx: HandlerContext, priceId: string) => Promise<OrderItem | undefined>;
|
|
309
|
+
/** Root-relative paths or absolute https URLs. One set for every locale, or
|
|
310
|
+
* one set per consent locale (`de` and `en` required). */
|
|
311
|
+
readonly legalLinks: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
303
312
|
};
|
|
304
313
|
export type BillingFoundationOptions<TTier extends string = string> = {
|
|
305
314
|
/** Absolute http(s) URL. Its origin is the only redirect origin
|
|
@@ -361,7 +370,7 @@ export type BillingPlansResult = {
|
|
|
361
370
|
readonly withdrawalLoss: string;
|
|
362
371
|
readonly consentTextVersion: string;
|
|
363
372
|
}>>;
|
|
364
|
-
readonly legalLinks:
|
|
373
|
+
readonly legalLinks: Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
365
374
|
};
|
|
366
375
|
};
|
|
367
376
|
declare module "@cosmicdrift/kumiko-framework/engine" {
|
|
@@ -30,3 +30,6 @@ import { SUBSCRIPTION_PROVIDER_EXTENSION, } from "./constants.js";
|
|
|
30
30
|
// =============================================================================
|
|
31
31
|
// Closed set a provider's recurring-price interval narrows to; an unrecognized provider value maps to null instead of widening this type.
|
|
32
32
|
export const KNOWN_RECURRING_INTERVALS = ["day", "week", "month", "year"];
|
|
33
|
+
export function isLegalLinkSet(links) {
|
|
34
|
+
return "terms" in links;
|
|
35
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BillingFoundationOptions } from "./types.js";
|
|
2
2
|
/** Validates `options` and throws a plain `Error` with a clear message at
|
|
3
3
|
* feature-definition time — same pattern as `createCapOverviewFeature`.
|
|
4
4
|
* `createBillingFoundationFeature()` (no options) always succeeds; the
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// re-exported from index.ts.
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { isTerminationScope, TERMINATION_SCOPES } from "./constants.js";
|
|
7
|
+
import { CONSENT_LOCALES } from "./consumer-protection/consent-locale.js";
|
|
8
|
+
import { isLegalLinkSet, } from "./types.js";
|
|
7
9
|
function isRootRelativePath(path) {
|
|
8
10
|
return path.startsWith("/") && !path.startsWith("//");
|
|
9
11
|
}
|
|
@@ -29,12 +31,32 @@ function validateConsumerProtection(cp) {
|
|
|
29
31
|
if (!z.email().safeParse(cp.operatorEmail).success) {
|
|
30
32
|
throw new Error(`createBillingFoundationFeature: consumerProtection.operatorEmail "${cp.operatorEmail}" is not a valid email address.`);
|
|
31
33
|
}
|
|
34
|
+
if (cp.oneOffItemLabel !== undefined && typeof cp.oneOffItemLabel !== "function") {
|
|
35
|
+
throw new Error("createBillingFoundationFeature: consumerProtection.oneOffItemLabel must be a function.");
|
|
36
|
+
}
|
|
32
37
|
if (cp.terminationScope !== undefined && !isTerminationScope(cp.terminationScope)) {
|
|
33
38
|
throw new Error(`createBillingFoundationFeature: consumerProtection.terminationScope "${String(cp.terminationScope)}" must be one of ${TERMINATION_SCOPES.map((scope) => `"${scope}"`).join(", ")}.`);
|
|
34
39
|
}
|
|
35
|
-
|
|
40
|
+
if (isLegalLinkSet(cp.legalLinks)) {
|
|
41
|
+
validateLegalLinkSet(cp.legalLinks, "legalLinks");
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
validatePerLocaleLegalLinks(cp.legalLinks);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function validatePerLocaleLegalLinks(legalLinks) {
|
|
48
|
+
for (const locale of CONSENT_LOCALES) {
|
|
49
|
+
const set = legalLinks[locale];
|
|
50
|
+
if (!set) {
|
|
51
|
+
throw new Error(`createBillingFoundationFeature: consumerProtection.legalLinks needs a "${locale}" entry when given per locale.`);
|
|
52
|
+
}
|
|
53
|
+
validateLegalLinkSet(set, `legalLinks.${locale}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function validateLegalLinkSet(set, path) {
|
|
57
|
+
for (const [key, link] of Object.entries(set)) {
|
|
36
58
|
if (!isRootRelativeOrHttpsUrl(link)) {
|
|
37
|
-
throw new Error(`createBillingFoundationFeature: consumerProtection
|
|
59
|
+
throw new Error(`createBillingFoundationFeature: consumerProtection.${path}.${key} "${link}" must be a root-relative path ("/..." not "//") or an absolute https URL.`);
|
|
38
60
|
}
|
|
39
61
|
}
|
|
40
62
|
}
|
|
@@ -189,7 +189,7 @@ export function BillingPlansPanel(_props) {
|
|
|
189
189
|
date: toInstant(result.subscription.cancelAt).toLocaleString(locale, {
|
|
190
190
|
dateStyle: "medium",
|
|
191
191
|
}),
|
|
192
|
-
}), " ", t("billing-foundation.plans.switchRequiresReactivation")] })), consumerProtection !== undefined && canManage && (_jsx("div", { children: _jsx(Button, { variant: "secondary", onClick: () => setCancelOpen(true), testId: "billing-plans-cancel-contract", children: t("billing-foundation.cancel.open") }) })), _jsxs(PlanGrid, { testId: "billing-plans-grid", children: [!currentTierInPlans && (_jsx(PlanCard, { title: t(result.currentTier.labelKey), current: true, testId: "billing-plan-card-current", features: result.currentTier.benefits.map((benefit) => t(benefit.labelKey, localizedParams(benefit.params, locale))), secondaryAction: manageOrReactivateAction(canManage, result.subscription, t, handleManage, redirecting) })), visiblePlans.map((plan) => (_jsx(PlanCard, { testId: `billing-plan-card-${plan.tier}`, ...toCardProps(plan) }, plan.tier)))] }), consumerProtection !== undefined &&
|
|
192
|
+
}), " ", t("billing-foundation.plans.switchRequiresReactivation")] })), consumerProtection !== undefined && canManage && result.subscription?.cancelAt == null && (_jsx("div", { children: _jsx(Button, { variant: "secondary", onClick: () => setCancelOpen(true), testId: "billing-plans-cancel-contract", children: t("billing-foundation.cancel.open") }) })), _jsxs(PlanGrid, { testId: "billing-plans-grid", children: [!currentTierInPlans && (_jsx(PlanCard, { title: t(result.currentTier.labelKey), current: true, testId: "billing-plan-card-current", features: result.currentTier.benefits.map((benefit) => t(benefit.labelKey, localizedParams(benefit.params, locale))), secondaryAction: manageOrReactivateAction(canManage, result.subscription, t, handleManage, redirecting) })), visiblePlans.map((plan) => (_jsx(PlanCard, { testId: `billing-plan-card-${plan.tier}`, ...toCardProps(plan) }, plan.tier)))] }), consumerProtection !== undefined &&
|
|
193
193
|
consentPlan !== undefined &&
|
|
194
194
|
consentPlan.price !== null && (_jsx(CheckoutConsentDialog, { open: true, onOpenChange: (open) => {
|
|
195
195
|
if (!open)
|
|
@@ -14,7 +14,7 @@ const TERMINATION_KINDS = ["ordinary", "extraordinary"];
|
|
|
14
14
|
export function CancelContractDialog({ open, onOpenChange, onTerminated, }) {
|
|
15
15
|
const t = useTranslation();
|
|
16
16
|
const locale = useLocale().locale();
|
|
17
|
-
const { Modal,
|
|
17
|
+
const { Modal, Text, Field, Input, Button, Banner } = usePrimitives();
|
|
18
18
|
const terminate = useMutation(SubscriptionFoundationHandlers.terminateContract);
|
|
19
19
|
const [step, setStep] = useState("form");
|
|
20
20
|
const [declarationType, setDeclarationType] = useState("termination");
|
|
@@ -38,7 +38,7 @@ export function CancelContractDialog({ open, onOpenChange, onTerminated, }) {
|
|
|
38
38
|
function formatInstant(iso) {
|
|
39
39
|
return toInstant(iso).toLocaleString(locale, { dateStyle: "medium", timeStyle: "short" });
|
|
40
40
|
}
|
|
41
|
-
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.cancel.title"), testId: "cancel-contract-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
41
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.cancel.title"), testId: "cancel-contract-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [step === "form" && (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "muted", children: t("billing-foundation.cancel.declaration") }), _jsx(ModeSwitch, { value: declarationType, options: DECLARATION_TYPES.map((value) => ({
|
|
42
42
|
value,
|
|
43
43
|
label: t(`billing-foundation.cancel.declaration.${value}`),
|
|
44
44
|
})), onChange: setDeclarationType, ariaLabel: t("billing-foundation.cancel.declaration"), testId: "cancel-declaration-type" }), declarationType === "withdrawal" && (_jsx(Text, { variant: "muted", children: t("billing-foundation.cancel.withdrawalHint") })), _jsx(Text, { variant: "muted", children: t("billing-foundation.cancel.kind") }), _jsx(ModeSwitch, { value: terminationKind, options: TERMINATION_KINDS.map((value) => ({
|
|
@@ -16,7 +16,7 @@ export type CheckoutConsentDialogProps = {
|
|
|
16
16
|
readonly price: {
|
|
17
17
|
readonly amount: string;
|
|
18
18
|
readonly renewalKey?: string;
|
|
19
|
-
readonly count
|
|
19
|
+
readonly count?: string;
|
|
20
20
|
};
|
|
21
21
|
readonly consumerProtection: ConsumerProtectionView;
|
|
22
22
|
readonly onOrder: (consent: CheckoutConsentPayload) => Promise<WriteResult<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLocale, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { resolveConsentLocale } from "../consumer-protection/consent-locale.js";
|
|
@@ -7,14 +7,14 @@ export const CONSENT_TEXT_OUTDATED_CODE = "consent_text_outdated";
|
|
|
7
7
|
export function CheckoutConsentDialog({ open, onOpenChange, planName, price, consumerProtection, onOrder, onConsentTextOutdated, }) {
|
|
8
8
|
const t = useTranslation();
|
|
9
9
|
const uiLocale = useLocale().locale();
|
|
10
|
-
const { Modal,
|
|
10
|
+
const { Modal, Text, Field, Input, Button, Link, Banner } = usePrimitives();
|
|
11
11
|
const [earlyPerformance, setEarlyPerformance] = useState(false);
|
|
12
12
|
const [withdrawalLoss, setWithdrawalLoss] = useState(false);
|
|
13
13
|
const [submitting, setSubmitting] = useState(false);
|
|
14
14
|
const [errorText, setErrorText] = useState(null);
|
|
15
15
|
const consentLocale = resolveConsentLocale(uiLocale);
|
|
16
16
|
const texts = consumerProtection.consentTexts[consentLocale];
|
|
17
|
-
const
|
|
17
|
+
const legalLinks = consumerProtection.legalLinks[consentLocale];
|
|
18
18
|
async function order() {
|
|
19
19
|
setSubmitting(true);
|
|
20
20
|
setErrorText(null);
|
|
@@ -34,5 +34,5 @@ export function CheckoutConsentDialog({ open, onOpenChange, planName, price, con
|
|
|
34
34
|
onConsentTextOutdated();
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.consent.title"), testId: "checkout-consent-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
37
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.consent.title"), testId: "checkout-consent-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1", "data-testid": "checkout-consent-summary", children: [_jsxs(Text, { children: [planName, " \u2014 ", price.amount] }), price.renewalKey !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "muted", children: t(price.renewalKey, price.count !== undefined ? { count: price.count } : undefined) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.consent.cancelAnytime") })] }))] }), _jsxs("div", { className: "flex flex-wrap gap-4", children: [_jsx(Link, { href: legalLinks.terms, target: "_blank", children: t("billing-foundation.consent.link.terms") }), _jsx(Link, { href: legalLinks.withdrawal, target: "_blank", children: t("billing-foundation.consent.link.withdrawal") }), _jsx(Link, { href: legalLinks.privacy, target: "_blank", children: t("billing-foundation.consent.link.privacy") })] }), _jsx(Field, { id: "consent-early-performance", label: texts.earlyPerformance, layout: "inline", children: _jsx(Input, { kind: "boolean", id: "consent-early-performance", name: "consent-early-performance", value: earlyPerformance, onChange: setEarlyPerformance, disabled: submitting }) }), _jsx(Field, { id: "consent-withdrawal-loss", label: texts.withdrawalLoss, layout: "inline", children: _jsx(Input, { kind: "boolean", id: "consent-withdrawal-loss", name: "consent-withdrawal-loss", value: withdrawalLoss, onChange: setWithdrawalLoss, disabled: submitting }) }), errorText !== null && (_jsx(Banner, { variant: "error", testId: "checkout-consent-error", children: errorText })), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "secondary", disabled: submitting, onClick: () => onOpenChange(false), testId: "checkout-consent-cancel", children: t("billing-foundation.consent.back") }), _jsx(Button, { disabled: !earlyPerformance || !withdrawalLoss || submitting, loading: submitting, onClick: order, testId: "checkout-consent-order", children: t("billing-foundation.consent.order") })] })] }) }));
|
|
38
38
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { BILLING_PLANS_SCREEN_ID, type BillingPlanAction, BillingPlanActions, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, type SubscriptionStatus, SubscriptionStatuses, } from "../constants.js";
|
|
2
|
-
export
|
|
2
|
+
export { CONSENT_LOCALES, type ConsentLocale, resolveConsentLocale, } from "../consumer-protection/consent-locale.js";
|
|
3
|
+
export type { BillingPlanBenefit, BillingPlanCatalog, BillingPlansResult, BillingPlanView, LegalLinkSet, } from "../types.js";
|
|
3
4
|
export { BillingPlansPanel } from "./billing-plans-panel.js";
|
|
5
|
+
export { CheckoutConsentDialog, type CheckoutConsentDialogProps, type CheckoutConsentPayload, CONSENT_TEXT_OUTDATED_CODE, } from "./checkout-consent-dialog.js";
|
|
4
6
|
export { type BillingFoundationClientOptions, billingFoundationClient } from "./client-plugin.js";
|
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
// the server side (createBillingFoundationFeature) lives under
|
|
5
5
|
// `@cosmicdrift/kumiko-bundled-features/billing-foundation` and has no React deps.
|
|
6
6
|
export { BILLING_PLANS_SCREEN_ID, BillingPlanActions, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, SubscriptionStatuses, } from "../constants.js";
|
|
7
|
+
export { CONSENT_LOCALES, resolveConsentLocale, } from "../consumer-protection/consent-locale.js";
|
|
7
8
|
export { BillingPlansPanel } from "./billing-plans-panel.js";
|
|
9
|
+
export { CheckoutConsentDialog, CONSENT_TEXT_OUTDATED_CODE, } from "./checkout-consent-dialog.js";
|
|
8
10
|
export { billingFoundationClient } from "./client-plugin.js";
|
|
@@ -27,6 +27,7 @@ export function createChannelDiscordFeature(options = {}) {
|
|
|
27
27
|
valueSchema: chatWebhookUrlSchema(webhookTarget),
|
|
28
28
|
});
|
|
29
29
|
const channel = createDiscordChannel(options, webhooks.keyFor);
|
|
30
|
+
r.translations({ keys: { "delivery.channel.discord": { en: "Discord" } } });
|
|
30
31
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
|
|
31
32
|
mode: channel.mode,
|
|
32
33
|
send: channel.send,
|
|
@@ -11,6 +11,7 @@ export function createChannelEmailFeature(options) {
|
|
|
11
11
|
recommended: false,
|
|
12
12
|
});
|
|
13
13
|
r.requires("delivery");
|
|
14
|
+
r.translations({ keys: { "delivery.channel.email": { en: "Email" } } });
|
|
14
15
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "email", {
|
|
15
16
|
mode: channel.mode,
|
|
16
17
|
resolve: channel.resolve,
|
|
@@ -19,6 +19,7 @@ export function createChannelInAppFeature() {
|
|
|
19
19
|
reason: "read_side.in_app_messages",
|
|
20
20
|
});
|
|
21
21
|
// Register as delivery channel via extension system
|
|
22
|
+
r.translations({ keys: { "delivery.channel.inApp": { en: "In-app" } } });
|
|
22
23
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "inApp", {
|
|
23
24
|
mode: inAppChannel.mode,
|
|
24
25
|
resolve: inAppChannel.resolve,
|
|
@@ -11,6 +11,7 @@ export function createChannelPushFeature(options) {
|
|
|
11
11
|
recommended: false,
|
|
12
12
|
});
|
|
13
13
|
r.requires("delivery");
|
|
14
|
+
r.translations({ keys: { "delivery.channel.push": { en: "Push" } } });
|
|
14
15
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "push", {
|
|
15
16
|
mode: channel.mode,
|
|
16
17
|
resolve: channel.resolve,
|
|
@@ -24,6 +24,7 @@ export function createChannelSlackFeature(options = {}) {
|
|
|
24
24
|
valueSchema: chatWebhookUrlSchema(webhookTarget),
|
|
25
25
|
});
|
|
26
26
|
const channel = createSlackChannel(options, webhooks.keyFor);
|
|
27
|
+
r.translations({ keys: { "delivery.channel.slack": { en: "Slack" } } });
|
|
27
28
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
|
|
28
29
|
mode: channel.mode,
|
|
29
30
|
send: channel.send,
|
|
@@ -28,6 +28,7 @@ export function createChannelTeamsFeature(options = {}) {
|
|
|
28
28
|
valueSchema: chatWebhookUrlSchema(webhookTarget),
|
|
29
29
|
});
|
|
30
30
|
const channel = createTeamsChannel(options, webhooks.keyFor);
|
|
31
|
+
r.translations({ keys: { "delivery.channel.teams": { en: "Microsoft Teams" } } });
|
|
31
32
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
|
|
32
33
|
mode: channel.mode,
|
|
33
34
|
send: channel.send,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
|
|
1
|
+
import { type ChatSendResult, type ChatWebhookChannelOptions, type ChatWebhookResponse, type DeliveryChannel } from "../delivery/index.js";
|
|
2
2
|
export type TeamsChannelOptions = ChatWebhookChannelOptions;
|
|
3
3
|
export declare const TEAMS_SECRET_KEYS: {
|
|
4
4
|
readonly webhookPrefix: "channel-teams:webhooks.";
|
|
@@ -6,4 +6,5 @@ export declare const TEAMS_SECRET_KEYS: {
|
|
|
6
6
|
};
|
|
7
7
|
export declare const TEAMS_DEFAULT_ALLOWED_HOSTS: readonly [".webhook.office.com", ".logic.azure.com", ".powerplatform.com"];
|
|
8
8
|
export declare function buildTeamsAdaptiveCard(title: string, body: string | undefined): unknown;
|
|
9
|
+
export declare function classifyTeamsResponse(response: ChatWebhookResponse): Promise<ChatSendResult>;
|
|
9
10
|
export declare function createTeamsChannel(options: TeamsChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
|
|
@@ -30,12 +30,24 @@ export function buildTeamsAdaptiveCard(title, body) {
|
|
|
30
30
|
],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
// Classic Office connectors answer 200 with the body "1". Workflow endpoints answer
|
|
34
|
+
// 202 and never say whether the card reached the channel. Any other 2xx (an empty
|
|
35
|
+
// 200 is what a made-up URL returns) is not a Teams success.
|
|
36
|
+
export async function classifyTeamsResponse(response) {
|
|
37
|
+
if (response.status === 202)
|
|
38
|
+
return { ok: true, confirmed: false };
|
|
39
|
+
if (response.status === 200 && (await response.readBodyPrefix()).trim() === "1") {
|
|
40
|
+
return { ok: true, confirmed: true };
|
|
41
|
+
}
|
|
42
|
+
return { ok: false, code: "unexpected_response" };
|
|
43
|
+
}
|
|
33
44
|
export function createTeamsChannel(options, keyFor) {
|
|
34
45
|
return createChatWebhookChannel({
|
|
35
46
|
name: "teams",
|
|
36
47
|
featureName: "channel-teams",
|
|
37
48
|
defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS,
|
|
38
49
|
keyFor,
|
|
50
|
+
classifyResponse: classifyTeamsResponse,
|
|
39
51
|
buildBody: (message) => buildTeamsAdaptiveCard(message.title, message.body),
|
|
40
52
|
options,
|
|
41
53
|
});
|
|
@@ -22,6 +22,7 @@ export function createChannelTelegramFeature(options = {}) {
|
|
|
22
22
|
valueSchema: z.string().refine(isTelegramBotToken, "invalid bot token"),
|
|
23
23
|
});
|
|
24
24
|
const channel = createTelegramChannel(options, botToken.name);
|
|
25
|
+
r.translations({ keys: { "delivery.channel.telegram": { en: "Telegram" } } });
|
|
25
26
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "telegram", {
|
|
26
27
|
mode: channel.mode,
|
|
27
28
|
send: channel.send,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChatSendResult, type ChatWebhookTarget } from "./chat-webhook-sender.js";
|
|
1
|
+
import { type ChatSendResult, type ChatWebhookRequest, type ChatWebhookTarget } from "./chat-webhook-sender.js";
|
|
2
2
|
import type { ChannelMessage, ChannelResult, DeliveryChannel } from "./types.js";
|
|
3
3
|
export type ChatWebhookChannelOptions = {
|
|
4
4
|
readonly allowedHosts?: readonly string[];
|
|
@@ -18,6 +18,7 @@ export type ChatWebhookChannelSpec = {
|
|
|
18
18
|
readonly requiredPathPrefix?: string;
|
|
19
19
|
readonly keyFor: (connection: string) => string;
|
|
20
20
|
readonly buildBody: (message: ChannelMessage) => unknown;
|
|
21
|
+
readonly classifyResponse?: ChatWebhookRequest["classifyResponse"];
|
|
21
22
|
readonly options: ChatWebhookChannelOptions;
|
|
22
23
|
};
|
|
23
24
|
export declare function createChatWebhookChannel(spec: ChatWebhookChannelSpec): DeliveryChannel;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
2
2
|
import { chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, } from "./chat-webhook-sender.js";
|
|
3
3
|
export function toChannelResult(address, result) {
|
|
4
|
-
|
|
4
|
+
if (!result.ok)
|
|
5
|
+
return { status: "failed", error: result.code, address };
|
|
6
|
+
return result.confirmed
|
|
5
7
|
? { status: "sent", address }
|
|
6
|
-
: { status: "
|
|
8
|
+
: { status: "sent", confirmed: false, address };
|
|
7
9
|
}
|
|
8
10
|
export function chatMessageText(message) {
|
|
9
11
|
return message.body ? `${message.title}\n${message.body}` : message.title;
|
|
@@ -46,6 +48,7 @@ export function createChatWebhookChannel(spec) {
|
|
|
46
48
|
...resolveChatWebhookTarget(spec, options),
|
|
47
49
|
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
48
50
|
body: spec.buildBody(message),
|
|
51
|
+
...(spec.classifyResponse && { classifyResponse: spec.classifyResponse }),
|
|
49
52
|
});
|
|
50
53
|
return toChannelResult(address, result);
|
|
51
54
|
},
|