jazz-tools 0.19.19 → 0.19.21
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/.svelte-kit/__package__/client.d.ts.map +1 -1
- package/.svelte-kit/__package__/client.js +3 -1
- package/.svelte-kit/__package__/server.d.ts.map +1 -1
- package/.svelte-kit/__package__/server.js +9 -7
- package/.svelte-kit/__package__/tests/client.test.js +48 -0
- package/.turbo/turbo-build.log +70 -66
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js +1 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +4 -4
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/database-adapter/index.js.map +1 -1
- package/dist/better-auth/database-adapter/repository/generic.d.ts +3 -3
- package/dist/better-auth/database-adapter/repository/session.d.ts +2 -2
- package/dist/better-auth/database-adapter/schema.d.ts +3 -3
- package/dist/better-auth/database-adapter/schema.d.ts.map +1 -1
- package/dist/{chunk-PEHQ7TN2.js → chunk-QCTQH5RS.js} +31 -4
- package/dist/chunk-QCTQH5RS.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/hooks.d.ts +1 -2
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.js +7 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +94 -3
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +130 -135
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStates.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStates.test.d.ts.map +1 -0
- package/dist/react-native/chunk-DGUM43GV.js +11 -0
- package/dist/react-native/chunk-DGUM43GV.js.map +1 -0
- package/dist/react-native/crypto.js +2 -0
- package/dist/react-native/crypto.js.map +1 -1
- package/dist/react-native/index.js +544 -29
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/auth/PasskeyAuth.d.ts +123 -0
- package/dist/react-native-core/auth/PasskeyAuth.d.ts.map +1 -0
- package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts +34 -0
- package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts.map +1 -0
- package/dist/react-native-core/auth/auth.d.ts +3 -0
- package/dist/react-native-core/auth/auth.d.ts.map +1 -1
- package/dist/react-native-core/auth/passkey-utils.d.ts +16 -0
- package/dist/react-native-core/auth/passkey-utils.d.ts.map +1 -0
- package/dist/react-native-core/auth/usePasskeyAuth.d.ts +48 -0
- package/dist/react-native-core/auth/usePasskeyAuth.d.ts.map +1 -0
- package/dist/react-native-core/chunk-DGUM43GV.js +11 -0
- package/dist/react-native-core/chunk-DGUM43GV.js.map +1 -0
- package/dist/react-native-core/crypto.js +2 -0
- package/dist/react-native-core/crypto.js.map +1 -1
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +539 -24
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/tests/PasskeyAuth.test.d.ts +2 -0
- package/dist/react-native-core/tests/PasskeyAuth.test.d.ts.map +1 -0
- package/dist/react-native-core/tests/passkey-utils.test.d.ts +2 -0
- package/dist/react-native-core/tests/passkey-utils.test.d.ts.map +1 -0
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts +38 -0
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/ClerkAuth.svelte.js +47 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts +67 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts +17 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/index.d.ts +2 -0
- package/dist/svelte/auth/index.d.ts.map +1 -1
- package/dist/svelte/auth/index.js +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.js +202 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts +8 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.d.ts +1 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -1
- package/dist/svelte/tests/testUtils.js +3 -1
- package/dist/testing.js +1 -1
- package/dist/tools/auth/clerk/index.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts.map +1 -1
- package/dist/tools/coValues/account.d.ts +5 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +30 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
- package/dist/tools/subscribe/types.d.ts +1 -1
- package/dist/tools/subscribe/types.d.ts.map +1 -1
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/better-auth/auth/client.ts +3 -1
- package/src/better-auth/auth/server.ts +9 -7
- package/src/better-auth/auth/tests/client.test.ts +66 -2
- package/src/better-auth/database-adapter/repository/generic.ts +3 -3
- package/src/better-auth/database-adapter/repository/session.ts +2 -2
- package/src/better-auth/database-adapter/schema.ts +5 -5
- package/src/react/hooks.tsx +4 -2
- package/src/react-core/hooks.ts +332 -178
- package/src/react-core/tests/useCoState.selector.test.ts +309 -22
- package/src/react-core/tests/useCoStates.test.tsx +414 -0
- package/src/react-native-core/auth/PasskeyAuth.ts +316 -0
- package/src/react-native-core/auth/PasskeyAuthBasicUI.tsx +284 -0
- package/src/react-native-core/auth/auth.ts +3 -0
- package/src/react-native-core/auth/passkey-utils.ts +47 -0
- package/src/react-native-core/auth/usePasskeyAuth.tsx +85 -0
- package/src/react-native-core/hooks.tsx +2 -0
- package/src/react-native-core/tests/PasskeyAuth.test.ts +463 -0
- package/src/react-native-core/tests/passkey-utils.test.ts +144 -0
- package/src/svelte/auth/ClerkAuth.svelte.ts +67 -0
- package/src/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/src/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/src/svelte/auth/index.ts +2 -0
- package/src/svelte/tests/ClerkAuth.svelte.test.ts +305 -0
- package/src/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/src/svelte/tests/testUtils.ts +4 -1
- package/src/tools/auth/clerk/types.ts +1 -1
- package/src/tools/coValues/account.ts +11 -3
- package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +27 -1
- package/src/tools/subscribe/types.ts +1 -1
- package/src/tools/tests/account.test.ts +2 -1
- package/src/tools/tests/inbox.test.ts +7 -7
- package/testSetup.ts +4 -0
- package/vitest.config.ts +1 -0
- package/dist/chunk-PEHQ7TN2.js.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { CryptoProvider, RawAccountID, cojsonInternals } from "cojson";
|
|
2
|
+
import {
|
|
3
|
+
Account,
|
|
4
|
+
AuthSecretStorage,
|
|
5
|
+
AuthenticateAccountFunction,
|
|
6
|
+
ID,
|
|
7
|
+
} from "jazz-tools";
|
|
8
|
+
import {
|
|
9
|
+
base64UrlToUint8Array,
|
|
10
|
+
uint8ArrayToBase64Url,
|
|
11
|
+
} from "./passkey-utils.js";
|
|
12
|
+
|
|
13
|
+
// Types for react-native-passkey library
|
|
14
|
+
// We define these here to avoid requiring the library as a direct dependency
|
|
15
|
+
interface PasskeyCreateRequest {
|
|
16
|
+
challenge: string;
|
|
17
|
+
rp: {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
user: {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
};
|
|
26
|
+
pubKeyCredParams: Array<{ alg: number; type: "public-key" }>;
|
|
27
|
+
authenticatorSelection?: {
|
|
28
|
+
authenticatorAttachment?: "platform" | "cross-platform";
|
|
29
|
+
requireResidentKey?: boolean;
|
|
30
|
+
residentKey?: "discouraged" | "preferred" | "required";
|
|
31
|
+
userVerification?: "discouraged" | "preferred" | "required";
|
|
32
|
+
};
|
|
33
|
+
timeout?: number;
|
|
34
|
+
attestation?: "none" | "indirect" | "direct" | "enterprise";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PasskeyGetRequest {
|
|
38
|
+
challenge: string;
|
|
39
|
+
rpId: string;
|
|
40
|
+
allowCredentials?: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
type: "public-key";
|
|
43
|
+
transports?: Array<"usb" | "nfc" | "ble" | "internal" | "hybrid">;
|
|
44
|
+
}>;
|
|
45
|
+
timeout?: number;
|
|
46
|
+
userVerification?: "discouraged" | "preferred" | "required";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PasskeyGetResult {
|
|
50
|
+
id: string;
|
|
51
|
+
rawId: string;
|
|
52
|
+
type: "public-key";
|
|
53
|
+
response: {
|
|
54
|
+
clientDataJSON: string;
|
|
55
|
+
authenticatorData: string;
|
|
56
|
+
signature: string;
|
|
57
|
+
userHandle: string | null;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Interface for the react-native-passkey module.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export interface PasskeyModule {
|
|
66
|
+
create: (request: PasskeyCreateRequest) => Promise<unknown>;
|
|
67
|
+
get: (request: PasskeyGetRequest) => Promise<PasskeyGetResult | null>;
|
|
68
|
+
isSupported: () => Promise<boolean>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let cachedPasskeyModule: PasskeyModule | null = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lazily loads the react-native-passkey module.
|
|
75
|
+
* This allows the module to be an optional peer dependency.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export function getPasskeyModule(): PasskeyModule {
|
|
79
|
+
if (cachedPasskeyModule) {
|
|
80
|
+
return cachedPasskeyModule;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
85
|
+
const module = require("react-native-passkey");
|
|
86
|
+
const passkeyModule: PasskeyModule =
|
|
87
|
+
module.Passkey || module.default || module;
|
|
88
|
+
cachedPasskeyModule = passkeyModule;
|
|
89
|
+
return passkeyModule;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error("Failed to load react-native-passkey:", e);
|
|
92
|
+
throw new Error(
|
|
93
|
+
"react-native-passkey is not installed. Please install it to use passkey authentication: npm install react-native-passkey",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sets a custom passkey module (for testing purposes).
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
export function setPasskeyModule(module: PasskeyModule | null): void {
|
|
103
|
+
cachedPasskeyModule = module;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if passkeys are supported on the current device.
|
|
108
|
+
* Returns false if the react-native-passkey module is not available or if the device doesn't support passkeys.
|
|
109
|
+
*/
|
|
110
|
+
export async function isPasskeySupported(): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const module = getPasskeyModule();
|
|
113
|
+
return await module.isSupported();
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `ReactNativePasskeyAuth` provides passkey (WebAuthn) authentication for React Native apps.
|
|
121
|
+
*
|
|
122
|
+
* This class uses the device's biometric authentication (FaceID/TouchID/fingerprint) to
|
|
123
|
+
* securely store and retrieve Jazz account credentials.
|
|
124
|
+
*
|
|
125
|
+
* **Requirements:**
|
|
126
|
+
* - Install `react-native-passkey` as a peer dependency
|
|
127
|
+
* - Configure your app's associated domains (iOS) and asset links (Android)
|
|
128
|
+
* - Passkeys require HTTPS domain verification
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { ReactNativePasskeyAuth } from "jazz-tools/react-native-core";
|
|
132
|
+
*
|
|
133
|
+
* const auth = new ReactNativePasskeyAuth(
|
|
134
|
+
* crypto,
|
|
135
|
+
* authenticate,
|
|
136
|
+
* authSecretStorage,
|
|
137
|
+
* "My App",
|
|
138
|
+
* "myapp.com"
|
|
139
|
+
* );
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @category Auth Providers
|
|
143
|
+
*/
|
|
144
|
+
export class ReactNativePasskeyAuth {
|
|
145
|
+
constructor(
|
|
146
|
+
protected crypto: CryptoProvider,
|
|
147
|
+
protected authenticate: AuthenticateAccountFunction,
|
|
148
|
+
protected authSecretStorage: AuthSecretStorage,
|
|
149
|
+
public appName: string,
|
|
150
|
+
public rpId: string,
|
|
151
|
+
) {}
|
|
152
|
+
|
|
153
|
+
static readonly id = "passkey";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Log in using an existing passkey.
|
|
157
|
+
* This will prompt the user to authenticate with their device biometrics.
|
|
158
|
+
*/
|
|
159
|
+
logIn = async () => {
|
|
160
|
+
const { crypto, authenticate } = this;
|
|
161
|
+
|
|
162
|
+
const webAuthNCredential = await this.getPasskeyCredentials();
|
|
163
|
+
|
|
164
|
+
if (!webAuthNCredential) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!webAuthNCredential.response.userHandle) {
|
|
169
|
+
throw new Error("Passkey credential is missing userHandle");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const webAuthNCredentialPayload = base64UrlToUint8Array(
|
|
173
|
+
webAuthNCredential.response.userHandle,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const accountSecretSeed = webAuthNCredentialPayload.slice(
|
|
177
|
+
0,
|
|
178
|
+
cojsonInternals.secretSeedLength,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const secret = crypto.agentSecretFromSecretSeed(accountSecretSeed);
|
|
182
|
+
|
|
183
|
+
const accountID = cojsonInternals.rawCoIDfromBytes(
|
|
184
|
+
webAuthNCredentialPayload.slice(
|
|
185
|
+
cojsonInternals.secretSeedLength,
|
|
186
|
+
cojsonInternals.secretSeedLength + cojsonInternals.shortHashLength,
|
|
187
|
+
),
|
|
188
|
+
) as ID<Account>;
|
|
189
|
+
|
|
190
|
+
await authenticate({
|
|
191
|
+
accountID,
|
|
192
|
+
accountSecret: secret,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await this.authSecretStorage.set({
|
|
196
|
+
accountID,
|
|
197
|
+
secretSeed: accountSecretSeed,
|
|
198
|
+
accountSecret: secret,
|
|
199
|
+
provider: "passkey",
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Register a new passkey for the current account.
|
|
205
|
+
* This will create a passkey that stores the account credentials securely on the device.
|
|
206
|
+
*
|
|
207
|
+
* @param username - The display name for the passkey
|
|
208
|
+
*/
|
|
209
|
+
signUp = async (username: string) => {
|
|
210
|
+
const credentials = await this.authSecretStorage.get();
|
|
211
|
+
|
|
212
|
+
if (!credentials?.secretSeed) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"Not enough credentials to register the account with passkey",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await this.createPasskeyCredentials({
|
|
219
|
+
accountID: credentials.accountID,
|
|
220
|
+
secretSeed: credentials.secretSeed,
|
|
221
|
+
username,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const currentAccount = await Account.getMe().$jazz.ensureLoaded({
|
|
225
|
+
resolve: {
|
|
226
|
+
profile: true,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (username.trim().length !== 0) {
|
|
231
|
+
currentAccount.profile.$jazz.set("name", username);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await this.authSecretStorage.set({
|
|
235
|
+
accountID: credentials.accountID,
|
|
236
|
+
secretSeed: credentials.secretSeed,
|
|
237
|
+
accountSecret: credentials.accountSecret,
|
|
238
|
+
provider: "passkey",
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
private async createPasskeyCredentials({
|
|
243
|
+
accountID,
|
|
244
|
+
secretSeed,
|
|
245
|
+
username,
|
|
246
|
+
}: {
|
|
247
|
+
accountID: ID<Account>;
|
|
248
|
+
secretSeed: Uint8Array;
|
|
249
|
+
username: string;
|
|
250
|
+
}) {
|
|
251
|
+
const webAuthNCredentialPayload = new Uint8Array(
|
|
252
|
+
cojsonInternals.secretSeedLength + cojsonInternals.shortHashLength,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
webAuthNCredentialPayload.set(secretSeed);
|
|
256
|
+
webAuthNCredentialPayload.set(
|
|
257
|
+
cojsonInternals.rawCoIDtoBytes(accountID as unknown as RawAccountID),
|
|
258
|
+
cojsonInternals.secretSeedLength,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const challenge = uint8ArrayToBase64Url(
|
|
262
|
+
new Uint8Array(this.crypto.randomBytes(32)),
|
|
263
|
+
);
|
|
264
|
+
const userId = uint8ArrayToBase64Url(webAuthNCredentialPayload);
|
|
265
|
+
|
|
266
|
+
const passkey = getPasskeyModule();
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await passkey.create({
|
|
270
|
+
challenge,
|
|
271
|
+
rp: {
|
|
272
|
+
id: this.rpId,
|
|
273
|
+
name: this.appName,
|
|
274
|
+
},
|
|
275
|
+
user: {
|
|
276
|
+
id: userId,
|
|
277
|
+
name: `${username} (${new Date().toLocaleString()})`,
|
|
278
|
+
displayName: username,
|
|
279
|
+
},
|
|
280
|
+
pubKeyCredParams: [
|
|
281
|
+
{ alg: -7, type: "public-key" }, // ES256
|
|
282
|
+
{ alg: -257, type: "public-key" }, // RS256
|
|
283
|
+
],
|
|
284
|
+
authenticatorSelection: {
|
|
285
|
+
residentKey: "required",
|
|
286
|
+
userVerification: "preferred",
|
|
287
|
+
},
|
|
288
|
+
timeout: 60000,
|
|
289
|
+
attestation: "none",
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error("Passkey creation aborted", { cause: error });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async getPasskeyCredentials(): Promise<PasskeyGetResult | null> {
|
|
297
|
+
const challenge = uint8ArrayToBase64Url(
|
|
298
|
+
new Uint8Array(this.crypto.randomBytes(32)),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const passkey = getPasskeyModule();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await passkey.get({
|
|
305
|
+
challenge,
|
|
306
|
+
rpId: this.rpId,
|
|
307
|
+
timeout: 60000,
|
|
308
|
+
userVerification: "preferred",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return result;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw new Error("Passkey authentication aborted", { cause: error });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
View,
|
|
8
|
+
useColorScheme,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { usePasskeyAuth } from "./usePasskeyAuth.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A basic UI component for passkey authentication in React Native apps.
|
|
14
|
+
*
|
|
15
|
+
* This component provides a simple sign-up and log-in interface using passkeys.
|
|
16
|
+
* It's designed for quick prototyping and can be customized or replaced with
|
|
17
|
+
* your own authentication UI.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { PasskeyAuthBasicUI } from "jazz-tools/react-native-core";
|
|
22
|
+
*
|
|
23
|
+
* function App() {
|
|
24
|
+
* return (
|
|
25
|
+
* <JazzProvider ...>
|
|
26
|
+
* <PasskeyAuthBasicUI
|
|
27
|
+
* appName="My App"
|
|
28
|
+
* rpId="myapp.com"
|
|
29
|
+
* >
|
|
30
|
+
* <MainApp />
|
|
31
|
+
* </PasskeyAuthBasicUI>
|
|
32
|
+
* </JazzProvider>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @category Auth Providers
|
|
38
|
+
*/
|
|
39
|
+
export const PasskeyAuthBasicUI = ({
|
|
40
|
+
appName,
|
|
41
|
+
rpId,
|
|
42
|
+
children,
|
|
43
|
+
}: {
|
|
44
|
+
appName: string;
|
|
45
|
+
rpId: string;
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
}) => {
|
|
48
|
+
const colorScheme = useColorScheme();
|
|
49
|
+
const darkMode = colorScheme === "dark";
|
|
50
|
+
const [username, setUsername] = useState<string>("");
|
|
51
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
const auth = usePasskeyAuth({ appName, rpId });
|
|
54
|
+
|
|
55
|
+
const handleSignUp = () => {
|
|
56
|
+
setErrorMessage(null);
|
|
57
|
+
|
|
58
|
+
auth.signUp(username).catch((error) => {
|
|
59
|
+
if (error.cause instanceof Error) {
|
|
60
|
+
setErrorMessage(error.cause.message);
|
|
61
|
+
} else {
|
|
62
|
+
setErrorMessage(error.message);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleLogIn = () => {
|
|
68
|
+
setErrorMessage(null);
|
|
69
|
+
|
|
70
|
+
auth.logIn().catch((error) => {
|
|
71
|
+
if (error.cause instanceof Error) {
|
|
72
|
+
setErrorMessage(error.cause.message);
|
|
73
|
+
} else {
|
|
74
|
+
setErrorMessage(error.message);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (auth.state === "signedIn") {
|
|
80
|
+
return children;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View
|
|
85
|
+
style={[
|
|
86
|
+
styles.container,
|
|
87
|
+
darkMode ? styles.darkBackground : styles.lightBackground,
|
|
88
|
+
]}
|
|
89
|
+
>
|
|
90
|
+
<View style={styles.formContainer}>
|
|
91
|
+
<Text
|
|
92
|
+
style={[
|
|
93
|
+
styles.headerText,
|
|
94
|
+
darkMode ? styles.darkText : styles.lightText,
|
|
95
|
+
]}
|
|
96
|
+
>
|
|
97
|
+
{appName}
|
|
98
|
+
</Text>
|
|
99
|
+
|
|
100
|
+
{errorMessage && <Text style={styles.errorText}>{errorMessage}</Text>}
|
|
101
|
+
|
|
102
|
+
<TextInput
|
|
103
|
+
placeholder="Display name"
|
|
104
|
+
value={username}
|
|
105
|
+
onChangeText={setUsername}
|
|
106
|
+
placeholderTextColor={darkMode ? "#999" : "#666"}
|
|
107
|
+
style={[
|
|
108
|
+
styles.textInput,
|
|
109
|
+
darkMode ? styles.darkInput : styles.lightInput,
|
|
110
|
+
]}
|
|
111
|
+
autoCapitalize="words"
|
|
112
|
+
autoCorrect={false}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<TouchableOpacity
|
|
116
|
+
onPress={handleSignUp}
|
|
117
|
+
style={[
|
|
118
|
+
styles.button,
|
|
119
|
+
darkMode ? styles.darkButton : styles.lightButton,
|
|
120
|
+
]}
|
|
121
|
+
>
|
|
122
|
+
<Text
|
|
123
|
+
style={darkMode ? styles.darkButtonText : styles.lightButtonText}
|
|
124
|
+
>
|
|
125
|
+
Sign Up with Passkey
|
|
126
|
+
</Text>
|
|
127
|
+
</TouchableOpacity>
|
|
128
|
+
|
|
129
|
+
<View style={styles.divider}>
|
|
130
|
+
<View
|
|
131
|
+
style={[
|
|
132
|
+
styles.dividerLine,
|
|
133
|
+
darkMode ? styles.darkDivider : styles.lightDivider,
|
|
134
|
+
]}
|
|
135
|
+
/>
|
|
136
|
+
<Text
|
|
137
|
+
style={[
|
|
138
|
+
styles.dividerText,
|
|
139
|
+
darkMode ? styles.darkText : styles.lightText,
|
|
140
|
+
]}
|
|
141
|
+
>
|
|
142
|
+
or
|
|
143
|
+
</Text>
|
|
144
|
+
<View
|
|
145
|
+
style={[
|
|
146
|
+
styles.dividerLine,
|
|
147
|
+
darkMode ? styles.darkDivider : styles.lightDivider,
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
|
|
152
|
+
<TouchableOpacity
|
|
153
|
+
onPress={handleLogIn}
|
|
154
|
+
style={[
|
|
155
|
+
styles.secondaryButton,
|
|
156
|
+
darkMode ? styles.darkSecondaryButton : styles.lightSecondaryButton,
|
|
157
|
+
]}
|
|
158
|
+
>
|
|
159
|
+
<Text style={darkMode ? styles.darkText : styles.lightText}>
|
|
160
|
+
Log In with Existing Passkey
|
|
161
|
+
</Text>
|
|
162
|
+
</TouchableOpacity>
|
|
163
|
+
</View>
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const styles = StyleSheet.create({
|
|
169
|
+
container: {
|
|
170
|
+
flex: 1,
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
padding: 20,
|
|
174
|
+
},
|
|
175
|
+
formContainer: {
|
|
176
|
+
width: "80%",
|
|
177
|
+
maxWidth: 300,
|
|
178
|
+
alignItems: "center",
|
|
179
|
+
justifyContent: "center",
|
|
180
|
+
},
|
|
181
|
+
headerText: {
|
|
182
|
+
fontSize: 24,
|
|
183
|
+
fontWeight: "600",
|
|
184
|
+
marginBottom: 30,
|
|
185
|
+
},
|
|
186
|
+
errorText: {
|
|
187
|
+
color: "#ff4444",
|
|
188
|
+
marginVertical: 10,
|
|
189
|
+
textAlign: "center",
|
|
190
|
+
fontSize: 14,
|
|
191
|
+
},
|
|
192
|
+
textInput: {
|
|
193
|
+
borderWidth: 1,
|
|
194
|
+
padding: 12,
|
|
195
|
+
marginVertical: 10,
|
|
196
|
+
width: "100%",
|
|
197
|
+
borderRadius: 8,
|
|
198
|
+
fontSize: 16,
|
|
199
|
+
},
|
|
200
|
+
darkInput: {
|
|
201
|
+
borderColor: "#444",
|
|
202
|
+
backgroundColor: "#1a1a1a",
|
|
203
|
+
color: "#fff",
|
|
204
|
+
},
|
|
205
|
+
lightInput: {
|
|
206
|
+
borderColor: "#ddd",
|
|
207
|
+
backgroundColor: "#fff",
|
|
208
|
+
color: "#000",
|
|
209
|
+
},
|
|
210
|
+
button: {
|
|
211
|
+
paddingVertical: 14,
|
|
212
|
+
paddingHorizontal: 10,
|
|
213
|
+
borderRadius: 8,
|
|
214
|
+
width: "100%",
|
|
215
|
+
marginVertical: 10,
|
|
216
|
+
},
|
|
217
|
+
darkButton: {
|
|
218
|
+
backgroundColor: "#0066cc",
|
|
219
|
+
},
|
|
220
|
+
lightButton: {
|
|
221
|
+
backgroundColor: "#007aff",
|
|
222
|
+
},
|
|
223
|
+
darkButtonText: {
|
|
224
|
+
color: "#fff",
|
|
225
|
+
textAlign: "center",
|
|
226
|
+
fontWeight: "600",
|
|
227
|
+
fontSize: 16,
|
|
228
|
+
},
|
|
229
|
+
lightButtonText: {
|
|
230
|
+
color: "#fff",
|
|
231
|
+
textAlign: "center",
|
|
232
|
+
fontWeight: "600",
|
|
233
|
+
fontSize: 16,
|
|
234
|
+
},
|
|
235
|
+
divider: {
|
|
236
|
+
flexDirection: "row",
|
|
237
|
+
alignItems: "center",
|
|
238
|
+
width: "100%",
|
|
239
|
+
marginVertical: 20,
|
|
240
|
+
},
|
|
241
|
+
dividerLine: {
|
|
242
|
+
flex: 1,
|
|
243
|
+
height: 1,
|
|
244
|
+
},
|
|
245
|
+
darkDivider: {
|
|
246
|
+
backgroundColor: "#444",
|
|
247
|
+
},
|
|
248
|
+
lightDivider: {
|
|
249
|
+
backgroundColor: "#ddd",
|
|
250
|
+
},
|
|
251
|
+
dividerText: {
|
|
252
|
+
marginHorizontal: 10,
|
|
253
|
+
fontSize: 14,
|
|
254
|
+
},
|
|
255
|
+
secondaryButton: {
|
|
256
|
+
paddingVertical: 14,
|
|
257
|
+
paddingHorizontal: 10,
|
|
258
|
+
borderRadius: 8,
|
|
259
|
+
width: "100%",
|
|
260
|
+
borderWidth: 1,
|
|
261
|
+
},
|
|
262
|
+
darkSecondaryButton: {
|
|
263
|
+
borderColor: "#444",
|
|
264
|
+
backgroundColor: "transparent",
|
|
265
|
+
},
|
|
266
|
+
lightSecondaryButton: {
|
|
267
|
+
borderColor: "#ddd",
|
|
268
|
+
backgroundColor: "transparent",
|
|
269
|
+
},
|
|
270
|
+
darkText: {
|
|
271
|
+
color: "#fff",
|
|
272
|
+
textAlign: "center",
|
|
273
|
+
},
|
|
274
|
+
lightText: {
|
|
275
|
+
color: "#000",
|
|
276
|
+
textAlign: "center",
|
|
277
|
+
},
|
|
278
|
+
darkBackground: {
|
|
279
|
+
backgroundColor: "#000",
|
|
280
|
+
},
|
|
281
|
+
lightBackground: {
|
|
282
|
+
backgroundColor: "#fff",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { KvStoreContext } from "../storage/kv-store-context.js";
|
|
2
2
|
|
|
3
3
|
export * from "./DemoAuthUI.js";
|
|
4
|
+
export * from "./PasskeyAuth.js";
|
|
5
|
+
export * from "./usePasskeyAuth.js";
|
|
6
|
+
export * from "./PasskeyAuthBasicUI.js";
|
|
4
7
|
|
|
5
8
|
export function clearUserCredentials() {
|
|
6
9
|
const kvStore = KvStoreContext.getInstance().getStorage();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for base64url encoding/decoding used by React Native passkey authentication.
|
|
3
|
+
*
|
|
4
|
+
* The react-native-passkey library uses base64url strings, while the browser WebAuthn API
|
|
5
|
+
* uses raw ArrayBuffers. These utilities handle the conversion between formats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a Uint8Array to a base64url-encoded string.
|
|
10
|
+
* Base64url uses '-' and '_' instead of '+' and '/', and omits padding '='.
|
|
11
|
+
*/
|
|
12
|
+
export function uint8ArrayToBase64Url(bytes: Uint8Array): string {
|
|
13
|
+
// Convert to regular base64 first
|
|
14
|
+
let binary = "";
|
|
15
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
16
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
17
|
+
}
|
|
18
|
+
const base64 = btoa(binary);
|
|
19
|
+
|
|
20
|
+
// Convert to base64url: replace + with -, / with _, remove =
|
|
21
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts a base64url-encoded string to a Uint8Array.
|
|
26
|
+
*/
|
|
27
|
+
export function base64UrlToUint8Array(base64url: string): Uint8Array {
|
|
28
|
+
// Convert base64url to regular base64
|
|
29
|
+
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
30
|
+
|
|
31
|
+
// Add padding if needed
|
|
32
|
+
const padding = base64.length % 4;
|
|
33
|
+
if (padding > 0) {
|
|
34
|
+
base64 += "=".repeat(4 - padding);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Decode base64 to binary string
|
|
38
|
+
const binary = atob(base64);
|
|
39
|
+
|
|
40
|
+
// Convert binary string to Uint8Array
|
|
41
|
+
const bytes = new Uint8Array(binary.length);
|
|
42
|
+
for (let i = 0; i < binary.length; i++) {
|
|
43
|
+
bytes[i] = binary.charCodeAt(i);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|