@nebulr-group/bridge-svelte 0.5.1 → 0.6.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/auth/route-guard.js +27 -4
- package/dist/client/BridgeBootstrap.js +35 -5
- package/dist/client/BridgeBootstrap.svelte +17 -6
- package/dist/client/components/sdk-auth/ForgotPassword.svelte +48 -20
- package/dist/client/components/sdk-auth/ForgotPassword.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/LoginForm.svelte +43 -36
- package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/MagicLink.svelte +36 -12
- package/dist/client/components/sdk-auth/MagicLink.svelte.d.ts +5 -0
- package/dist/client/components/sdk-auth/MfaChallenge.svelte +21 -14
- package/dist/client/components/sdk-auth/MfaChallenge.svelte.d.ts +3 -0
- package/dist/client/components/sdk-auth/MfaSetup.svelte +55 -19
- package/dist/client/components/sdk-auth/MfaSetup.svelte.d.ts +14 -0
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte +11 -4
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte.d.ts +3 -0
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte +38 -15
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +45 -19
- package/dist/client/components/sdk-auth/PasskeySetup.svelte.d.ts +13 -0
- package/dist/client/components/sdk-auth/SignupForm.svelte +49 -14
- package/dist/client/components/sdk-auth/SignupForm.svelte.d.ts +16 -0
- package/dist/client/components/sdk-auth/auth-form-description.test.d.ts +1 -0
- package/dist/client/components/sdk-auth/auth-form-description.test.js +470 -0
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte +37 -1
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte.d.ts +22 -0
- package/dist/client/stores/i18n.d.ts +18 -0
- package/dist/client/stores/i18n.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +3 -3
package/dist/auth/route-guard.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
// src/lib/auth/route-guard.ts — thin wrapper delegating to auth-core via bridge-instance
|
|
2
2
|
import { getBridgeAuth } from '../core/bridge-instance.js';
|
|
3
|
-
import { getRouteGuardConfig } from '../client/stores/config.store.js';
|
|
3
|
+
import { getConfig, getRouteGuardConfig } from '../client/stores/config.store.js';
|
|
4
4
|
export function createRouteGuard(flagsReady) {
|
|
5
5
|
const config = getRouteGuardConfig();
|
|
6
|
-
|
|
6
|
+
// TBP-629 — feed the app's own loginRoute into the guard so it can refuse to
|
|
7
|
+
// make the login page its own return target. The consumer already told us
|
|
8
|
+
// where their login page is via BridgeConfig; making them repeat it under
|
|
9
|
+
// routeConfig.returnTo would be a second source of truth that can drift.
|
|
10
|
+
// An explicit routeConfig value still wins.
|
|
11
|
+
const { loginRoute } = getConfig();
|
|
12
|
+
const guardConfig = {
|
|
13
|
+
...config,
|
|
14
|
+
returnTo: {
|
|
15
|
+
...config?.returnTo,
|
|
16
|
+
loginRoute: config?.returnTo?.loginRoute ?? loginRoute,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const guard = getBridgeAuth().createRouteGuard(guardConfig);
|
|
7
20
|
if (!flagsReady)
|
|
8
21
|
return guard;
|
|
9
22
|
// Wrap checkRouteRestrictions to await flagsReady before evaluating
|
|
@@ -13,9 +26,19 @@ export function createRouteGuard(flagsReady) {
|
|
|
13
26
|
await flagsReady;
|
|
14
27
|
return guard.checkRouteRestrictions(pathname);
|
|
15
28
|
},
|
|
16
|
-
async getNavigationDecision(pathname) {
|
|
29
|
+
async getNavigationDecision(pathname, attempted) {
|
|
17
30
|
if (guard.shouldRedirectToLogin(pathname)) {
|
|
18
|
-
|
|
31
|
+
// TBP-629 — this branch short-circuits before flagsReady on purpose
|
|
32
|
+
// (an unauthenticated visitor needs no flag evaluation), which is
|
|
33
|
+
// exactly why `attempted` has to be threaded through here too. The
|
|
34
|
+
// wrapper previously rebuilt the decision by hand and would silently
|
|
35
|
+
// drop any argument auth-core's version learned to accept.
|
|
36
|
+
const returnTo = guard.resolveReturnTo(attempted ?? pathname);
|
|
37
|
+
return {
|
|
38
|
+
type: 'login',
|
|
39
|
+
loginUrl: guard.getLoginRedirect(),
|
|
40
|
+
...(returnTo ? { returnTo } : {}),
|
|
41
|
+
};
|
|
19
42
|
}
|
|
20
43
|
await flagsReady;
|
|
21
44
|
const redirectTo = await guard.checkRouteRestrictions(pathname);
|
|
@@ -4,9 +4,9 @@ import { get } from 'svelte/store';
|
|
|
4
4
|
import { createRouteGuard } from '../auth/route-guard.js';
|
|
5
5
|
import { getBridgeAuth, bridgeReadyStore, markReady, waitForBridge as _waitForBridge, } from '../core/bridge-instance.js';
|
|
6
6
|
import { installBridgeAuthFetch } from '../core/bridge-runtime.js';
|
|
7
|
-
import { useBridge } from '@nebulr-group/bridge-auth-core';
|
|
7
|
+
import { useBridge, stashReturnTo, takeReturnTo, withReturnTo } from '@nebulr-group/bridge-auth-core';
|
|
8
8
|
import { logger } from '../shared/logger.js';
|
|
9
|
-
import { bridgeConfig, getConfig } from './stores/config.store.js';
|
|
9
|
+
import { bridgeConfig, getConfig, getRouteGuardConfig } from './stores/config.store.js';
|
|
10
10
|
export async function bridgeBootstrap(url, config, routeConfig = { rules: [], defaultAccess: 'protected' }, kitFetch) {
|
|
11
11
|
// If we've already completed bootstrap once, short-circuit
|
|
12
12
|
if (get(bridgeReadyStore)) {
|
|
@@ -58,7 +58,17 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
|
|
|
58
58
|
history.replaceState = svelteReplaceState;
|
|
59
59
|
}
|
|
60
60
|
const payment = url.searchParams.get('payment');
|
|
61
|
-
|
|
61
|
+
// TBP-629 — this line used to hard-code '/', which is where hosted
|
|
62
|
+
// mode lost the deep link even though the OAuth round-trip itself
|
|
63
|
+
// worked fine. `takeReturnTo()` is one-shot and re-sanitizes, and
|
|
64
|
+
// returns null when nothing was stashed, so the old behaviour is
|
|
65
|
+
// exactly what happens when there is no deep link to restore.
|
|
66
|
+
//
|
|
67
|
+
// `payment` wins: it signals a just-completed checkout whose landing
|
|
68
|
+
// page the billing flow owns, and that is a deliberate destination
|
|
69
|
+
// rather than a remembered one.
|
|
70
|
+
const stashedReturnTo = takeReturnTo();
|
|
71
|
+
redirect(303, payment ? `/?payment=${payment}` : (stashedReturnTo ?? '/'));
|
|
62
72
|
}
|
|
63
73
|
catch (err) {
|
|
64
74
|
if (isRedirect(err))
|
|
@@ -170,13 +180,33 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
|
|
|
170
180
|
hasTokens: !!currentTokens?.accessToken,
|
|
171
181
|
isAuthenticated: currentAuth
|
|
172
182
|
});
|
|
173
|
-
|
|
183
|
+
// TBP-629 — hand the guard the FULL attempted target, not just the pathname.
|
|
184
|
+
// `?key=…` style query is part of the deep link for plenty of routes, and an
|
|
185
|
+
// exported-file link that loses its query is as broken as one that loses its
|
|
186
|
+
// path.
|
|
187
|
+
const attempted = `${url.pathname}${url.search}`;
|
|
188
|
+
const decision = await guard.getNavigationDecision(url.pathname, attempted);
|
|
174
189
|
logger.debug('[bridgeBootstrap] navigation decision', decision);
|
|
175
190
|
if (decision.type === 'login') {
|
|
176
191
|
const { loginRoute } = getConfig();
|
|
192
|
+
const returnToParam = getRouteGuardConfig()?.returnTo?.param;
|
|
177
193
|
// SDK mode: consumer explicitly set loginRoute → redirect to in-app login view
|
|
178
194
|
// Hosted mode (default): no loginRoute → redirect to hosted auth portal
|
|
179
|
-
|
|
195
|
+
//
|
|
196
|
+
// TBP-629: only SDK mode gains the return target. Hosted mode is left exactly
|
|
197
|
+
// as it was — `createLoginUrl()` already carries its own `redirectUri` (the
|
|
198
|
+
// configured callbackUrl) and changing what that means would alter an OAuth
|
|
199
|
+
// round-trip that consumers have registered redirect URIs against.
|
|
200
|
+
if (loginRoute) {
|
|
201
|
+
redirect(303, withReturnTo(loginRoute, decision.returnTo, returnToParam));
|
|
202
|
+
}
|
|
203
|
+
// Hosted mode (TBP-629): the target CANNOT ride on the URL. `createLoginUrl()`
|
|
204
|
+
// feeds `redirectUri` to the OAuth authorize call and bridge-api validates it
|
|
205
|
+
// with an exact `allowedRedirectUris.includes()` match, so adding a query to
|
|
206
|
+
// it would break login rather than improve it. Stash it instead and pick it
|
|
207
|
+
// up at the callback below — the OAuth request itself stays untouched.
|
|
208
|
+
stashReturnTo(decision.returnTo);
|
|
209
|
+
redirect(303, bridge.createLoginUrl());
|
|
180
210
|
}
|
|
181
211
|
if (decision.type === 'redirect' && url.pathname !== decision.to) {
|
|
182
212
|
redirect(303, decision.to);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { page } from '$app/stores';
|
|
4
4
|
import { onMount, onDestroy } from 'svelte';
|
|
5
5
|
import { createRouteGuard, routeRulesReferenceFlag } from '../auth/route-guard.js';
|
|
6
|
+
import { stashReturnTo, withReturnTo } from '@nebulr-group/bridge-auth-core';
|
|
6
7
|
import {
|
|
7
8
|
getBridgeAuth,
|
|
8
9
|
isAuthenticated,
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
} from '../core/bridge-instance.js';
|
|
13
14
|
import { bridge as bridgeSurface } from '../core/bridge.js';
|
|
14
15
|
import { setBridgeContext } from '../core/use-bridge.js';
|
|
15
|
-
import { getConfig } from './stores/config.store.js';
|
|
16
|
+
import { getConfig, getRouteGuardConfig } from './stores/config.store.js';
|
|
16
17
|
import {
|
|
17
18
|
onBridgeFlagChange,
|
|
18
19
|
startBridgeRuntime,
|
|
@@ -73,14 +74,24 @@
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
async function handleRoute(pathname: string, cancel?: () => void) {
|
|
77
|
-
|
|
77
|
+
async function handleRoute(pathname: string, cancel?: () => void, search?: string) {
|
|
78
|
+
// TBP-629 — client-side navigation loses the deep link the same way the
|
|
79
|
+
// load-time path did. Fixing only BridgeBootstrap.ts would leave somebody
|
|
80
|
+
// who clicks an in-app link into a protected route while their session is
|
|
81
|
+
// gone landing on the default route, which is the same bug with a different
|
|
82
|
+
// trigger.
|
|
83
|
+
const attempted = `${pathname}${search ?? ''}`;
|
|
84
|
+
const decision = await guard.getNavigationDecision(pathname, attempted);
|
|
78
85
|
if (decision.type === 'login') {
|
|
79
86
|
if (cancel) cancel();
|
|
80
87
|
const { loginRoute } = getConfig();
|
|
81
88
|
if (loginRoute) {
|
|
82
|
-
goto(loginRoute);
|
|
89
|
+
goto(withReturnTo(loginRoute, decision.returnTo, getRouteGuardConfig()?.returnTo?.param));
|
|
83
90
|
} else {
|
|
91
|
+
// Hosted mode (TBP-629) — stash before handing off to the portal; the
|
|
92
|
+
// callback in BridgeBootstrap.ts picks it up. Same reason as there: the
|
|
93
|
+
// OAuth redirectUri is exact-matched server-side and must not be touched.
|
|
94
|
+
stashReturnTo(decision.returnTo);
|
|
84
95
|
getBridgeAuth().login();
|
|
85
96
|
}
|
|
86
97
|
return;
|
|
@@ -110,7 +121,7 @@
|
|
|
110
121
|
_recheckTimer = undefined;
|
|
111
122
|
// No `cancel` here: there is no navigation in flight to cancel. A denied
|
|
112
123
|
// verdict redirects the user off the page they are already on.
|
|
113
|
-
handleRoute(window.location.pathname).catch(() => {
|
|
124
|
+
handleRoute(window.location.pathname, undefined, window.location.search).catch(() => {
|
|
114
125
|
/* a failed re-check must never break the page; the next navigation
|
|
115
126
|
re-evaluates anyway */
|
|
116
127
|
});
|
|
@@ -180,6 +191,6 @@
|
|
|
180
191
|
|
|
181
192
|
beforeNavigate(async ({ to, cancel }) => {
|
|
182
193
|
if (!to) return;
|
|
183
|
-
await handleRoute(to.url.pathname, cancel);
|
|
194
|
+
await handleRoute(to.url.pathname, cancel, to.url.search);
|
|
184
195
|
});
|
|
185
196
|
</script>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
3
4
|
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
5
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
4
6
|
import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
|
|
5
7
|
import Spinner from './shared/Spinner.svelte';
|
|
6
8
|
import Alert from './shared/Alert.svelte';
|
|
@@ -15,6 +17,14 @@
|
|
|
15
17
|
* Pass `null`/`''` to render no heading and use your own page title.
|
|
16
18
|
*/
|
|
17
19
|
heading?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Step description. Pass `null`/`''` to render nothing and use your own
|
|
22
|
+
* subtitle (TBP-631). Only ever shown on the send-link step; the set-password
|
|
23
|
+
* and success states carry none.
|
|
24
|
+
*/
|
|
25
|
+
description?: string | null;
|
|
26
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
27
|
+
messages?: MessageOverrides;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
let {
|
|
@@ -23,11 +33,15 @@
|
|
|
23
33
|
onError,
|
|
24
34
|
loginHref = '/login',
|
|
25
35
|
heading = undefined,
|
|
36
|
+
description = undefined,
|
|
37
|
+
messages,
|
|
26
38
|
class: className,
|
|
27
39
|
style,
|
|
28
40
|
...rest
|
|
29
41
|
}: Props = $props();
|
|
30
42
|
|
|
43
|
+
const t = $derived(getTranslator(messages));
|
|
44
|
+
|
|
31
45
|
let email = $state('');
|
|
32
46
|
let password = $state('');
|
|
33
47
|
let confirmPassword = $state('');
|
|
@@ -42,11 +56,25 @@
|
|
|
42
56
|
// In a success state ("Password set" / the email-sent alert) the form heading
|
|
43
57
|
// is redundant, so suppress it. Otherwise use the override (if provided) or
|
|
44
58
|
// the built-in, state-appropriate heading.
|
|
45
|
-
const builtInHeading = $derived(
|
|
59
|
+
const builtInHeading = $derived(
|
|
60
|
+
isSetMode ? t('forgot.headingSet') : t('forgot.headingRequest'),
|
|
61
|
+
);
|
|
46
62
|
const wrapperHeading = $derived(
|
|
47
63
|
passwordReset || emailSent ? null : heading !== undefined ? heading : builtInHeading,
|
|
48
64
|
);
|
|
49
65
|
|
|
66
|
+
// TBP-631 — same shape as the heading above: the description belongs to the
|
|
67
|
+
// send-link step only. `undefined` means "not overridden" and falls through to
|
|
68
|
+
// the built-in; `null` is an explicit suppression from the host and must be
|
|
69
|
+
// respected, which is why this cannot collapse to `description ?? builtIn`.
|
|
70
|
+
const wrapperDescription = $derived(
|
|
71
|
+
isSetMode || passwordReset || emailSent
|
|
72
|
+
? null
|
|
73
|
+
: description !== undefined
|
|
74
|
+
? description
|
|
75
|
+
: t('forgot.description'),
|
|
76
|
+
);
|
|
77
|
+
|
|
50
78
|
async function handleSendLink() {
|
|
51
79
|
if (loading) return;
|
|
52
80
|
error = null;
|
|
@@ -55,7 +83,7 @@
|
|
|
55
83
|
await getBridgeAuth().sendResetPasswordLink(email);
|
|
56
84
|
emailSent = true;
|
|
57
85
|
} catch (err: any) {
|
|
58
|
-
error = err.message || '
|
|
86
|
+
error = err.message || t('forgot.error.send');
|
|
59
87
|
onError?.(err);
|
|
60
88
|
} finally {
|
|
61
89
|
loading = false;
|
|
@@ -67,12 +95,12 @@
|
|
|
67
95
|
error = null;
|
|
68
96
|
|
|
69
97
|
if (password !== confirmPassword) {
|
|
70
|
-
error = '
|
|
98
|
+
error = t('forgot.error.mismatch');
|
|
71
99
|
return;
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
if (password.length < 8) {
|
|
75
|
-
error = '
|
|
103
|
+
error = t('forgot.error.tooShort');
|
|
76
104
|
return;
|
|
77
105
|
}
|
|
78
106
|
|
|
@@ -82,7 +110,7 @@
|
|
|
82
110
|
passwordReset = true;
|
|
83
111
|
onComplete?.();
|
|
84
112
|
} catch (err: any) {
|
|
85
|
-
error = err.message || '
|
|
113
|
+
error = err.message || t('forgot.error.update');
|
|
86
114
|
onError?.(err);
|
|
87
115
|
} finally {
|
|
88
116
|
loading = false;
|
|
@@ -92,6 +120,7 @@
|
|
|
92
120
|
|
|
93
121
|
<AuthFormWrapper
|
|
94
122
|
heading={wrapperHeading}
|
|
123
|
+
description={wrapperDescription}
|
|
95
124
|
class={className}
|
|
96
125
|
{style}
|
|
97
126
|
{...rest}
|
|
@@ -102,19 +131,19 @@
|
|
|
102
131
|
|
|
103
132
|
{#if isSetMode}
|
|
104
133
|
{#if passwordReset}
|
|
105
|
-
<h2 class="bridge-success-heading">
|
|
134
|
+
<h2 class="bridge-success-heading">{t('forgot.successHeading')}</h2>
|
|
106
135
|
<div class="bridge-form-footer">
|
|
107
|
-
<a href={loginHref}>
|
|
136
|
+
<a href={loginHref}>{t('action.backToLogin')}</a>
|
|
108
137
|
</div>
|
|
109
138
|
{:else}
|
|
110
139
|
<form onsubmit={(e) => { e.preventDefault(); handleSetPassword(); }}>
|
|
111
140
|
<div class="bridge-form-group">
|
|
112
|
-
<label for="newPassword">
|
|
141
|
+
<label for="newPassword">{t('field.newPassword')}</label>
|
|
113
142
|
<div class="bridge-password-wrapper">
|
|
114
143
|
<input
|
|
115
144
|
id="newPassword"
|
|
116
145
|
type={showPasswords ? 'text' : 'password'}
|
|
117
|
-
placeholder=
|
|
146
|
+
placeholder={t('placeholder.newPassword')}
|
|
118
147
|
required
|
|
119
148
|
bind:value={password}
|
|
120
149
|
disabled={loading}
|
|
@@ -124,7 +153,7 @@
|
|
|
124
153
|
class="bridge-password-toggle"
|
|
125
154
|
onclick={() => showPasswords = !showPasswords}
|
|
126
155
|
tabindex={-1}
|
|
127
|
-
aria-label={showPasswords ? '
|
|
156
|
+
aria-label={showPasswords ? t('action.hidePasswords') : t('action.showPasswords')}
|
|
128
157
|
>
|
|
129
158
|
{#if showPasswords}
|
|
130
159
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
@@ -135,12 +164,12 @@
|
|
|
135
164
|
</div>
|
|
136
165
|
</div>
|
|
137
166
|
<div class="bridge-form-group">
|
|
138
|
-
<label for="confirmPassword">
|
|
167
|
+
<label for="confirmPassword">{t('field.confirmPassword')}</label>
|
|
139
168
|
<div class="bridge-password-wrapper">
|
|
140
169
|
<input
|
|
141
170
|
id="confirmPassword"
|
|
142
171
|
type={showPasswords ? 'text' : 'password'}
|
|
143
|
-
placeholder=
|
|
172
|
+
placeholder={t('placeholder.confirmPassword')}
|
|
144
173
|
required
|
|
145
174
|
bind:value={confirmPassword}
|
|
146
175
|
disabled={loading}
|
|
@@ -148,36 +177,35 @@
|
|
|
148
177
|
</div>
|
|
149
178
|
</div>
|
|
150
179
|
<button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !password}>
|
|
151
|
-
{#if loading}<Spinner size={16} />{:else}
|
|
180
|
+
{#if loading}<Spinner size={16} />{:else}{t('forgot.setSubmit')}{/if}
|
|
152
181
|
</button>
|
|
153
182
|
</form>
|
|
154
183
|
{/if}
|
|
155
184
|
{:else}
|
|
156
185
|
{#if emailSent}
|
|
157
|
-
<Alert variant="success">
|
|
186
|
+
<Alert variant="success">{t('forgot.emailSent')}</Alert>
|
|
158
187
|
<div class="bridge-form-footer">
|
|
159
|
-
<a href={loginHref}>
|
|
188
|
+
<a href={loginHref}>{t('action.backToLogin')}</a>
|
|
160
189
|
</div>
|
|
161
190
|
{:else}
|
|
162
|
-
<p class="bridge-step-desc">Enter your email and we'll send you a link to reset your password.</p>
|
|
163
191
|
<form onsubmit={(e) => { e.preventDefault(); handleSendLink(); }}>
|
|
164
192
|
<div class="bridge-form-group">
|
|
165
|
-
<label for="reset-email">
|
|
193
|
+
<label for="reset-email">{t('field.email')}</label>
|
|
166
194
|
<input
|
|
167
195
|
id="reset-email"
|
|
168
196
|
type="email"
|
|
169
|
-
placeholder=
|
|
197
|
+
placeholder={t('placeholder.email')}
|
|
170
198
|
required
|
|
171
199
|
bind:value={email}
|
|
172
200
|
disabled={loading}
|
|
173
201
|
/>
|
|
174
202
|
</div>
|
|
175
203
|
<button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !email.trim()}>
|
|
176
|
-
{#if loading}<Spinner size={16} />{:else}
|
|
204
|
+
{#if loading}<Spinner size={16} />{:else}{t('forgot.submit')}{/if}
|
|
177
205
|
</button>
|
|
178
206
|
</form>
|
|
179
207
|
<div class="bridge-form-footer">
|
|
180
|
-
<a href={loginHref}>
|
|
208
|
+
<a href={loginHref}>{t('action.backToLogin')}</a>
|
|
181
209
|
</div>
|
|
182
210
|
{/if}
|
|
183
211
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
2
3
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
4
|
token?: string;
|
|
4
5
|
onComplete?: () => void;
|
|
@@ -9,6 +10,14 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
|
9
10
|
* Pass `null`/`''` to render no heading and use your own page title.
|
|
10
11
|
*/
|
|
11
12
|
heading?: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Step description. Pass `null`/`''` to render nothing and use your own
|
|
15
|
+
* subtitle (TBP-631). Only ever shown on the send-link step; the set-password
|
|
16
|
+
* and success states carry none.
|
|
17
|
+
*/
|
|
18
|
+
description?: string | null;
|
|
19
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
20
|
+
messages?: MessageOverrides;
|
|
12
21
|
}
|
|
13
22
|
declare const ForgotPassword: import("svelte").Component<Props, {}, "">;
|
|
14
23
|
type ForgotPassword = ReturnType<typeof ForgotPassword>;
|