@seamless-auth/svelte 0.0.0-stage → 0.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/README.md +253 -2
  4. package/dist/auth.svelte.d.ts +170 -0
  5. package/dist/auth.svelte.js +208 -0
  6. package/dist/config.d.ts +49 -0
  7. package/dist/config.js +6 -0
  8. package/dist/context.d.ts +13 -0
  9. package/dist/context.js +35 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.js +21 -0
  12. package/dist/kit/index.d.ts +55 -0
  13. package/dist/kit/index.js +121 -0
  14. package/dist/kit/watch.svelte.d.ts +10 -0
  15. package/dist/kit/watch.svelte.js +28 -0
  16. package/dist/navigation.d.ts +40 -0
  17. package/dist/navigation.js +22 -0
  18. package/dist/screens/AuthLayout.svelte +38 -0
  19. package/dist/screens/AuthLayout.svelte.d.ts +8 -0
  20. package/dist/screens/FallbackOptions.svelte +69 -0
  21. package/dist/screens/FallbackOptions.svelte.d.ts +13 -0
  22. package/dist/screens/Login.svelte +253 -0
  23. package/dist/screens/Login.svelte.d.ts +3 -0
  24. package/dist/screens/MagicLinkSent.svelte +74 -0
  25. package/dist/screens/MagicLinkSent.svelte.d.ts +3 -0
  26. package/dist/screens/OAuthCallback.svelte +69 -0
  27. package/dist/screens/OAuthCallback.svelte.d.ts +3 -0
  28. package/dist/screens/OAuthProviderButtons.svelte +49 -0
  29. package/dist/screens/OAuthProviderButtons.svelte.d.ts +6 -0
  30. package/dist/screens/OtpInput.svelte +101 -0
  31. package/dist/screens/OtpInput.svelte.d.ts +10 -0
  32. package/dist/screens/PasskeyLogin.svelte +34 -0
  33. package/dist/screens/PasskeyLogin.svelte.d.ts +3 -0
  34. package/dist/screens/RegisterPasskey.svelte +110 -0
  35. package/dist/screens/RegisterPasskey.svelte.d.ts +3 -0
  36. package/dist/screens/VerifyMagicLink.svelte +195 -0
  37. package/dist/screens/VerifyMagicLink.svelte.d.ts +3 -0
  38. package/dist/screens/VerifyOtp.svelte +141 -0
  39. package/dist/screens/VerifyOtp.svelte.d.ts +7 -0
  40. package/dist/seamless-auth.css +260 -0
  41. package/dist/styles.generated.d.ts +2 -0
  42. package/dist/styles.generated.js +3 -0
  43. package/package.json +76 -4
