diff --git a/public/confirm-aktionariat/confirm.js b/public/confirm-aktionariat/confirm.js
index 6ac52f1..d19f613 100644
--- a/public/confirm-aktionariat/confirm.js
+++ b/public/confirm-aktionariat/confirm.js
@@ -42,7 +42,7 @@
// confirmation email always reaches this web page; the app registers the scheme
// to re-open itself after confirmation.
- var STATES = ['loading', 'confirmed', 'invalid', 'unavailable'];
+ var STATES = ['loading', 'confirmed', 'invalid', 'no-registration', 'unavailable'];
function show(state) {
STATES.forEach(function (s) {
document.getElementById('state-' + s).hidden = s !== state;
@@ -58,6 +58,8 @@
show('confirmed');
} else if (status === 'invalid') {
show('invalid');
+ } else if (status === 'no-registration') {
+ show('no-registration');
} else {
show('unavailable');
}
@@ -66,7 +68,7 @@
function confirm() {
show('loading');
- // Mock hook for LOCAL preview only (?mock=confirmed|invalid|unavailable).
+ // Mock hook for LOCAL preview only (?mock=confirmed|invalid|no-registration|unavailable).
// Never honored on the real realunit.app / dev.realunit.app hosts, so a
// shared prod link cannot render a spoofed confirmation screen.
var mock = params.get('mock');
diff --git a/public/confirm-aktionariat/index.html b/public/confirm-aktionariat/index.html
index 07da093..753d429 100644
--- a/public/confirm-aktionariat/index.html
+++ b/public/confirm-aktionariat/index.html
@@ -117,6 +117,16 @@
Dieser Bestätigungslink ist ungültig oder bereits abgelaufen. Bitte fordern Sie in der App einen neuen an.
+
+
diff --git a/public/js/lib/confirm-core.js b/public/js/lib/confirm-core.js
index bbaba5f..7f10543 100644
--- a/public/js/lib/confirm-core.js
+++ b/public/js/lib/confirm-core.js
@@ -39,6 +39,9 @@
'unavailable.body':
'Wir konnten die Bestätigung gerade nicht abschliessen. Bitte versuchen Sie es in ein paar Minuten erneut.',
'unavailable.cta': 'Erneut versuchen',
+ 'no-registration.title': 'Link passt zu keiner Registrierung',
+ 'no-registration.body':
+ 'Ihre E-Mail-Adresse wurde zwar bestätigt, aber wir haben dazu keine Wallet-Registrierung gefunden. Bitte prüfen Sie, ob Sie in der RealUnit-App eine andere E-Mail-Adresse verwendet haben, oder kontaktieren Sie unseren Support.',
},
en: {
'doc.title': 'RealUnit — Address confirmation',
@@ -57,6 +60,9 @@
'unavailable.body':
'We couldn’t complete the confirmation right now. Please try again in a few minutes.',
'unavailable.cta': 'Try again',
+ 'no-registration.title': 'Link doesn’t match a registration',
+ 'no-registration.body':
+ 'Your email address was confirmed, but we couldn’t find a wallet registration for it. Please check whether you used a different email address in the RealUnit app, or contact our support.',
},
};
@@ -148,13 +154,19 @@
// Map an API response to a UI state. Any non-2xx (validation 400, rate-limit
// 429, 5xx) is a transient/unknown state → 'unavailable' (retryable), never a
- // hard rejection. On 2xx the body's own status decides, and an unrecognized
+ // hard rejection. On 2xx the body's own status decides: 'confirmed_no_registration'
+ // (the email was confirmed but no wallet registration matched it — a permanent
+ // outcome, not a transient failure) maps to the dedicated 'no-registration' state,
+ // the other recognized statuses pass through unchanged, and an unrecognized
// status is treated as unavailable rather than trusted.
function mapResult(response) {
if (!response.ok) {
return 'unavailable';
}
var status = response.body && response.body.status;
+ if (status === 'confirmed_no_registration') {
+ return 'no-registration';
+ }
if (status === 'confirmed' || status === 'invalid' || status === 'unavailable') {
return status;
}
diff --git a/test/confirm-core.test.mjs b/test/confirm-core.test.mjs
index f7b828a..56ce91d 100644
--- a/test/confirm-core.test.mjs
+++ b/test/confirm-core.test.mjs
@@ -173,6 +173,12 @@ describe('mapResult', () => {
expect(mapResult({ ok: true, body: { status: 'unavailable' } })).toBe('unavailable');
});
+ test('a 2xx response with status confirmed_no_registration maps to no-registration', () => {
+ expect(mapResult({ ok: true, body: { status: 'confirmed_no_registration' } })).toBe(
+ 'no-registration',
+ );
+ });
+
test('a 2xx response with an unrecognized status maps to unavailable', () => {
expect(mapResult({ ok: true, body: { status: 'weird' } })).toBe('unavailable');
});
diff --git a/tests/__screenshots__/desktop-chromium/confirm-no-registration-en.png b/tests/__screenshots__/desktop-chromium/confirm-no-registration-en.png
new file mode 100644
index 0000000..803b84b
Binary files /dev/null and b/tests/__screenshots__/desktop-chromium/confirm-no-registration-en.png differ
diff --git a/tests/__screenshots__/desktop-chromium/confirm-no-registration.png b/tests/__screenshots__/desktop-chromium/confirm-no-registration.png
new file mode 100644
index 0000000..73d4351
Binary files /dev/null and b/tests/__screenshots__/desktop-chromium/confirm-no-registration.png differ
diff --git a/tests/__screenshots__/mobile-safari/confirm-no-registration.png b/tests/__screenshots__/mobile-safari/confirm-no-registration.png
new file mode 100644
index 0000000..24e2658
Binary files /dev/null and b/tests/__screenshots__/mobile-safari/confirm-no-registration.png differ
diff --git a/tests/__screenshots__/tablet-chromium/confirm-no-registration.png b/tests/__screenshots__/tablet-chromium/confirm-no-registration.png
new file mode 100644
index 0000000..6b5eadf
Binary files /dev/null and b/tests/__screenshots__/tablet-chromium/confirm-no-registration.png differ
diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs
index 61b04ab..49a891d 100644
--- a/tests/behavior.spec.mjs
+++ b/tests/behavior.spec.mjs
@@ -75,7 +75,7 @@ test.describe('confirm-aktionariat flow', () => {
expect(confirmCalls).toEqual([]);
});
- for (const state of ['confirmed', 'invalid', 'unavailable']) {
+ for (const state of ['confirmed', 'invalid', 'no-registration', 'unavailable']) {
test(`?mock=${state} renders the ${state} state`, async ({ page }) => {
await page.goto(`/confirm-aktionariat/?mock=${state}`);
await expect(page.locator(`#state-${state}`)).toBeVisible();
@@ -178,6 +178,14 @@ test.describe('confirm-aktionariat flow', () => {
await expect(page.locator('#state-invalid')).toBeVisible();
});
+ test('a 200 response with the confirmed_no_registration status shows the no-registration state', async ({
+ page,
+ }) => {
+ await routeConfirm(page, { status: 200, body: { status: 'confirmed_no_registration' } });
+ await page.goto('/confirm-aktionariat/?email=a%40b.ch&code=C&user=U');
+ await expect(page.locator('#state-no-registration')).toBeVisible();
+ });
+
test('a non-2xx API response shows the unavailable state', async ({ page }) => {
await routeConfirm(page, { status: 500, body: {} });
await page.goto('/confirm-aktionariat/?email=a%40b.ch&code=C&user=U');
diff --git a/tests/pages.mjs b/tests/pages.mjs
index eb3b49f..976a6a2 100644
--- a/tests/pages.mjs
+++ b/tests/pages.mjs
@@ -20,8 +20,8 @@ export const PROJECTS = ['desktop-chromium', 'tablet-chromium', 'mobile-safari']
// override before the page scripts run, so platform.js sets
// html[data-platform] deterministically regardless of the device
// waitFor — optional confirm-page state ('confirmed' | 'invalid' |
-// 'unavailable') to wait for before the shot (the ?mock hook renders
-// it after a short delay)
+// 'no-registration' | 'unavailable') to wait for before the shot (the
+// ?mock hook renders it after a short delay)
// projects — the viewports this view applies to
//
// Coverage: the landing page in both its equal-badge (desktop/tablet) and
@@ -73,6 +73,20 @@ export const VIEWS = [
waitFor: 'invalid',
projects: ['desktop-chromium'],
},
+ // Confirm — no-registration state (email confirmed, no wallet registration
+ // matched it — a permanent outcome, no retry CTA).
+ {
+ slug: 'confirm-no-registration',
+ path: '/confirm-aktionariat/?mock=no-registration&lang=de',
+ waitFor: 'no-registration',
+ projects: ['desktop-chromium', 'tablet-chromium', 'mobile-safari'],
+ },
+ {
+ slug: 'confirm-no-registration-en',
+ path: '/confirm-aktionariat/?mock=no-registration&lang=en',
+ waitFor: 'no-registration',
+ projects: ['desktop-chromium'],
+ },
// Confirm — service unavailable (the retry button is shown).
{
slug: 'confirm-unavailable',