@yackey-labs/yauth-ui-solidjs 0.1.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 ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@yackey-labs/yauth-ui-solidjs",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "vite build",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "bun test --conditions browser"
23
+ },
24
+ "dependencies": {
25
+ "@simplewebauthn/browser": "^13.0.0",
26
+ "@yackey-labs/yauth-client": "0.1.0",
27
+ "@yackey-labs/yauth-shared": "0.1.0",
28
+ "solid-js": "^1.9.5"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.7.2",
32
+ "vite": "^7.3.1",
33
+ "vite-plugin-solid": "^2.11.10"
34
+ }
35
+ }
@@ -0,0 +1,140 @@
1
+ import { type Component, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface ChangePasswordFormProps {
6
+ onSuccess?: () => void;
7
+ onError?: (error: Error) => void;
8
+ }
9
+
10
+ export const ChangePasswordForm: Component<ChangePasswordFormProps> = (
11
+ props,
12
+ ) => {
13
+ const { client } = useYAuth();
14
+ const [currentPassword, setCurrentPassword] = createSignal("");
15
+ const [newPassword, setNewPassword] = createSignal("");
16
+ const [confirmPassword, setConfirmPassword] = createSignal("");
17
+ const [error, setError] = createSignal<string | null>(null);
18
+ const [success, setSuccess] = createSignal(false);
19
+ const [loading, setLoading] = createSignal(false);
20
+
21
+ const handleSubmit = async (e: SubmitEvent) => {
22
+ e.preventDefault();
23
+ setError(null);
24
+ setSuccess(false);
25
+
26
+ const form = e.currentTarget as HTMLFormElement;
27
+ const formData = new FormData(form);
28
+ const currentPw =
29
+ (formData.get("current_password") as string) || currentPassword();
30
+ const newPw = (formData.get("new_password") as string) || newPassword();
31
+ const confirmPw =
32
+ (formData.get("confirm_password") as string) || confirmPassword();
33
+
34
+ if (newPw !== confirmPw) {
35
+ setError("Passwords do not match");
36
+ return;
37
+ }
38
+
39
+ setLoading(true);
40
+
41
+ try {
42
+ await client.emailPassword.changePassword(currentPw, newPw);
43
+ setSuccess(true);
44
+ setCurrentPassword("");
45
+ setNewPassword("");
46
+ setConfirmPassword("");
47
+ props.onSuccess?.();
48
+ } catch (err) {
49
+ const error = err instanceof Error ? err : new Error(String(err));
50
+ setError(error.message);
51
+ props.onError?.(error);
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ return (
58
+ <form class="space-y-4" onSubmit={handleSubmit}>
59
+ <Show when={error()}>
60
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
61
+ {error()}
62
+ </div>
63
+ </Show>
64
+
65
+ <Show when={success()}>
66
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
67
+ Password changed successfully.
68
+ </div>
69
+ </Show>
70
+
71
+ <div class="space-y-2">
72
+ <label
73
+ class="text-sm font-medium leading-none"
74
+ for="yauth-current-password"
75
+ >
76
+ Current password
77
+ </label>
78
+ <input
79
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
80
+ id="yauth-current-password"
81
+ name="current_password"
82
+ type="password"
83
+ value={currentPassword()}
84
+ onInput={(e) => setCurrentPassword(e.currentTarget.value)}
85
+ required
86
+ autocomplete="current-password"
87
+ disabled={loading()}
88
+ />
89
+ </div>
90
+
91
+ <div class="space-y-2">
92
+ <label
93
+ class="text-sm font-medium leading-none"
94
+ for="yauth-new-password"
95
+ >
96
+ New password
97
+ </label>
98
+ <input
99
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
100
+ id="yauth-new-password"
101
+ name="new_password"
102
+ type="password"
103
+ value={newPassword()}
104
+ onInput={(e) => setNewPassword(e.currentTarget.value)}
105
+ required
106
+ autocomplete="new-password"
107
+ disabled={loading()}
108
+ />
109
+ </div>
110
+
111
+ <div class="space-y-2">
112
+ <label
113
+ class="text-sm font-medium leading-none"
114
+ for="yauth-confirm-password"
115
+ >
116
+ Confirm new password
117
+ </label>
118
+ <input
119
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
120
+ id="yauth-confirm-password"
121
+ name="confirm_password"
122
+ type="password"
123
+ value={confirmPassword()}
124
+ onInput={(e) => setConfirmPassword(e.currentTarget.value)}
125
+ required
126
+ autocomplete="new-password"
127
+ disabled={loading()}
128
+ />
129
+ </div>
130
+
131
+ <button
132
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
133
+ type="submit"
134
+ disabled={loading()}
135
+ >
136
+ {loading() ? "Changing password..." : "Change password"}
137
+ </button>
138
+ </form>
139
+ );
140
+ };
@@ -0,0 +1,152 @@
1
+ import { type Component, createSignal, For } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+
4
+ export interface ConsentScreenProps {
5
+ clientName?: string;
6
+ clientId: string;
7
+ scopes?: string[];
8
+ redirectUri: string;
9
+ responseType: string;
10
+ codeChallenge: string;
11
+ codeChallengeMethod: string;
12
+ state?: string;
13
+ /** Called when user submits their consent decision */
14
+ onSubmit?: (approved: boolean) => void;
15
+ /** Called on error */
16
+ onError?: (error: Error) => void;
17
+ /** Auth API base URL (e.g. "/api/auth") */
18
+ authBaseUrl?: string;
19
+ }
20
+
21
+ export const ConsentScreen: Component<ConsentScreenProps> = (props) => {
22
+ const [loading, setLoading] = createSignal(false);
23
+ const [error, setError] = createSignal<string | null>(null);
24
+
25
+ const handleDecision = async (approved: boolean) => {
26
+ setError(null);
27
+ setLoading(true);
28
+
29
+ try {
30
+ const baseUrl = props.authBaseUrl ?? "/api/auth";
31
+ const response = await fetch(`${baseUrl}/authorize`, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ credentials: "include",
35
+ body: JSON.stringify({
36
+ client_id: props.clientId,
37
+ redirect_uri: props.redirectUri,
38
+ response_type: props.responseType,
39
+ code_challenge: props.codeChallenge,
40
+ code_challenge_method: props.codeChallengeMethod,
41
+ scope: props.scopes?.join(" "),
42
+ state: props.state,
43
+ approved,
44
+ }),
45
+ });
46
+
47
+ if (response.redirected) {
48
+ window.location.href = response.url;
49
+ return;
50
+ }
51
+
52
+ if (!response.ok) {
53
+ const body = await response.json().catch(() => null);
54
+ throw new Error(
55
+ body?.error_description ?? body?.error ?? "Authorization failed",
56
+ );
57
+ }
58
+
59
+ // If the response contains a redirect URL in the Location header
60
+ const location = response.headers.get("Location");
61
+ if (location) {
62
+ window.location.href = location;
63
+ return;
64
+ }
65
+
66
+ props.onSubmit?.(approved);
67
+ } catch (err) {
68
+ const error = err instanceof Error ? err : new Error(String(err));
69
+ setError(error.message);
70
+ props.onError?.(error);
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ };
75
+
76
+ const displayName = () => props.clientName ?? props.clientId;
77
+
78
+ return (
79
+ <div class="mx-auto max-w-md space-y-6 p-6">
80
+ <div class="space-y-2 text-center">
81
+ <h2 class="text-2xl font-semibold tracking-tight">
82
+ Authorize {displayName()}
83
+ </h2>
84
+ <p class="text-sm text-muted-foreground">
85
+ <strong>{displayName()}</strong> is requesting access to your account.
86
+ </p>
87
+ </div>
88
+
89
+ <Show when={error()}>
90
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
91
+ {error()}
92
+ </div>
93
+ </Show>
94
+
95
+ <Show when={props.scopes && props.scopes.length > 0}>
96
+ <div class="rounded-md border p-4 space-y-3">
97
+ <p class="text-sm font-medium">
98
+ This application is requesting the following permissions:
99
+ </p>
100
+ <ul class="space-y-2">
101
+ <For each={props.scopes}>
102
+ {(scope) => (
103
+ <li class="flex items-center gap-2 text-sm">
104
+ <svg
105
+ class="h-4 w-4 text-primary"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ viewBox="0 0 24 24"
109
+ aria-label="Checkmark"
110
+ role="img"
111
+ >
112
+ <path
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ stroke-width="2"
116
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
117
+ />
118
+ </svg>
119
+ <span>{scope}</span>
120
+ </li>
121
+ )}
122
+ </For>
123
+ </ul>
124
+ </div>
125
+ </Show>
126
+
127
+ <div class="flex gap-3">
128
+ <button
129
+ class="inline-flex h-9 flex-1 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
130
+ type="button"
131
+ disabled={loading()}
132
+ onClick={() => handleDecision(false)}
133
+ >
134
+ Deny
135
+ </button>
136
+ <button
137
+ class="inline-flex h-9 flex-1 cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
138
+ type="button"
139
+ disabled={loading()}
140
+ onClick={() => handleDecision(true)}
141
+ >
142
+ {loading() ? "Authorizing..." : "Authorize"}
143
+ </button>
144
+ </div>
145
+
146
+ <p class="text-center text-xs text-muted-foreground">
147
+ By authorizing, you allow this application to access your account with
148
+ the permissions listed above.
149
+ </p>
150
+ </div>
151
+ );
152
+ };
@@ -0,0 +1,83 @@
1
+ import { type Component, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface ForgotPasswordFormProps {
6
+ onSuccess?: (message: string) => void;
7
+ }
8
+
9
+ export const ForgotPasswordForm: Component<ForgotPasswordFormProps> = (
10
+ props,
11
+ ) => {
12
+ const { client } = useYAuth();
13
+ const [email, setEmail] = createSignal("");
14
+ const [error, setError] = createSignal<string | null>(null);
15
+ const [success, setSuccess] = createSignal<string | null>(null);
16
+ const [loading, setLoading] = createSignal(false);
17
+
18
+ const handleSubmit = async (e: SubmitEvent) => {
19
+ e.preventDefault();
20
+ setError(null);
21
+ setSuccess(null);
22
+ setLoading(true);
23
+
24
+ try {
25
+ const form = e.currentTarget as HTMLFormElement;
26
+ const formData = new FormData(form);
27
+ const result = await client.emailPassword.forgotPassword(
28
+ (formData.get("email") as string) || email(),
29
+ );
30
+ setSuccess(result.message);
31
+ props.onSuccess?.(result.message);
32
+ } catch (err) {
33
+ const error = err instanceof Error ? err : new Error(String(err));
34
+ setError(error.message);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <form class="space-y-4" onSubmit={handleSubmit}>
42
+ <Show when={error()}>
43
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
44
+ {error()}
45
+ </div>
46
+ </Show>
47
+
48
+ <Show when={success()}>
49
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
50
+ {success()}
51
+ </div>
52
+ </Show>
53
+
54
+ <div class="space-y-2">
55
+ <label
56
+ class="text-sm font-medium leading-none"
57
+ for="yauth-forgot-password-email"
58
+ >
59
+ Email
60
+ </label>
61
+ <input
62
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
63
+ id="yauth-forgot-password-email"
64
+ name="email"
65
+ type="email"
66
+ value={email()}
67
+ onInput={(e) => setEmail(e.currentTarget.value)}
68
+ required
69
+ autocomplete="email"
70
+ disabled={loading()}
71
+ />
72
+ </div>
73
+
74
+ <button
75
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
76
+ type="submit"
77
+ disabled={loading()}
78
+ >
79
+ {loading() ? "Sending..." : "Send reset link"}
80
+ </button>
81
+ </form>
82
+ );
83
+ };
@@ -0,0 +1,128 @@
1
+ import type { AuthUser } from "@yackey-labs/yauth-shared";
2
+ import { type Component, createSignal } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+ import { useYAuth } from "../provider";
5
+ import { PasskeyButton } from "./passkey-button";
6
+
7
+ export interface LoginFormProps {
8
+ onSuccess?: (user: AuthUser) => void;
9
+ onMfaRequired?: (pendingSessionId: string) => void;
10
+ onError?: (error: Error) => void;
11
+ showPasskey?: boolean;
12
+ }
13
+
14
+ export const LoginForm: Component<LoginFormProps> = (props) => {
15
+ const { client, refetch } = useYAuth();
16
+ const [email, setEmail] = createSignal("");
17
+ const [password, setPassword] = createSignal("");
18
+ const [error, setError] = createSignal<string | null>(null);
19
+ const [loading, setLoading] = createSignal(false);
20
+
21
+ const handleSubmit = async (e: SubmitEvent) => {
22
+ e.preventDefault();
23
+ setError(null);
24
+ setLoading(true);
25
+
26
+ try {
27
+ const form = e.currentTarget as HTMLFormElement;
28
+ const formData = new FormData(form);
29
+ const result = await client.emailPassword.login({
30
+ email: (formData.get("email") as string) || email(),
31
+ password: (formData.get("password") as string) || password(),
32
+ });
33
+
34
+ if ("mfa_required" in result && result.mfa_required) {
35
+ props.onMfaRequired?.(result.pending_session_id);
36
+ } else {
37
+ // Session cookie is set — refetch and await so reactive store updates
38
+ const user = await refetch();
39
+ props.onSuccess?.(user!);
40
+ }
41
+ } catch (err) {
42
+ const error = err instanceof Error ? err : new Error(String(err));
43
+ setError(error.message);
44
+ props.onError?.(error);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <form class="space-y-6" on:submit={handleSubmit}>
52
+ <Show when={error()}>
53
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
54
+ {error()}
55
+ </div>
56
+ </Show>
57
+
58
+ <div class="space-y-2">
59
+ <label class="text-sm font-medium leading-none" for="yauth-login-email">
60
+ Email
61
+ </label>
62
+ <input
63
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
64
+ id="yauth-login-email"
65
+ name="email"
66
+ type="email"
67
+ value={email()}
68
+ on:input={(e) => setEmail(e.currentTarget.value)}
69
+ required
70
+ autocomplete="email"
71
+ disabled={loading()}
72
+ />
73
+ </div>
74
+
75
+ <div class="space-y-2">
76
+ <label
77
+ class="text-sm font-medium leading-none"
78
+ for="yauth-login-password"
79
+ >
80
+ Password
81
+ </label>
82
+ <input
83
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
84
+ id="yauth-login-password"
85
+ name="password"
86
+ type="password"
87
+ value={password()}
88
+ on:input={(e) => setPassword(e.currentTarget.value)}
89
+ required
90
+ autocomplete="current-password"
91
+ disabled={loading()}
92
+ />
93
+ </div>
94
+
95
+ <button
96
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
97
+ type="submit"
98
+ disabled={loading()}
99
+ >
100
+ {loading() ? "Signing in..." : "Sign in"}
101
+ </button>
102
+
103
+ <Show when={props.showPasskey}>
104
+ <div class="relative">
105
+ <div
106
+ class="absolute inset-0 flex items-center"
107
+ style="pointer-events:none"
108
+ >
109
+ <span class="w-full border-t" />
110
+ </div>
111
+ <div class="relative flex justify-center text-xs uppercase">
112
+ <span class="bg-background px-2 text-muted-foreground">or</span>
113
+ </div>
114
+ </div>
115
+
116
+ <PasskeyButton
117
+ mode="login"
118
+ email={email()}
119
+ onSuccess={(user) => {
120
+ void refetch();
121
+ props.onSuccess?.(user);
122
+ }}
123
+ onError={props.onError}
124
+ />
125
+ </Show>
126
+ </form>
127
+ );
128
+ };
@@ -0,0 +1,84 @@
1
+ import { type Component, createSignal } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface MagicLinkFormProps {
6
+ onSuccess?: (message: string) => void;
7
+ }
8
+
9
+ export const MagicLinkForm: Component<MagicLinkFormProps> = (props) => {
10
+ const { client } = useYAuth();
11
+ const [email, setEmail] = createSignal("");
12
+ const [error, setError] = createSignal<string | null>(null);
13
+ const [success, setSuccess] = createSignal<string | null>(null);
14
+ const [loading, setLoading] = createSignal(false);
15
+
16
+ const handleSubmit = async (e: SubmitEvent) => {
17
+ e.preventDefault();
18
+ setError(null);
19
+ setSuccess(null);
20
+ setLoading(true);
21
+
22
+ try {
23
+ const form = e.currentTarget as HTMLFormElement;
24
+ const formData = new FormData(form);
25
+ const result = await client.magicLink.send(
26
+ (formData.get("email") as string) || email(),
27
+ );
28
+ setSuccess(result.message);
29
+ props.onSuccess?.(result.message);
30
+ } catch (err) {
31
+ const error = err instanceof Error ? err : new Error(String(err));
32
+ setError(error.message);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <form class="space-y-4" onSubmit={handleSubmit}>
40
+ <Show when={error()}>
41
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
42
+ {error()}
43
+ </div>
44
+ </Show>
45
+
46
+ <Show when={success()}>
47
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
48
+ {success()}
49
+ </div>
50
+ </Show>
51
+
52
+ <Show when={!success()}>
53
+ <div class="space-y-2">
54
+ <label
55
+ class="text-sm font-medium leading-none"
56
+ for="yauth-magic-link-email"
57
+ >
58
+ Email
59
+ </label>
60
+ <input
61
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
62
+ id="yauth-magic-link-email"
63
+ name="email"
64
+ type="email"
65
+ value={email()}
66
+ onInput={(e) => setEmail(e.currentTarget.value)}
67
+ required
68
+ autocomplete="email"
69
+ disabled={loading()}
70
+ placeholder="you@example.com"
71
+ />
72
+ </div>
73
+
74
+ <button
75
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
76
+ type="submit"
77
+ disabled={loading()}
78
+ >
79
+ {loading() ? "Sending..." : "Send magic link"}
80
+ </button>
81
+ </Show>
82
+ </form>
83
+ );
84
+ };