@cosmicdrift/kumiko-bundled-features 0.207.0 → 0.208.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/package.json +9 -8
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +1 -1
- package/src/admin-shell/i18n.ts +17 -39
- package/src/admin-shell/web/i18n.ts +6 -11
- package/src/audit/i18n.ts +25 -41
- package/src/audit/web/i18n.ts +5 -9
- package/src/auth-email-password/__tests__/email-templates.test.ts +4 -0
- package/src/auth-email-password/email-templates.ts +97 -149
- package/src/auth-email-password/i18n.ts +6 -265
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +7 -7
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +14 -14
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +23 -25
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +60 -68
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +24 -24
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +20 -20
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +9 -9
- package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +11 -11
- package/src/auth-email-password/web/__tests__/test-utils.tsx +2 -2
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +3 -3
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +3 -3
- package/src/auth-email-password/web/invite-accept-screen.tsx +3 -1
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +2 -4
- package/src/auth-mfa/i18n.ts +1 -3
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +15 -15
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +45 -49
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +10 -10
- package/src/auth-mfa/web/i18n.ts +0 -126
- package/src/cap-counter/i18n.ts +5 -11
- package/src/compliance-profiles/i18n.ts +3 -11
- package/src/compliance-profiles/web/i18n.ts +0 -20
- package/src/config/i18n.ts +5 -5
- package/src/config/web/i18n.ts +1 -19
- package/src/custom-fields/web/i18n.ts +1 -18
- package/src/delivery/i18n.ts +3 -7
- package/src/delivery/web/i18n.ts +0 -20
- package/src/feature-toggles/i18n.ts +3 -11
- package/src/feature-toggles/web/i18n.ts +0 -24
- package/src/folders/web/i18n.ts +1 -51
- package/src/jobs/i18n.ts +40 -68
- package/src/jobs/web/i18n.ts +5 -9
- package/src/managed-pages/i18n.ts +23 -67
- package/src/managed-pages/web/i18n.ts +6 -11
- package/src/notes-history/i18n.ts +1 -1
- package/src/notes-history/web/i18n.ts +1 -17
- package/src/personal-access-tokens/i18n.ts +2 -6
- package/src/personal-access-tokens/web/i18n.ts +0 -72
- package/src/sessions/i18n.ts +17 -21
- package/src/tags/i18n.ts +2 -2
- package/src/tags/web/i18n.ts +1 -52
- package/src/template-resolver/web/__tests__/client-plugin.test.tsx +2 -2
- package/src/template-resolver/web/__tests__/editor-read-only.test.tsx +5 -5
- package/src/template-resolver/web/i18n.ts +0 -28
- package/src/tenant/__tests__/members-screens.boot.test.ts +0 -2
- package/src/tenant/i18n.ts +13 -27
- package/src/tenant/web/i18n.ts +0 -38
- package/src/tier-engine/i18n.ts +3 -39
- package/src/user/i18n.ts +14 -32
- package/src/user-data-rights/__tests__/default-mailers.test.ts +4 -0
- package/src/user-data-rights/__tests__/email-templates.test.ts +4 -0
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +4 -0
- package/src/user-data-rights/email-templates.ts +55 -91
- package/src/user-data-rights/i18n.ts +18 -70
- package/src/user-data-rights/lib/default-mailers.ts +4 -1
- package/src/user-data-rights/web/__tests__/deletion-screens.test.tsx +11 -11
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +5 -5
- package/src/user-data-rights/web/i18n.ts +9 -157
- package/src/user-profile/i18n.ts +1 -71
package/src/tier-engine/i18n.ts
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Default-Bundles für den TierAdminScreen. Werden vom tierEngineClient()
|
|
3
3
|
// als Fallback-Bundle in den LocaleProvider gehängt — Apps überschreiben
|
|
4
|
-
// einzelne Keys via `tierEngineClient({ translations
|
|
4
|
+
// einzelne Keys via `tierEngineClient({ translations })`.
|
|
5
5
|
|
|
6
6
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
7
7
|
|
|
8
|
-
type LocalizedString = { readonly
|
|
8
|
+
type LocalizedString = { readonly en: string };
|
|
9
9
|
|
|
10
10
|
export const TIER_ENGINE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
11
|
-
"screen:tier-admin.title": {
|
|
12
|
-
de: "Tier manuell zuweisen",
|
|
13
|
-
en: "Assign tier manually",
|
|
14
|
-
es: "Asignar tier manualmente",
|
|
15
|
-
},
|
|
11
|
+
"screen:tier-admin.title": { en: "Assign tier manually" },
|
|
16
12
|
};
|
|
17
13
|
|
|
18
14
|
export const defaultTranslations: TranslationsByLocale = {
|
|
19
|
-
de: {
|
|
20
|
-
"screen:tier-admin.title": "Tier manuell zuweisen",
|
|
21
|
-
"tier-admin.title": "Tier manuell zuweisen",
|
|
22
|
-
"tier-admin.explainer":
|
|
23
|
-
"Weise einem Tenant ein Tier ohne Kauf zu. Der Grant wird als „manuell“ markiert und von einem späteren Billing-Sync nicht überschrieben.",
|
|
24
|
-
"tier-admin.tenant.label": "Tenant",
|
|
25
|
-
"tier-admin.current.label": "Aktuelles Tier",
|
|
26
|
-
"tier-admin.current.none": "— noch keins —",
|
|
27
|
-
"tier-admin.tier.label": "Neues Tier",
|
|
28
|
-
"tier-admin.submit": "Tier zuweisen",
|
|
29
|
-
"tier-admin.success": "Tier „{tier}“ zugewiesen.",
|
|
30
|
-
"tier-admin.error.generic": "Konnte das Tier nicht zuweisen.",
|
|
31
|
-
"tier-admin.error.load": "Tenants konnten nicht geladen werden.",
|
|
32
|
-
"tier-admin.error.noTiers":
|
|
33
|
-
"Diese App hat keine TierMap konfiguriert — es gibt keine zuweisbaren Tiers.",
|
|
34
|
-
},
|
|
35
15
|
en: {
|
|
36
16
|
"screen:tier-admin.title": "Assign tier manually",
|
|
37
17
|
"tier-admin.title": "Assign tier manually",
|
|
@@ -48,20 +28,4 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
48
28
|
"tier-admin.error.noTiers":
|
|
49
29
|
"This app has no TierMap configured — there are no assignable tiers.",
|
|
50
30
|
},
|
|
51
|
-
es: {
|
|
52
|
-
"screen:tier-admin.title": "Asignar tier manualmente",
|
|
53
|
-
"tier-admin.title": "Asignar tier manualmente",
|
|
54
|
-
"tier-admin.explainer":
|
|
55
|
-
"Asigna un tier a un tenant sin necesidad de compra. La asignación se marca como «manual» y no será sobrescrita por una sincronización de facturación posterior.",
|
|
56
|
-
"tier-admin.tenant.label": "Tenant",
|
|
57
|
-
"tier-admin.current.label": "Tier actual",
|
|
58
|
-
"tier-admin.current.none": "— aún ninguno —",
|
|
59
|
-
"tier-admin.tier.label": "Nuevo tier",
|
|
60
|
-
"tier-admin.submit": "Asignar tier",
|
|
61
|
-
"tier-admin.success": "Tier «{tier}» asignado.",
|
|
62
|
-
"tier-admin.error.generic": "No se pudo asignar el tier.",
|
|
63
|
-
"tier-admin.error.load": "No se pudieron cargar los tenants.",
|
|
64
|
-
"tier-admin.error.noTiers":
|
|
65
|
-
"Esta aplicación no tiene una TierMap configurada — no hay tiers asignables.",
|
|
66
|
-
},
|
|
67
31
|
};
|
package/src/user/i18n.ts
CHANGED
|
@@ -1,38 +1,20 @@
|
|
|
1
|
-
type LocalizedString = { readonly
|
|
1
|
+
type LocalizedString = { readonly en: string };
|
|
2
2
|
|
|
3
3
|
export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
-
"screen:user-list.title": {
|
|
5
|
-
"screen:user-edit.title": {
|
|
6
|
-
"user:entity:user:field:email": {
|
|
7
|
-
"user:entity:user:field:displayName": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
"user:entity:user:field:status": {
|
|
13
|
-
"user:entity:user:field:
|
|
14
|
-
de: "E-Mail bestätigt",
|
|
15
|
-
en: "Email verified",
|
|
16
|
-
es: "Correo verificado",
|
|
17
|
-
},
|
|
18
|
-
"user:entity:user:field:locale": { de: "Sprache", en: "Locale", es: "Idioma" },
|
|
19
|
-
"user:entity:user:field:timezone": { de: "Zeitzone", en: "Timezone", es: "Zona horaria" },
|
|
20
|
-
"user:entity:user:field:status:option:active": { de: "Aktiv", en: "Active", es: "Activo" },
|
|
21
|
-
"user:entity:user:field:status:option:restricted": {
|
|
22
|
-
de: "Eingeschränkt",
|
|
23
|
-
en: "Restricted",
|
|
24
|
-
es: "Restringido",
|
|
25
|
-
},
|
|
4
|
+
"screen:user-list.title": { en: "Users" },
|
|
5
|
+
"screen:user-edit.title": { en: "Edit user" },
|
|
6
|
+
"user:entity:user:field:email": { en: "Email" },
|
|
7
|
+
"user:entity:user:field:displayName": { en: "Display name" },
|
|
8
|
+
"user:entity:user:field:status": { en: "Status" },
|
|
9
|
+
"user:entity:user:field:emailVerified": { en: "Email verified" },
|
|
10
|
+
"user:entity:user:field:locale": { en: "Locale" },
|
|
11
|
+
"user:entity:user:field:timezone": { en: "Timezone" },
|
|
12
|
+
"user:entity:user:field:status:option:active": { en: "Active" },
|
|
13
|
+
"user:entity:user:field:status:option:restricted": { en: "Restricted" },
|
|
26
14
|
"user:entity:user:field:status:option:deletionRequested": {
|
|
27
|
-
de: "Löschung angefordert",
|
|
28
15
|
en: "Deletion requested",
|
|
29
|
-
es: "Eliminación solicitada",
|
|
30
|
-
},
|
|
31
|
-
"user:entity:user:field:status:option:deleted": {
|
|
32
|
-
de: "Gelöscht",
|
|
33
|
-
en: "Deleted",
|
|
34
|
-
es: "Eliminado",
|
|
35
16
|
},
|
|
36
|
-
"user:entity:user:field:
|
|
37
|
-
"user:entity:user:field:emailVerified:option:
|
|
17
|
+
"user:entity:user:field:status:option:deleted": { en: "Deleted" },
|
|
18
|
+
"user:entity:user:field:emailVerified:option:true": { en: "Yes" },
|
|
19
|
+
"user:entity:user:field:emailVerified:option:false": { en: "No" },
|
|
38
20
|
};
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
EmailTransport,
|
|
11
11
|
} from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
12
12
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
14
|
+
import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
|
|
13
15
|
import {
|
|
14
16
|
makeDefaultDeletionExecutedEmail,
|
|
15
17
|
makeDefaultExportReadyEmail,
|
|
@@ -27,6 +29,8 @@ function capturingTransport(): { transport: EmailTransport; sent: EmailMessage[]
|
|
|
27
29
|
|
|
28
30
|
const TENANT_A = "00000000-0000-4000-8000-00000000000a" as TenantId;
|
|
29
31
|
|
|
32
|
+
registerMailTranslations("de", localeDeBundle);
|
|
33
|
+
|
|
30
34
|
describe("default-mailers dispatch", () => {
|
|
31
35
|
test("export-ready: rendert Template + sendet an userEmail ueber tenant-transport", async () => {
|
|
32
36
|
const cap = capturingTransport();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
3
|
+
import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
|
|
2
4
|
import {
|
|
3
5
|
renderDeletionExecutedEmail,
|
|
4
6
|
renderDeletionRequestedEmail,
|
|
@@ -6,6 +8,8 @@ import {
|
|
|
6
8
|
renderExportReadyEmail,
|
|
7
9
|
} from "../email-templates";
|
|
8
10
|
|
|
11
|
+
registerMailTranslations("de", localeDeBundle);
|
|
12
|
+
|
|
9
13
|
describe("gdpr email-templates", () => {
|
|
10
14
|
test("export-ready: subject + download-button + formatted expiry, de/en differ", () => {
|
|
11
15
|
const en = renderExportReadyEmail({
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
22
22
|
import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
23
23
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
24
|
+
import { localeDe } from "@cosmicdrift/kumiko-locale-de";
|
|
24
25
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
25
26
|
import { configValuesTable, createConfigFeature, createConfigResolver } from "../../config";
|
|
26
27
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
|
|
@@ -63,6 +64,7 @@ beforeAll(async () => {
|
|
|
63
64
|
mailTransportInMemoryFeature,
|
|
64
65
|
// KEINE send*Email-Opts — die mail-foundation-Defaults muessen greifen.
|
|
65
66
|
createUserDataRightsFeature(),
|
|
67
|
+
localeDe(),
|
|
66
68
|
createUserDataRightsDefaultsFeature(),
|
|
67
69
|
],
|
|
68
70
|
});
|
|
@@ -14,6 +14,7 @@ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth
|
|
|
14
14
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
15
15
|
import { type JobContext, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
16
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
17
|
+
import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
17
18
|
import {
|
|
18
19
|
setupTestStack,
|
|
19
20
|
type TestStack,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
24
|
import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
24
25
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
26
|
+
import { localeDeBundle } from "@cosmicdrift/kumiko-locale-de";
|
|
25
27
|
import {
|
|
26
28
|
createComplianceProfilesFeature,
|
|
27
29
|
tenantComplianceProfileEntity,
|
|
@@ -134,6 +136,8 @@ async function seedPendingJob(): Promise<string> {
|
|
|
134
136
|
return res.jobId;
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
registerMailTranslations("de", localeDeBundle);
|
|
140
|
+
|
|
137
141
|
describe("run-export-jobs cron-context", () => {
|
|
138
142
|
test("Cron-Job-Kontext (configResolver, KEIN config) → Export läuft durch, bytesWritten > 0", async () => {
|
|
139
143
|
const jobId = await seedPendingJob();
|
|
@@ -1,34 +1,46 @@
|
|
|
1
|
-
// Default
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// rendert + versendet ueber diese Templates (siehe lib/default-mailers.ts).
|
|
6
|
-
//
|
|
7
|
-
// Apps die ihr eigenes Branding wollen, uebergeben eigene send*Email-Opts —
|
|
8
|
-
// dann greifen diese Defaults nicht. Pattern + plain-inline-HTML gespiegelt
|
|
9
|
-
// von auth-email-password/email-templates.ts (kein CSS-Framework, kein
|
|
10
|
-
// Bild-Asset; Mail-Clients rendern table-layout + inline-CSS verlaesslich).
|
|
11
|
-
//
|
|
12
|
-
// Locale: de + en. Apps mit anderen Sprachen uebergeben eigene Callbacks.
|
|
1
|
+
// Default HTML renderers for GDPR notification mails. English ships here;
|
|
2
|
+
// other languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
|
|
3
|
+
// localeEs() call registerMailTranslations). Apps that want their own
|
|
4
|
+
// branding pass send*Email callbacks and these defaults do not run.
|
|
13
5
|
|
|
6
|
+
import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
14
7
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
15
8
|
import { Temporal } from "temporal-polyfill";
|
|
16
9
|
|
|
17
|
-
export type GdprMailLocale =
|
|
10
|
+
export type GdprMailLocale = string;
|
|
18
11
|
|
|
19
|
-
// user.locale ist Freitext ("de", "en", "de-DE", "fr", null). Die Templates
|
|
20
|
-
// koennen nur de/en — alles andere (inkl. unbekannte Sprachen) faellt auf
|
|
21
|
-
// undefined zurueck, sodass der Caller auf mailDefaults.locale bzw. "en" geht.
|
|
22
12
|
export function normalizeGdprMailLocale(
|
|
23
13
|
raw: string | null | undefined,
|
|
24
14
|
): GdprMailLocale | undefined {
|
|
25
15
|
if (!raw) return undefined;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (lower.startsWith("en")) return "en";
|
|
29
|
-
return undefined;
|
|
16
|
+
const root = raw.toLowerCase().split("-")[0];
|
|
17
|
+
return root && root.length > 0 ? root : undefined;
|
|
30
18
|
}
|
|
31
19
|
|
|
20
|
+
const GDPR_MAIL_EN: Readonly<Record<string, string>> = {
|
|
21
|
+
"gdpr.mail.appNameDefault": "Account",
|
|
22
|
+
"gdpr.mail.greeting": "Hi,",
|
|
23
|
+
"gdpr.mail.exportReady.subject": "{app} — Your data export is ready",
|
|
24
|
+
"gdpr.mail.exportReady.intro":
|
|
25
|
+
"your requested data export for {app} is ready. Download it using the link below:",
|
|
26
|
+
"gdpr.mail.exportReady.button": "Download data export",
|
|
27
|
+
"gdpr.mail.exportReady.expiry": "The download link expires on {when}.",
|
|
28
|
+
"gdpr.mail.exportFailed.subject": "{app} — Your data export failed",
|
|
29
|
+
"gdpr.mail.exportFailed.intro":
|
|
30
|
+
"your requested data export for {app} could not be created. Please request the export again.",
|
|
31
|
+
"gdpr.mail.deletionRequested.subject": "{app} — Account deletion requested",
|
|
32
|
+
"gdpr.mail.deletionRequested.intro":
|
|
33
|
+
"we received your request to delete your {app} account. Your account and associated data will be permanently deleted on {when}.",
|
|
34
|
+
"gdpr.mail.deletionRequested.cancel":
|
|
35
|
+
"If you didn't request this, sign in and cancel the deletion in your account settings before the deadline.",
|
|
36
|
+
"gdpr.mail.deletionExecuted.subject": "{app} — Your account has been deleted",
|
|
37
|
+
"gdpr.mail.deletionExecuted.intro":
|
|
38
|
+
"your {app} account and the associated personal data were deleted on {when}. This action is permanent.",
|
|
39
|
+
"gdpr.mail.fallbackUrl": "If the button doesn't work, copy this link into your browser:",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
registerMailTranslations("en", GDPR_MAIL_EN);
|
|
43
|
+
|
|
32
44
|
export type RenderedEmail = {
|
|
33
45
|
readonly subject: string;
|
|
34
46
|
readonly html: string;
|
|
@@ -58,76 +70,35 @@ export type RenderDeletionExecutedEmailArgs = {
|
|
|
58
70
|
readonly appName?: string;
|
|
59
71
|
};
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
exportReadySubject: (app: string) => `${app} — Dein Datenexport ist bereit`,
|
|
65
|
-
exportReadyIntro: (app: string) =>
|
|
66
|
-
`dein angeforderter Datenexport fuer ${app} ist fertig. Lade ihn ueber den folgenden Link herunter:`,
|
|
67
|
-
exportReadyButton: "Datenexport herunterladen",
|
|
68
|
-
exportReadyExpiry: (when: string) => `Der Download-Link laeuft am ${when} ab.`,
|
|
69
|
-
exportFailedSubject: (app: string) => `${app} — Dein Datenexport ist fehlgeschlagen`,
|
|
70
|
-
exportFailedIntro: (app: string) =>
|
|
71
|
-
`dein angeforderter Datenexport fuer ${app} konnte leider nicht erstellt werden. Bitte fordere den Export erneut an.`,
|
|
72
|
-
deletionRequestedSubject: (app: string) => `${app} — Loeschung deines Kontos angefordert`,
|
|
73
|
-
deletionRequestedIntro: (app: string, when: string) =>
|
|
74
|
-
`wir haben deinen Antrag zur Loeschung deines ${app}-Kontos erhalten. Dein Konto und die zugehoerigen Daten werden am ${when} endgueltig geloescht.`,
|
|
75
|
-
deletionRequestedCancel:
|
|
76
|
-
"Falls du das nicht angefordert hast, melde dich an und brich die Loeschung in den Kontoeinstellungen ab, bevor die Frist ablaeuft.",
|
|
77
|
-
deletionExecutedSubject: (app: string) => `${app} — Dein Konto wurde geloescht`,
|
|
78
|
-
deletionExecutedIntro: (app: string, when: string) =>
|
|
79
|
-
`dein ${app}-Konto und die zugehoerigen personenbezogenen Daten wurden am ${when} geloescht. Diese Aktion ist endgueltig.`,
|
|
80
|
-
fallbackUrl: "Falls der Button nicht funktioniert, kopiere diesen Link in den Browser:",
|
|
81
|
-
},
|
|
82
|
-
en: {
|
|
83
|
-
greeting: "Hi,",
|
|
84
|
-
exportReadySubject: (app: string) => `${app} — Your data export is ready`,
|
|
85
|
-
exportReadyIntro: (app: string) =>
|
|
86
|
-
`your requested data export for ${app} is ready. Download it using the link below:`,
|
|
87
|
-
exportReadyButton: "Download data export",
|
|
88
|
-
exportReadyExpiry: (when: string) => `The download link expires on ${when}.`,
|
|
89
|
-
exportFailedSubject: (app: string) => `${app} — Your data export failed`,
|
|
90
|
-
exportFailedIntro: (app: string) =>
|
|
91
|
-
`your requested data export for ${app} could not be created. Please request the export again.`,
|
|
92
|
-
deletionRequestedSubject: (app: string) => `${app} — Account deletion requested`,
|
|
93
|
-
deletionRequestedIntro: (app: string, when: string) =>
|
|
94
|
-
`we received your request to delete your ${app} account. Your account and associated data will be permanently deleted on ${when}.`,
|
|
95
|
-
deletionRequestedCancel:
|
|
96
|
-
"If you didn't request this, sign in and cancel the deletion in your account settings before the deadline.",
|
|
97
|
-
deletionExecutedSubject: (app: string) => `${app} — Your account has been deleted`,
|
|
98
|
-
deletionExecutedIntro: (app: string, when: string) =>
|
|
99
|
-
`your ${app} account and the associated personal data were deleted on ${when}. This action is permanent.`,
|
|
100
|
-
fallbackUrl: "If the button doesn't work, copy this link into your browser:",
|
|
101
|
-
},
|
|
102
|
-
} as const;
|
|
73
|
+
function t(locale: string, key: string, params?: Readonly<Record<string, string>>): string {
|
|
74
|
+
return mailT(locale, key, params);
|
|
75
|
+
}
|
|
103
76
|
|
|
104
77
|
function appNameFor(args: { locale?: GdprMailLocale; appName?: string }): string {
|
|
105
78
|
const locale = args.locale ?? "en";
|
|
106
|
-
return args.appName ?? (locale
|
|
79
|
+
return args.appName ?? t(locale, "gdpr.mail.appNameDefault");
|
|
107
80
|
}
|
|
108
81
|
|
|
109
82
|
export function renderExportReadyEmail(args: RenderExportReadyEmailArgs): RenderedEmail {
|
|
110
83
|
const locale = args.locale ?? "en";
|
|
111
84
|
const app = appNameFor(args);
|
|
112
|
-
const
|
|
113
|
-
const subject = t.exportReadySubject(app);
|
|
85
|
+
const subject = t(locale, "gdpr.mail.exportReady.subject", { app });
|
|
114
86
|
const body = `
|
|
115
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t.greeting)}</p>
|
|
116
|
-
<p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t.
|
|
117
|
-
<p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t.
|
|
118
|
-
<p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t.
|
|
119
|
-
${renderFallbackUrl({ url: args.downloadUrl, label: t.fallbackUrl })}`;
|
|
87
|
+
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
88
|
+
<p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportReady.intro", { app }))}</p>
|
|
89
|
+
<p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t(locale, "gdpr.mail.exportReady.button") })}</p>
|
|
90
|
+
<p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }))}</p>
|
|
91
|
+
${renderFallbackUrl({ url: args.downloadUrl, label: t(locale, "gdpr.mail.fallbackUrl") })}`;
|
|
120
92
|
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }) };
|
|
121
93
|
}
|
|
122
94
|
|
|
123
95
|
export function renderExportFailedEmail(args: RenderExportFailedEmailArgs): RenderedEmail {
|
|
124
96
|
const locale = args.locale ?? "en";
|
|
125
97
|
const app = appNameFor(args);
|
|
126
|
-
const
|
|
127
|
-
const subject = t.exportFailedSubject(app);
|
|
98
|
+
const subject = t(locale, "gdpr.mail.exportFailed.subject", { app });
|
|
128
99
|
const body = `
|
|
129
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t.greeting)}</p>
|
|
130
|
-
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t.
|
|
100
|
+
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
101
|
+
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportFailed.intro", { app }))}</p>`;
|
|
131
102
|
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }) };
|
|
132
103
|
}
|
|
133
104
|
|
|
@@ -136,23 +107,23 @@ export function renderDeletionRequestedEmail(
|
|
|
136
107
|
): RenderedEmail {
|
|
137
108
|
const locale = args.locale ?? "en";
|
|
138
109
|
const app = appNameFor(args);
|
|
139
|
-
const
|
|
140
|
-
const subject = t.
|
|
110
|
+
const when = formatTimestamp(args.gracePeriodEnd);
|
|
111
|
+
const subject = t(locale, "gdpr.mail.deletionRequested.subject", { app });
|
|
141
112
|
const body = `
|
|
142
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t.greeting)}</p>
|
|
143
|
-
<p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t.
|
|
144
|
-
<p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t.
|
|
113
|
+
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
114
|
+
<p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.intro", { app, when }))}</p>
|
|
115
|
+
<p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.cancel"))}</p>`;
|
|
145
116
|
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }) };
|
|
146
117
|
}
|
|
147
118
|
|
|
148
119
|
export function renderDeletionExecutedEmail(args: RenderDeletionExecutedEmailArgs): RenderedEmail {
|
|
149
120
|
const locale = args.locale ?? "en";
|
|
150
121
|
const app = appNameFor(args);
|
|
151
|
-
const
|
|
152
|
-
const subject = t.
|
|
122
|
+
const when = formatTimestamp(args.executedAt);
|
|
123
|
+
const subject = t(locale, "gdpr.mail.deletionExecuted.subject", { app });
|
|
153
124
|
const body = `
|
|
154
|
-
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t.greeting)}</p>
|
|
155
|
-
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t.
|
|
125
|
+
<p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
|
|
126
|
+
<p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }))}</p>`;
|
|
156
127
|
return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }) };
|
|
157
128
|
}
|
|
158
129
|
|
|
@@ -160,11 +131,9 @@ function wrapCell(bodyHtml: string): string {
|
|
|
160
131
|
return `<tr><td>${bodyHtml}</td></tr>`;
|
|
161
132
|
}
|
|
162
133
|
|
|
163
|
-
// Plain inline-styled HTML — gespiegelt von auth-email-password.
|
|
164
|
-
// guard:dup-ok — Email-HTML (table-layout, inline CSS) ≠ Web-HTML (legal-pages/markdown.ts)
|
|
165
134
|
function renderShell(args: { title: string; bodyHtml: string; locale: GdprMailLocale }): string {
|
|
166
135
|
return `<!DOCTYPE html>
|
|
167
|
-
<html lang="${args.locale}">
|
|
136
|
+
<html lang="${escapeHtmlAttr(args.locale)}">
|
|
168
137
|
<head>
|
|
169
138
|
<meta charset="utf-8" />
|
|
170
139
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -184,19 +153,14 @@ function renderShell(args: { title: string; bodyHtml: string; locale: GdprMailLo
|
|
|
184
153
|
</html>`;
|
|
185
154
|
}
|
|
186
155
|
|
|
187
|
-
// guard:dup-ok — Email-HTML-Helper; selbe normalisierte AST-Form wie auth-email-password, verschiedene Semantik
|
|
188
156
|
function renderButton(args: { url: string; label: string }): string {
|
|
189
157
|
return `<a href="${escapeHtmlAttr(args.url)}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500;">${escapeHtml(args.label)}</a>`;
|
|
190
158
|
}
|
|
191
159
|
|
|
192
|
-
// guard:dup-ok — Email-HTML-Helper; selbe normalisierte AST-Form wie auth-email-password, verschiedene Semantik
|
|
193
160
|
function renderFallbackUrl(args: { url: string; label: string }): string {
|
|
194
161
|
return `<p style="margin: 24px 0 0; font-size: 12px; color: #666;">${escapeHtml(args.label)}<br /><a href="${escapeHtmlAttr(args.url)}" style="color: #1a1a1a; word-break: break-all;">${escapeHtml(args.url)}</a></p>`;
|
|
195
162
|
}
|
|
196
163
|
|
|
197
|
-
// ISO-Timestamp → "2026-05-04 13:45 UTC". Locale-unabhaengig + UTC-Suffix
|
|
198
|
-
// damit der User unabhaengig von seiner Tz sieht wann etwas passiert; bei
|
|
199
|
-
// un-parsbarem Input faellt's auf den raw-string zurueck.
|
|
200
164
|
function formatTimestamp(iso: string): string {
|
|
201
165
|
try {
|
|
202
166
|
const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
|
|
@@ -1,88 +1,36 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
// Server + client i18n for user-data-rights (operator screens + nav). Pure
|
|
3
3
|
// data, importable from the web bundle (web/i18n.ts derives the client keys).
|
|
4
|
-
type LocalizedString = { readonly
|
|
4
|
+
type LocalizedString = { readonly en: string };
|
|
5
5
|
|
|
6
6
|
export const USER_DATA_RIGHTS_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
-
"user-data-rights:nav.exportJobs": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
"screen:export-job-detail.title": {
|
|
18
|
-
de: "Export-Job",
|
|
19
|
-
en: "Export job",
|
|
20
|
-
es: "Trabajo de exportación",
|
|
21
|
-
},
|
|
22
|
-
"screen:download-attempt-list.title": {
|
|
23
|
-
de: "Download-Versuche",
|
|
24
|
-
en: "Download attempts",
|
|
25
|
-
es: "Intentos de descarga",
|
|
26
|
-
},
|
|
27
|
-
"screen:privacy-center.title": { de: "Datenschutz", en: "Privacy", es: "Privacidad" },
|
|
28
|
-
"user-data-rights:entity:export-job:field:userId": { de: "Benutzer", en: "User", es: "Usuario" },
|
|
29
|
-
"user-data-rights:entity:export-job:field:status": { de: "Status", en: "Status", es: "Estado" },
|
|
30
|
-
"user-data-rights:entity:export-job:field:requestedAt": {
|
|
31
|
-
de: "Angefordert",
|
|
32
|
-
en: "Requested",
|
|
33
|
-
es: "Solicitado",
|
|
34
|
-
},
|
|
35
|
-
"user-data-rights:entity:export-job:field:completedAt": {
|
|
36
|
-
de: "Abgeschlossen",
|
|
37
|
-
en: "Completed",
|
|
38
|
-
es: "Completado",
|
|
39
|
-
},
|
|
40
|
-
"user-data-rights:entity:export-job:field:expiresAt": {
|
|
41
|
-
de: "Läuft ab",
|
|
42
|
-
en: "Expires",
|
|
43
|
-
es: "Caduca",
|
|
44
|
-
},
|
|
7
|
+
"user-data-rights:nav.exportJobs": { en: "GDPR exports" },
|
|
8
|
+
"screen:export-job-list.title": { en: "GDPR exports" },
|
|
9
|
+
"screen:export-job-detail.title": { en: "Export job" },
|
|
10
|
+
"screen:download-attempt-list.title": { en: "Download attempts" },
|
|
11
|
+
"screen:privacy-center.title": { en: "Privacy" },
|
|
12
|
+
"user-data-rights:entity:export-job:field:userId": { en: "User" },
|
|
13
|
+
"user-data-rights:entity:export-job:field:status": { en: "Status" },
|
|
14
|
+
"user-data-rights:entity:export-job:field:requestedAt": { en: "Requested" },
|
|
15
|
+
"user-data-rights:entity:export-job:field:completedAt": { en: "Completed" },
|
|
16
|
+
"user-data-rights:entity:export-job:field:expiresAt": { en: "Expires" },
|
|
45
17
|
"user-data-rights:entity:export-job:field:requestedFromTenantId": {
|
|
46
|
-
de: "Mandant",
|
|
47
18
|
en: "Tenant",
|
|
48
|
-
es: "Organización",
|
|
49
|
-
},
|
|
50
|
-
"user-data-rights:entity:export-job:field:startedAt": {
|
|
51
|
-
de: "Gestartet",
|
|
52
|
-
en: "Started",
|
|
53
|
-
es: "Iniciado",
|
|
54
19
|
},
|
|
20
|
+
"user-data-rights:entity:export-job:field:startedAt": { en: "Started" },
|
|
55
21
|
"user-data-rights:entity:export-job:field:downloadStorageKey": {
|
|
56
|
-
de: "Speicher-Schlüssel",
|
|
57
22
|
en: "Storage key",
|
|
58
|
-
es: "Clave de almacenamiento",
|
|
59
|
-
},
|
|
60
|
-
"user-data-rights:entity:export-job:field:bytesWritten": {
|
|
61
|
-
de: "Bytes",
|
|
62
|
-
en: "Bytes",
|
|
63
|
-
es: "Bytes",
|
|
64
|
-
},
|
|
65
|
-
"user-data-rights:entity:export-job:field:errorMessage": {
|
|
66
|
-
de: "Fehler",
|
|
67
|
-
en: "Error",
|
|
68
|
-
es: "Error",
|
|
69
23
|
},
|
|
24
|
+
"user-data-rights:entity:export-job:field:bytesWritten": { en: "Bytes" },
|
|
25
|
+
"user-data-rights:entity:export-job:field:errorMessage": { en: "Error" },
|
|
70
26
|
"user-data-rights:entity:download-attempt:field:attemptedAt": {
|
|
71
|
-
de: "Zeitpunkt",
|
|
72
27
|
en: "Attempted at",
|
|
73
|
-
es: "Fecha y hora",
|
|
74
|
-
},
|
|
75
|
-
"user-data-rights:entity:download-attempt:field:result": {
|
|
76
|
-
de: "Ergebnis",
|
|
77
|
-
en: "Result",
|
|
78
|
-
es: "Resultado",
|
|
79
28
|
},
|
|
80
|
-
"user-data-rights:entity:download-attempt:field:
|
|
81
|
-
"user-data-rights:entity:download-attempt:field:
|
|
29
|
+
"user-data-rights:entity:download-attempt:field:result": { en: "Result" },
|
|
30
|
+
"user-data-rights:entity:download-attempt:field:via": { en: "Via" },
|
|
31
|
+
"user-data-rights:entity:download-attempt:field:ip": { en: "IP" },
|
|
82
32
|
"user-data-rights:entity:download-attempt:field:attemptedByUserId": {
|
|
83
|
-
de: "Benutzer",
|
|
84
33
|
en: "User",
|
|
85
|
-
es: "Usuario",
|
|
86
34
|
},
|
|
87
|
-
"user-data-rights:entity:download-attempt:field:jobId": {
|
|
35
|
+
"user-data-rights:entity:download-attempt:field:jobId": { en: "Job" },
|
|
88
36
|
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { EmailTransport } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
13
13
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import { hasMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
14
15
|
import {
|
|
15
16
|
type GdprMailLocale,
|
|
16
17
|
normalizeGdprMailLocale,
|
|
@@ -44,7 +45,9 @@ function localeFor(
|
|
|
44
45
|
userLocale: string | null,
|
|
45
46
|
defaults: GdprMailDefaults,
|
|
46
47
|
): GdprMailLocale | undefined {
|
|
47
|
-
|
|
48
|
+
const user = normalizeGdprMailLocale(userLocale);
|
|
49
|
+
if (user !== undefined && hasMailTranslations(user)) return user;
|
|
50
|
+
return defaults.locale;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export function makeDefaultExportReadyEmail(
|
|
@@ -14,7 +14,7 @@ import { ConfirmAccountDeletionScreen } from "../confirm-deletion-screen";
|
|
|
14
14
|
import { defaultTranslations } from "../i18n";
|
|
15
15
|
import { RequestAccountDeletionScreen } from "../request-deletion-screen";
|
|
16
16
|
|
|
17
|
-
const resolver = createStaticLocaleResolver({ locale: "
|
|
17
|
+
const resolver = createStaticLocaleResolver({ locale: "en" });
|
|
18
18
|
|
|
19
19
|
type WriteCall = { readonly type: string; readonly payload: unknown };
|
|
20
20
|
|
|
@@ -62,7 +62,7 @@ describe("RequestAccountDeletionScreen", () => {
|
|
|
62
62
|
const ui = renderWith(<RequestAccountDeletionScreen />, makeDispatcher(true, calls));
|
|
63
63
|
fireEvent.change(ui.getByRole("textbox"), { target: { value: "a@b.com" } });
|
|
64
64
|
fireEvent.click(ui.getByRole("button"));
|
|
65
|
-
await waitFor(() => expect(ui.getByText(/
|
|
65
|
+
await waitFor(() => expect(ui.getByText(/Email sent/)).toBeTruthy());
|
|
66
66
|
await waitFor(() => expect(calls).toHaveLength(1));
|
|
67
67
|
expect(calls[0]?.type).toBe("user-data-rights:write:request-deletion-by-email");
|
|
68
68
|
expect(calls[0]?.payload).toEqual({ email: "a@b.com" });
|
|
@@ -73,8 +73,8 @@ describe("RequestAccountDeletionScreen", () => {
|
|
|
73
73
|
const ui = renderWith(<RequestAccountDeletionScreen />, makeDispatcher(false, calls));
|
|
74
74
|
fireEvent.change(ui.getByRole("textbox"), { target: { value: "a@b.com" } });
|
|
75
75
|
fireEvent.click(ui.getByRole("button"));
|
|
76
|
-
await waitFor(() => expect(ui.getByText(/
|
|
77
|
-
expect(ui.queryByText(/
|
|
76
|
+
await waitFor(() => expect(ui.getByText(/went wrong/)).toBeTruthy());
|
|
77
|
+
expect(ui.queryByText(/Email sent/)).toBeNull();
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -82,7 +82,7 @@ describe("ConfirmAccountDeletionScreen", () => {
|
|
|
82
82
|
test("ohne #token → missingToken, kein Confirm-Button", () => {
|
|
83
83
|
window.history.replaceState({}, "", "/delete-account/confirm");
|
|
84
84
|
const ui = renderWith(<ConfirmAccountDeletionScreen />, makeDispatcher(true, []));
|
|
85
|
-
expect(ui.getByText(/
|
|
85
|
+
expect(ui.getByText(/No token/)).toBeTruthy();
|
|
86
86
|
expect(ui.queryByRole("button")).toBeNull();
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -91,7 +91,7 @@ describe("ConfirmAccountDeletionScreen", () => {
|
|
|
91
91
|
const calls: WriteCall[] = [];
|
|
92
92
|
const ui = renderWith(<ConfirmAccountDeletionScreen />, makeDispatcher(true, calls));
|
|
93
93
|
fireEvent.click(ui.getByRole("button"));
|
|
94
|
-
await waitFor(() => expect(ui.getByText(/
|
|
94
|
+
await waitFor(() => expect(ui.getByText(/Deletion scheduled/)).toBeTruthy());
|
|
95
95
|
await waitFor(() => expect(calls).toHaveLength(1));
|
|
96
96
|
expect(calls[0]?.type).toBe("user-data-rights:write:confirm-deletion-by-token");
|
|
97
97
|
expect(calls[0]?.payload).toEqual({ token: "tok-123" });
|
|
@@ -101,16 +101,16 @@ describe("ConfirmAccountDeletionScreen", () => {
|
|
|
101
101
|
window.history.replaceState({}, "", "/delete-account/confirm#token=bad");
|
|
102
102
|
const ui = renderWith(<ConfirmAccountDeletionScreen />, makeDispatcher(false, []));
|
|
103
103
|
fireEvent.click(ui.getByRole("button"));
|
|
104
|
-
await waitFor(() => expect(ui.getByText(/
|
|
105
|
-
expect(ui.queryByText(/
|
|
104
|
+
await waitFor(() => expect(ui.getByText(/invalid or expired/)).toBeTruthy());
|
|
105
|
+
expect(ui.queryByText(/Deletion scheduled/)).toBeNull();
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test("write wirft → generischer Error-Banner, NICHT invalidToken", async () => {
|
|
109
109
|
window.history.replaceState({}, "", "/delete-account/confirm#token=tok-123");
|
|
110
110
|
const ui = renderWith(<ConfirmAccountDeletionScreen />, makeThrowingDispatcher());
|
|
111
111
|
fireEvent.click(ui.getByRole("button"));
|
|
112
|
-
await waitFor(() => expect(ui.getByText(/
|
|
113
|
-
expect(ui.queryByText(/
|
|
114
|
-
expect(ui.queryByText(/
|
|
112
|
+
await waitFor(() => expect(ui.getByText(/went wrong/)).toBeTruthy());
|
|
113
|
+
expect(ui.queryByText(/invalid or expired/)).toBeNull();
|
|
114
|
+
expect(ui.queryByText(/Deletion scheduled/)).toBeNull();
|
|
115
115
|
});
|
|
116
116
|
});
|