@@ -0,0 +1,38 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!--
7
+ The frame every bundled screen renders in. Its stylesheet is unscoped, so an
8
+ application themes the .sa-* classes and --seamless-* properties like any
9
+ other global style.
10
+ -->
11
+ <script lang="ts">
12
+ import { onMount, type Snippet } from 'svelte';
13
+
14
+ import { getSeamlessAuth } from '../context.js';
15
+ import styles from '../styles.generated.js';
16
+
17
+ let { card = true, children }: { card?: boolean; children: Snippet } = $props();
18
+
19
+ const STYLE_ID = 'seamless-auth-styles';
20
+ const { styles: options } = getSeamlessAuth();
21
+
22
+ onMount(() => {
23
+ if (!options.inject || document.getElementById(STYLE_ID)) return;
24
+
25
+ const style = document.createElement('style');
26
+ style.id = STYLE_ID;
27
+ if (options.nonce) style.nonce = options.nonce;
28
+ style.textContent = styles;
29
+ // First in <head>, so the application's own stylesheets override it.
30
+ document.head.prepend(style);
31
+ });
32
+ </script>
33
+
34
+ <div class="sa-auth">
35
+ <div class={card ? 'sa-card' : 'sa-center'}>
36
+ {@render children()}
37
+ </div>
38
+ </div>
@@ -0,0 +1,8 @@
1
+ import { type Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ card?: boolean;
4
+ children: Snippet;
5
+ };
6
+ declare const AuthLayout: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type AuthLayout = ReturnType<typeof AuthLayout>;
8
+ export default AuthLayout;
@@ -0,0 +1,69 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!-- The other ways to sign in, offered when a passkey is not the way in. -->
7
+ <script lang="ts">
8
+ import {
9
+ fallbackSignInOptions,
10
+ hasFallbackSignInOption,
11
+ type LoginMethod,
12
+ } from '@seamless-auth/client';
13
+
14
+ let {
15
+ identifier,
16
+ loginMethods = null,
17
+ onMagicLink,
18
+ onEmailOtp,
19
+ onPhoneOtp,
20
+ onPasskeyRetry,
21
+ }: {
22
+ identifier: string;
23
+ /** Null while the methods are unknown; the options then stay permissive. */
24
+ loginMethods?: LoginMethod[] | null;
25
+ onMagicLink: () => void;
26
+ onEmailOtp?: () => void;
27
+ onPhoneOtp: () => void;
28
+ onPasskeyRetry?: () => void;
29
+ } = $props();
30
+
31
+ const options = $derived(
32
+ fallbackSignInOptions(identifier, loginMethods, {
33
+ emailOtp: Boolean(onEmailOtp),
34
+ passkeyRetry: Boolean(onPasskeyRetry),
35
+ })
36
+ );
37
+ </script>
38
+
39
+ {#if hasFallbackSignInOption(options)}
40
+ <div class="sa-fallback">
41
+ <div class="sa-fallback-header">Choose a sign-in method</div>
42
+ <p class="sa-fallback-description">Choose another secure sign-in method.</p>
43
+
44
+ <div class="sa-actions">
45
+ {#if options.magicLink}
46
+ <button type="button" class="sa-action" onclick={onMagicLink}>
47
+ <span class="sa-action-title">Email Magic Link</span>
48
+ <span class="sa-action-subtext">Send a secure sign-in link to your email</span>
49
+ </button>
50
+ {/if}
51
+ {#if options.emailOtp}
52
+ <button type="button" class="sa-action" onclick={onEmailOtp}>
53
+ <span class="sa-action-title">Email Code</span>
54
+ <span class="sa-action-subtext">Receive a one-time code by email</span>
55
+ </button>
56
+ {/if}
57
+ {#if options.phoneOtp}
58
+ <button type="button" class="sa-action" onclick={onPhoneOtp}>
59
+ <span class="sa-action-title">Text Message Code</span>
60
+ <span class="sa-action-subtext">Receive a one-time code via SMS</span>
61
+ </button>
62
+ {/if}
63
+ </div>
64
+
65
+ {#if options.passkeyRetry}
66
+ <button type="button" class="sa-link" onclick={onPasskeyRetry}>Try passkey anyway</button>
67
+ {/if}
68
+ </div>
69
+ {/if}
@@ -0,0 +1,13 @@
1
+ import { type LoginMethod } from '@seamless-auth/client';
2
+ type $$ComponentProps = {
3
+ identifier: string;
4
+ /** Null while the methods are unknown; the options then stay permissive. */
5
+ loginMethods?: LoginMethod[] | null;
6
+ onMagicLink: () => void;
7
+ onEmailOtp?: () => void;
8
+ onPhoneOtp: () => void;
9
+ onPasskeyRetry?: () => void;
10
+ };
11
+ declare const FallbackOptions: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type FallbackOptions = ReturnType<typeof FallbackOptions>;
13
+ export default FallbackOptions;
@@ -0,0 +1,253 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!-- Sign in with an email or phone number, or create an account with an email. -->
7
+ <script lang="ts">
8
+ import {
9
+ beginSignIn,
10
+ isValidEmail,
11
+ isValidPhoneNumber,
12
+ PASSKEY_SIGN_IN_FAILED,
13
+ registerWithEmail,
14
+ type LoginMethod,
15
+ } from '@seamless-auth/client';
16
+ import { onMount } from 'svelte';
17
+
18
+ import { getAuthNavigator, getSeamlessAuth } from '../context.js';
19
+ import AuthLayout from './AuthLayout.svelte';
20
+ import FallbackOptions from './FallbackOptions.svelte';
21
+ import OAuthProviderButtons from './OAuthProviderButtons.svelte';
22
+
23
+ const auth = getSeamlessAuth();
24
+ const navigator = getAuthNavigator();
25
+
26
+ // Opens on Sign In for a browser that has signed in before.
27
+ let chosenMode = $state<'login' | 'register' | null>(null);
28
+ const mode = $derived(chosenMode ?? (auth.hasSignedInBefore ? 'login' : 'register'));
29
+
30
+ let identifier = $state('');
31
+ let email = $state('');
32
+ let identifierError = $state('');
33
+ let emailError = $state('');
34
+ let formError = $state('');
35
+ let showFallbackOptions = $state(false);
36
+ let loginMethods = $state.raw<LoginMethod[] | null>(null);
37
+ let submitting = $state(false);
38
+
39
+ const canSubmit = $derived(
40
+ mode === 'login'
41
+ ? isValidEmail(identifier) || isValidPhoneNumber(identifier)
42
+ : // Registration only needs a valid email. A phone can be added later.
43
+ isValidEmail(email)
44
+ );
45
+
46
+ // A disabled button is skipped by screen readers and explains nothing to
47
+ // anyone else, so the reason it is refusing lives in a live region instead.
48
+ const submitHint = $derived.by(() => {
49
+ if (mode === 'login') {
50
+ if (!identifier) return 'Enter your email or phone number to continue.';
51
+ return canSubmit
52
+ ? 'Ready to continue.'
53
+ : 'This does not look like a complete email or phone number yet.';
54
+ }
55
+
56
+ if (!email) return 'Enter your email address to continue.';
57
+ return canSubmit
58
+ ? 'Ready to continue.'
59
+ : 'This does not look like a complete email address yet.';
60
+ });
61
+
62
+ onMount(() => {
63
+ void auth.loadLoginMethods();
64
+ void auth.checkPasskeySupport();
65
+ });
66
+
67
+ function validateIdentifier() {
68
+ if (!identifier) return;
69
+ identifierError =
70
+ isValidEmail(identifier) || isValidPhoneNumber(identifier)
71
+ ? ''
72
+ : 'Please enter a valid email or phone number';
73
+ }
74
+
75
+ function validateEmail() {
76
+ if (!email) return;
77
+ emailError = isValidEmail(email) ? '' : 'Please enter a valid email';
78
+ }
79
+
80
+ async function submit(event: SubmitEvent) {
81
+ event.preventDefault();
82
+ if (!canSubmit || submitting) return;
83
+
84
+ formError = '';
85
+ showFallbackOptions = false;
86
+ submitting = true;
87
+
88
+ try {
89
+ if (mode === 'register') {
90
+ const { error } = await registerWithEmail(auth.client, email);
91
+
92
+ if (error) {
93
+ formError = error;
94
+ return;
95
+ }
96
+
97
+ await navigator.toScreen('verifyEmailOtp');
98
+ return;
99
+ }
100
+
101
+ // Both are cached, so awaiting them costs nothing once loaded and removes
102
+ // the race where a fast submit saw passkeys as unsupported.
103
+ const [passkeySupported, configuredMethods] = await Promise.all([
104
+ auth.checkPasskeySupport(),
105
+ auth.loadLoginMethods(),
106
+ ]);
107
+
108
+ const step = await beginSignIn(auth, { identifier, passkeySupported, configuredMethods });
109
+
110
+ if (step.kind === 'error') {
111
+ formError = step.message;
112
+ return;
113
+ }
114
+
115
+ if (step.kind === 'signed_in') {
116
+ await navigator.toApp();
117
+ return;
118
+ }
119
+
120
+ loginMethods = step.loginMethods;
121
+ showFallbackOptions = true;
122
+
123
+ if (step.passkeyFailed) formError = PASSKEY_SIGN_IN_FAILED;
124
+ } catch {
125
+ // Backstop for unexpected errors only. The client reports request
126
+ // failures through `error`, not by throwing.
127
+ formError = 'Failed to continue sign-in. Please try again.';
128
+ } finally {
129
+ submitting = false;
130
+ }
131
+ }
132
+
133
+ async function retryPasskey() {
134
+ formError = '';
135
+ const { error } = await auth.handlePasskeyLogin();
136
+
137
+ if (error) {
138
+ formError = PASSKEY_SIGN_IN_FAILED;
139
+ return;
140
+ }
141
+
142
+ await navigator.toApp();
143
+ }
144
+
145
+ async function sendMagicLink() {
146
+ const { error } = await auth.client.requestMagicLink();
147
+
148
+ if (error) {
149
+ formError = 'Failed to send magic link.';
150
+ return;
151
+ }
152
+
153
+ await navigator.toScreen('magicLinkSent', { identifier });
154
+ }
155
+
156
+ async function sendEmailOtp() {
157
+ const { error } = await auth.client.requestLoginEmailOtp();
158
+
159
+ if (error) {
160
+ formError = 'Failed to send email code.';
161
+ return;
162
+ }
163
+
164
+ await navigator.toScreen('verifyEmailOtp', { flow: 'login' });
165
+ }
166
+
167
+ async function sendPhoneOtp() {
168
+ const { error } = await auth.client.requestLoginPhoneOtp();
169
+
170
+ if (error) {
171
+ formError = 'Failed to send OTP.';
172
+ return;
173
+ }
174
+
175
+ await navigator.toScreen('verifyPhoneOtp', { flow: 'login' });
176
+ }
177
+ </script>
178
+
179
+ <AuthLayout>
180
+ <h2 class="sa-heading">{mode === 'login' ? 'Sign In' : 'Create Account'}</h2>
181
+
182
+ <form onsubmit={submit}>
183
+ {#if mode === 'login'}
184
+ <div class="sa-input-group">
185
+ <label for="identifier" class="sa-label">Email Address / Phone Number</label>
186
+ <input
187
+ id="identifier"
188
+ type="text"
189
+ class="sa-input"
190
+ autocomplete="off"
191
+ placeholder="Email or Phone Number"
192
+ required
193
+ bind:value={identifier}
194
+ onblur={validateIdentifier}
195
+ />
196
+ <p class="sa-helper-text">Phone numbers must include a country code e.g. +1</p>
197
+
198
+ {#if showFallbackOptions}
199
+ <FallbackOptions
200
+ {identifier}
201
+ {loginMethods}
202
+ onMagicLink={sendMagicLink}
203
+ onEmailOtp={sendEmailOtp}
204
+ onPhoneOtp={sendPhoneOtp}
205
+ onPasskeyRetry={auth.passkeySupported ? retryPasskey : undefined}
206
+ />
207
+ {/if}
208
+
209
+ {#if identifierError}
210
+ <p class="sa-error">{identifierError}</p>
211
+ {/if}
212
+ </div>
213
+ {:else}
214
+ <div class="sa-input-group">
215
+ <label for="email" class="sa-label">Email Address</label>
216
+ <input
217
+ id="email"
218
+ type="email"
219
+ class="sa-input"
220
+ autocomplete="off"
221
+ required
222
+ bind:value={email}
223
+ onblur={validateEmail}
224
+ />
225
+ {#if emailError}
226
+ <p class="sa-error">{emailError}</p>
227
+ {/if}
228
+ </div>
229
+ {/if}
230
+
231
+ <button
232
+ type="submit"
233
+ class="sa-button"
234
+ disabled={!canSubmit || submitting}
235
+ aria-describedby="seamless-submit-hint"
236
+ >
237
+ {mode === 'login' ? 'Login' : 'Register'}
238
+ </button>
239
+ <p id="seamless-submit-hint" role="status" class="sa-submit-hint">{submitHint}</p>
240
+ {#if formError}
241
+ <p class="sa-error">{formError}</p>
242
+ {/if}
243
+ <button
244
+ type="button"
245
+ class="sa-link"
246
+ onclick={() => (chosenMode = mode === 'login' ? 'register' : 'login')}
247
+ >
248
+ {mode === 'login' ? "Don't have an account? Create one" : 'Already have an account? Sign in'}
249
+ </button>
250
+ </form>
251
+
252
+ <OAuthProviderButtons />
253
+ </AuthLayout>
@@ -0,0 +1,3 @@
1
+ declare const Login: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Login = ReturnType<typeof Login>;
3
+ export default Login;
@@ -0,0 +1,74 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!--
7
+ Waits for an emailed link to be used, then signs this tab in. A link opened in
8
+ another tab of this browser reports back at once; one opened on another device
9
+ is picked up by polling.
10
+ -->
11
+ <script lang="ts">
12
+ import { MAGIC_LINK_RESEND_COOLDOWN_SECONDS, watchMagicLink } from '@seamless-auth/client';
13
+ import { onMount } from 'svelte';
14
+
15
+ import { getAuthNavigator, getSeamlessAuth } from '../context.js';
16
+ import AuthLayout from './AuthLayout.svelte';
17
+
18
+ const auth = getSeamlessAuth();
19
+ const navigator = getAuthNavigator();
20
+ const identifier = navigator.state().identifier as string | undefined;
21
+ let cooldown = $state(MAGIC_LINK_RESEND_COOLDOWN_SECONDS);
22
+
23
+ // Browser only: a server render must not poll or open a channel.
24
+ onMount(() => {
25
+ const timer = setInterval(() => {
26
+ cooldown = Math.max(0, cooldown - 1);
27
+ }, 1000);
28
+
29
+ const stop = watchMagicLink({
30
+ client: auth.client,
31
+ refreshSession: auth.refreshSession,
32
+ onSignedIn: () => void navigator.toApp(),
33
+ });
34
+
35
+ return () => {
36
+ clearInterval(timer);
37
+ stop();
38
+ };
39
+ });
40
+
41
+ async function resend() {
42
+ if (cooldown > 0) return;
43
+ await auth.client.requestMagicLink();
44
+ cooldown = MAGIC_LINK_RESEND_COOLDOWN_SECONDS;
45
+ }
46
+ </script>
47
+
48
+ <AuthLayout>
49
+ <div class="sa-center">
50
+ <h2 class="sa-heading">Check your email</h2>
51
+ <p class="sa-description">
52
+ If an account exists for this address, we sent a secure sign-in link.
53
+ </p>
54
+ {#if identifier}
55
+ <div class="sa-identifier">{identifier}</div>
56
+ {/if}
57
+ <p class="sa-helper-text">Open the email and click the link to finish signing in.</p>
58
+ <p class="sa-helper-text">
59
+ Didn't receive anything? Check your spam folder or try creating a new account.
60
+ </p>
61
+ </div>
62
+
63
+ <div class="sa-actions">
64
+ <button type="button" class="sa-secondary" disabled={cooldown > 0} onclick={resend}>
65
+ Resend link
66
+ </button>
67
+ {#if cooldown > 0}
68
+ <div class="sa-helper-text sa-center">Available in {cooldown}s</div>
69
+ {/if}
70
+ <button type="button" class="sa-link" onclick={() => navigator.toScreen('login')}>
71
+ Change email or phone
72
+ </button>
73
+ </div>
74
+ </AuthLayout>
@@ -0,0 +1,3 @@
1
+ declare const MagicLinkSent: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type MagicLinkSent = ReturnType<typeof MagicLinkSent>;
3
+ export default MagicLinkSent;
@@ -0,0 +1,69 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!-- The OAuth redirect URI. Finishes a provider sign-in. -->
7
+ <script module lang="ts">
8
+ // Codes this page has already spent. A code can be exchanged once, and a screen
9
+ // that mounts again must not send it a second time.
10
+ const spent = new Set<string>();
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import { completeOAuthCallback } from '@seamless-auth/client';
15
+ import { onMount } from 'svelte';
16
+
17
+ import { getAuthNavigator, getSeamlessAuth } from '../context.js';
18
+ import AuthLayout from './AuthLayout.svelte';
19
+
20
+ const auth = getSeamlessAuth();
21
+ const navigator = getAuthNavigator();
22
+ let error = $state('');
23
+
24
+ // Browser only: the code is single use, and a server render has neither the
25
+ // browser's cookies nor a window.
26
+ onMount(() => {
27
+ void (async () => {
28
+ const params = new URLSearchParams();
29
+ for (const name of ['code', 'state']) {
30
+ const value = navigator.query(name);
31
+ if (value !== null) params.set(name, value);
32
+ }
33
+
34
+ const code = params.get('code');
35
+ if (code !== null && spent.has(code)) {
36
+ error = 'This sign-in link has already been used.';
37
+ return;
38
+ }
39
+ if (code !== null) spent.add(code);
40
+
41
+ // The code and state are read; keep them out of history and Referers.
42
+ void navigator.dropQuery().catch(() => undefined);
43
+
44
+ const outcome = await completeOAuthCallback(auth, params, window.location.origin);
45
+
46
+ if (outcome.kind === 'error') {
47
+ error = outcome.message;
48
+ return;
49
+ }
50
+
51
+ if (outcome.kind === 'enroll_passkey') {
52
+ await navigator.toScreen('registerPasskey', { returnTo: outcome.returnTo });
53
+ return;
54
+ }
55
+
56
+ await navigator.toLocation(outcome.destination);
57
+ })();
58
+ });
59
+ </script>
60
+
61
+ <AuthLayout card={false}>
62
+ <h2 class="sa-heading">{error ? 'Sign-in failed' : 'Completing sign-in...'}</h2>
63
+ {#if error}
64
+ <p>{error}</p>
65
+ <button type="button" class="sa-link" onclick={() => navigator.toScreen('login')}>
66
+ Back to login
67
+ </button>
68
+ {/if}
69
+ </AuthLayout>
@@ -0,0 +1,3 @@
1
+ declare const OAuthCallback: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type OAuthCallback = ReturnType<typeof OAuthCallback>;
3
+ export default OAuthCallback;
@@ -0,0 +1,49 @@
1
+ <!--
2
+ Copyright © 2026 Fells Code, LLC
3
+ Licensed under the Apache License, Version 2.0
4
+ See LICENSE file in the project root for full license information
5
+ -->
6
+ <!-- A button per OAuth provider the instance has enabled. Renders nothing without one. -->
7
+ <script lang="ts">
8
+ import { startOAuthSignIn, type OAuthProvider } from '@seamless-auth/client';
9
+ import { onMount } from 'svelte';
10
+
11
+ import { getAuthNavigator, getSeamlessAuth } from '../context.js';
12
+
13
+ /** The redirect URI to register with providers. Defaults to the bundled callback screen. */
14
+ let { redirectUri }: { redirectUri?: string } = $props();
15
+
16
+ const auth = getSeamlessAuth();
17
+ const navigator = getAuthNavigator();
18
+ let providers = $state.raw<OAuthProvider[]>([]);
19
+ let error = $state('');
20
+
21
+ onMount(async () => {
22
+ const { data } = await auth.listOAuthProviders();
23
+ providers = data?.providers ?? [];
24
+ });
25
+
26
+ async function select(providerId: string) {
27
+ error = '';
28
+
29
+ const result = await startOAuthSignIn(
30
+ { actions: auth, oauthRedirect: auth.ports.oauthRedirect },
31
+ { providerId, redirectUri: redirectUri ?? navigator.absoluteUrl('oauthCallback') }
32
+ );
33
+
34
+ if (result.error) error = result.error;
35
+ }
36
+ </script>
37
+
38
+ {#if providers.length > 0}
39
+ <div class="sa-actions">
40
+ {#each providers as provider (provider.id)}
41
+ <button type="button" class="sa-action" onclick={() => select(provider.id)}>
42
+ <span class="sa-action-title">Continue with {provider.name}</span>
43
+ </button>
44
+ {/each}
45
+ {#if error}
46
+ <p class="sa-error">{error}</p>
47
+ {/if}
48
+ </div>
49
+ {/if}
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ redirectUri?: string;
3
+ };
4
+ declare const OAuthProviderButtons: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type OAuthProviderButtons = ReturnType<typeof OAuthProviderButtons>;
6
+ export default OAuthProviderButtons;