Compare commits

..

113 Commits

Author SHA1 Message Date
Guido D'Orsi
d7895504a4 Merge pull request #1896 from garden-co/changeset-release/main
Version Packages
2025-04-11 17:28:50 +02:00
github-actions[bot]
699db46ed6 Version Packages 2025-04-11 15:15:37 +00:00
Guido D'Orsi
d303be6709 Merge pull request #1898 from garden-co/fix/unavailable-messages-error
fix(inbox): gracefully handle unavailable messages error
2025-04-11 17:12:42 +02:00
Guido D'Orsi
fe6f561afd chore: changeset 2025-04-11 17:11:46 +02:00
Guido D'Orsi
93c7f6c253 Merge remote-tracking branch 'origin/main' into fix/unavailable-messages-error 2025-04-11 15:58:14 +02:00
Guido D'Orsi
3b2831f7de Merge pull request #1895 from garden-co/fix/inbox-messages-upload
fix(inbox): ensure that the message is fully uploaded before sharing the id with the worker
2025-04-11 15:57:59 +02:00
Guido D'Orsi
902e539f56 fix(inbox): gracefully handle unavailable messages error 2025-04-11 15:56:23 +02:00
Anselm Eickhoff
9e9946cf48 Merge pull request #1874 from garden-co/create-jazz-app-git-monorepo-init
Initialize git for a scaffolded project only if not inside a monorepo
2025-04-11 14:21:31 +01:00
Trisha Lim
051e6c497e Merge pull request #1897 from garden-co/fix/hide-headings
fix: TOC is showing hidden headings on clerk react native docs
2025-04-11 16:25:48 +07:00
Trisha Lim
9b2a73b900 create separate mdx for react native clerk docs 2025-04-11 16:00:12 +07:00
Guido D'Orsi
dd8ed599f7 Merge pull request #1846 from garden-co/fix/inspector-invalid-html
fix(inspector): invalid html, set z-index to 999
2025-04-11 10:41:10 +02:00
Guido D'Orsi
33e51267e3 Merge pull request #1894 from garden-co/1438-release
Bumps version for release
2025-04-11 10:16:09 +02:00
Guido D'Orsi
59251a58c4 fix(inbox): ensure that the message is fully uploaded before sharing the id with the worker 2025-04-11 10:12:23 +02:00
Benjamin S. Leveritt
e090b3992d Bumps version for release
Closes #1438
2025-04-11 09:08:29 +01:00
Guido D'Orsi
31794684a0 test(music-player): fix loginButton selector 2025-04-11 09:26:30 +02:00
Guido D'Orsi
14fcc8dde5 test: add wait for the visibility checks 2025-04-11 09:16:01 +02:00
Benjamin S. Leveritt
13f1821c04 Merge pull request #1880 from garden-co/remove-catchall-for-docs
Remove catchall-route for docs
2025-04-11 07:09:51 +01:00
Trisha Lim
4c3b85abcd fix logo size on docs intro page 2025-04-11 11:52:48 +07:00
Trisha Lim
4b0544fd0d shorten error message 2025-04-11 11:46:50 +07:00
Trisha Lim
568674a7fa lint fixes 2025-04-11 11:44:51 +07:00
Trisha Lim
ef012f07aa fix coming soon page 2025-04-11 11:41:19 +07:00
Guido D'Orsi
eee2330325 Merge pull request #1891 from garden-co/changeset-release/main
Version Packages
2025-04-10 19:49:47 +02:00
github-actions[bot]
b83ec05ccc Version Packages 2025-04-10 17:36:24 +00:00
Guido D'Orsi
386525db48 Merge pull request #1886 from garden-co/fix/clerk-auth
fix: handle Clerk expiration and fix logout
2025-04-10 19:34:01 +02:00
Guido D'Orsi
a8809d840c test: add logout test on music-player 2025-04-10 19:26:28 +02:00
Guido D'Orsi
005fc1f8c9 fix: restore logOut when logOutReplacement is not passed 2025-04-10 19:07:34 +02:00
Guido D'Orsi
3129982582 fix: handle Clerk expiration and fix logout 2025-04-10 18:22:52 +02:00
Benjamin S. Leveritt
5d7bb70c7d Fix pages with empty TOCs 2025-04-10 16:29:50 +01:00
Anselm
1a7a84f71b Get rid of table-of-contents context workaround 2025-04-10 14:46:19 +01:00
Anselm
1d51bdc016 Establish new content folder, get rid of docs catchall segment 2025-04-10 14:46:19 +01:00
Guido D'Orsi
8da6f3a897 Merge pull request #1858 from garden-co/changeset-release/main
Version Packages
2025-04-10 15:45:31 +02:00
github-actions[bot]
dca9293ae7 Version Packages 2025-04-10 13:42:32 +00:00
Guido D'Orsi
2b362fd331 Merge pull request #1878 from garden-co/feature/log-permission-errors
feat: log permission errors when loading or subscribing to values
2025-04-10 15:37:45 +02:00
Guido D'Orsi
f03f1b6de8 Merge pull request #1882 from garden-co/fix/null-deep-loading
fix: handle null properties in the CoMap deep loading
2025-04-10 15:37:32 +02:00
Guido D'Orsi
bd57177586 fix: handle null properties in the CoMap deep loading 2025-04-10 15:30:49 +02:00
Guido D'Orsi
9e0e2709a5 chore: fix typo in the log error 2025-04-10 15:29:09 +02:00
Guido D'Orsi
01b2ab7148 feat: add unaccessible value id and tests 2025-04-10 14:50:06 +02:00
Sammii
44b7d39467 Merge pull request #1866 from garden-co/footer-links-fix
adjusting footer title styling on dark mode
2025-04-10 12:39:34 +01:00
Guido D'Orsi
5373ee2858 Merge pull request #1877 from garden-co/jmsv/1869/relax-websocket-types
Relax the WebSocket types on jazz-nodejs
2025-04-10 13:22:01 +02:00
James Vickery
b19cab78d3 changeset 2025-04-10 12:21:34 +01:00
Giordano Ricci
3f86dfce4f Merge pull request #1876 from garden-co/gio/tweak-status-page-cache-pt2
chore: add env to turbo, set force-static
2025-04-10 12:21:25 +01:00
Guido D'Orsi
12f8bfa28f feat: log permission errors when loading or subscribing to values 2025-04-10 13:12:31 +02:00
James Vickery
53211a4fca relax websocket types with AnyWebSocket 2025-04-10 12:11:42 +01:00
Giordano Ricci
0a6cd4e9b2 move env to the cortrect turbo.json 2025-04-10 12:00:40 +01:00
Giordano Ricci
69954caee6 chore: add env to turbo, set force-static 2025-04-10 11:50:35 +01:00
Giordano Ricci
6a5bd28d07 Merge pull request #1875 from garden-co/gio/tweak-status-page-cache
chore: tweak status page cache
2025-04-10 11:35:26 +01:00
Giordano Ricci
ba58bc3ace chore: tweak cache 2025-04-10 11:28:09 +01:00
pax-k
94cb615a38 chore: cleanup 2025-04-10 13:22:42 +03:00
pax-k
b130f46b6c chore: changeset 2025-04-10 13:20:39 +03:00
pax-k
989d59f978 fix(create-jazz-app): initialize git only if not inside a monorepo 2025-04-10 13:19:23 +03:00
Sammii
c3dd099ee1 adjusting footer title styling on dark mode 2025-04-10 10:25:53 +01:00
Benjamin S. Leveritt
882fd55d69 Merge pull request #1864 from garden-co/1863-reorder-guestmode-authentication-notes
Reorders the lists
2025-04-10 10:11:03 +01:00
Benjamin S. Leveritt
b2fdb8b9e5 Reorders the lists 2025-04-10 10:01:24 +01:00
Sammii
e879ec981e Merge pull request #1860 from garden-co/not-found-page
Not found page
2025-04-10 09:53:01 +01:00
Benjamin S. Leveritt
980609ca87 Merge pull request #1859 from garden-co/1852-break-out-authentication-methods
Breaks up the authentication overview
2025-04-10 09:40:50 +01:00
Benjamin S. Leveritt
71cd7396b7 Merge pull request #1847 from garden-co/1722-clarify-distinction-between-anonymous-auth-and-guest-mode
1722 clarify distinction between anonymous auth and guest mode
2025-04-10 09:39:10 +01:00
Benjamin S. Leveritt
dedc3e277c Adds note on browser support 2025-04-10 07:30:25 +01:00
Benjamin S. Leveritt
656866729d Updates for more uniform layout 2025-04-10 07:30:25 +01:00
Benjamin S. Leveritt
f93dd1f779 Add template for auth methods 2025-04-10 07:30:25 +01:00
Benjamin S. Leveritt
80d499f002 Improve the Passphrase explanation 2025-04-10 07:30:25 +01:00
Benjamin S. Leveritt
c9a87e52f3 Breaks up the authentication overview 2025-04-10 07:30:25 +01:00
Benjamin S. Leveritt
3f98d9ab73 Adds dep link to jazz-react-native 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
abb0e8fada Tidy up for non-React readers 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
be2e1f3c61 Adds note on sync options for Providers 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
75f20d8176 Adds notes on keeping data on migration 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
e6bef5275b Fix guest checks 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
9813db1603 Add note about logging out going back to Anon 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
a3143f20a9 Add section on Auth flow 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
f54beb2d88 Fix musica example 2025-04-10 07:30:24 +01:00
Benjamin S. Leveritt
b7ce1e2da0 Add notes on difference between anon and guest 2025-04-10 07:30:23 +01:00
Benjamin S. Leveritt
6dba138ec7 Add SignInButton to Clerk Expo example 2025-04-10 07:30:23 +01:00
Benjamin S. Leveritt
32f59a618f Adds typechecking to examples 2025-04-10 07:30:23 +01:00
Sammii
9b2de387ed updating styling 2025-04-09 17:22:26 +01:00
Guido D'Orsi
b612258c5e docs: update ensureLoaded to the new resolve APIs 2025-04-09 18:19:36 +02:00
Sammii
09b59ed18b amending styling and code tidy 2025-04-09 17:07:18 +01:00
Sammii
b7b186b67e adding new 404 icon to design system and creating not-found page for jazz homepage 2025-04-09 17:03:27 +01:00
Sammii
f2ba925db6 Merge pull request #1836 from garden-co/1830-progmatically-created-homepage-preview-image
1830 feat: creating opengraph-image.tsx on homepage
2025-04-09 16:35:34 +01:00
Guido D'Orsi
bc9488241f Merge pull request #1850 from garden-co/fix/inspector-coliteral-strings
fix(inspector): handle displaying a CoList of primitives
2025-04-09 16:14:08 +02:00
Guido D'Orsi
4fc36779dd docs: fix import from jazz-expo 2025-04-09 15:32:15 +02:00
Trisha Lim
bc008aeb23 render primitives in a table with index and value as headers 2025-04-09 20:14:57 +07:00
Sammii
8ad45a421e adjusting letting specing 2025-04-09 13:38:36 +01:00
Sammii
197317efbf amend width & height defaults on logo 2025-04-09 13:28:15 +01:00
Sammii
775ad975f3 creating marketingCopy const to create SSOT 2025-04-09 13:27:57 +01:00
Sammii
eaedf455d4 amending styling 2025-04-09 13:23:09 +01:00
Trisha Lim
017f6c8074 handle displaying a CoList of primitives 2025-04-09 18:32:10 +07:00
Sammii
48dd922712 add base values for width and height for JazzLogo 2025-04-09 12:13:42 +01:00
Sammii
202b320ad1 fighting this svg 2025-04-09 12:08:52 +01:00
Sammii
ea094ae64b doubling logo size 2025-04-09 11:17:53 +01:00
Sammii
9412aeb938 amending svg size 2025-04-09 11:04:49 +01:00
Trisha Lim
785fc893ee set z-index of embedded inspector to 999 2025-04-09 16:51:18 +07:00
Sammii
4f5e471667 adding alt and seeing if anything affects this svg logo 2025-04-09 10:44:42 +01:00
Sammii
fc6b20d370 adding width and height to logo svg 2025-04-09 10:37:43 +01:00
Sammii
e4886d1b03 adding viewBoxSize prop to JazzLogo to manipulate size on build prerender for preview image 2025-04-09 10:22:48 +01:00
Sammii
dfb2b19209 wrapping JazzLogo component in img to size viewbox on svg 2025-04-09 10:10:17 +01:00
Sammii
1a92d6b1e5 moving Manrope to homepage prject public dir 2025-04-09 09:47:21 +01:00
Sammii
d5c1f49cc5 logo size debug 2025-04-09 09:41:33 +01:00
Sammii
7d6ce843de seeing if we can keep fonts together in the design-system for the open graph image 2025-04-09 09:41:26 +01:00
Sammii
0298f0eb29 updating Logo import 2025-04-09 09:29:41 +01:00
Sammii
794c56dfac creating reusable OpenGraphImage component, updating styling 2025-04-09 09:22:45 +01:00
Sammii
6b9382b5e9 code tidy 2025-04-09 09:21:14 +01:00
Trisha Lim
08ae9b295f fix: invalid html 2025-04-09 15:15:50 +07:00
Sammii
917e8a21d8 more tweaks 2025-04-09 08:28:48 +01:00
Sammii
595e3c89df amending OG image styling to fix overflow issues 2025-04-08 19:11:54 +01:00
Sammii
b9afa42662 amending styling, again... 2025-04-08 17:51:50 +01:00
Sammii
5246a54118 switching padding to margin to try fix overflow issues on !X examples 2025-04-08 17:44:34 +01:00
Sammii
a036391f69 amending image styling 2025-04-08 17:25:28 +01:00
Sammii
6ad24315bb updating copy 2025-04-08 16:03:44 +01:00
Sammii
48a83c356d adding display flex prop to div 2025-04-08 15:52:38 +01:00
Sammii
756d52d106 moving opengraph-image to app/ 2025-04-08 15:47:29 +01:00
Sammii
83876a3523 renaming twitter-image to opengraph-image 2025-04-08 15:19:40 +01:00
Sammii
b77c6d4edc adding Manrope font to project directory for font loading process 2025-04-08 15:18:07 +01:00
Sammii
c74fc11b25 commenting out images on open graph metadata 2025-04-08 15:17:39 +01:00
Sammii
7a636bd8c2 creating twitter-image.tsx on homepage 2025-04-08 14:58:58 +01:00
240 changed files with 3572 additions and 1070 deletions

View File

@@ -13,7 +13,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "starters/react-passkey-auth"]
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "starters/react-passkey-auth"]
steps:
- uses: actions/checkout@v4

View File

@@ -1,5 +1,34 @@
# chat-rn-expo-clerk
## 1.0.98
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-expo@0.13.5
- jazz-react-native-media-images@0.13.5
## 1.0.97
### Patch Changes
- Updated dependencies [3129982]
- Updated dependencies [3129982]
- jazz-expo@0.13.4
- jazz-tools@0.13.4
- jazz-react-native-media-images@0.13.4
## 1.0.96
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-expo@0.13.3
- jazz-react-native-media-images@0.13.3
## 1.0.95
### Patch Changes

View File

@@ -2,28 +2,39 @@ import "../global.css";
import { ClerkLoaded, ClerkProvider } from "@clerk/clerk-expo";
import { secureStore } from "@clerk/clerk-expo/secure-store";
import { useFonts } from "expo-font";
import { Slot } from "expo-router";
import { Slot, useRouter, useSegments } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useIsAuthenticated, useJazzContext } from "jazz-expo";
import React, { useEffect } from "react";
import { tokenCache } from "../cache";
import { JazzAndAuth } from "../src/auth-context";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
function InitialLayout() {
const isAuthenticated = useIsAuthenticated();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
const inAuthGroup = segments[0] === "(auth)";
if (!loaded) {
return null;
}
if (isAuthenticated && inAuthGroup) {
router.replace("/chat");
} else if (!isAuthenticated && !inAuthGroup) {
router.replace("/");
}
SplashScreen.hideAsync();
}, [isAuthenticated, segments, router]);
return <Slot />;
}
export default function RootLayout() {
const [fontsLoaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
@@ -33,6 +44,17 @@ export default function RootLayout() {
);
}
useEffect(() => {
if (fontsLoaded) {
} else {
SplashScreen.preventAutoHideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
<ClerkProvider
tokenCache={tokenCache}
@@ -41,7 +63,7 @@ export default function RootLayout() {
>
<ClerkLoaded>
<JazzAndAuth>
<Slot />
<InitialLayout />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-expo-clerk",
"main": "index.js",
"version": "1.0.95",
"version": "1.0.98",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,5 +1,31 @@
# chat-rn-expo
## 1.0.85
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-expo@0.13.5
## 1.0.84
### Patch Changes
- Updated dependencies [3129982]
- Updated dependencies [3129982]
- jazz-expo@0.13.4
- jazz-tools@0.13.4
## 1.0.83
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-expo@0.13.3
## 1.0.82
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn-expo",
"version": "1.0.82",
"version": "1.0.85",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,5 +1,35 @@
# chat-rn
## 1.0.94
### Patch Changes
- Updated dependencies [e090b39]
- Updated dependencies [fe6f561]
- cojson@0.13.5
- jazz-tools@0.13.5
- cojson-transport-ws@0.13.5
- jazz-react-native@0.13.5
## 1.0.93
### Patch Changes
- Updated dependencies [3129982]
- jazz-tools@0.13.4
- jazz-react-native@0.13.4
## 1.0.92
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [b19cab7]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- cojson-transport-ws@0.13.3
- jazz-react-native@0.13.3
## 1.0.91
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.91",
"version": "1.0.94",
"main": "index.js",
"scripts": {
"android": "react-native run-android",

View File

@@ -1,5 +1,33 @@
# chat-vue
## 0.0.78
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-browser@0.13.5
- jazz-vue@0.13.5
## 0.0.77
### Patch Changes
- Updated dependencies [3129982]
- jazz-browser@0.13.4
- jazz-tools@0.13.4
- jazz-vue@0.13.4
## 0.0.76
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-browser@0.13.3
- jazz-vue@0.13.3
## 0.0.75
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.75",
"version": "0.0.78",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,35 @@
# jazz-example-chat
## 0.0.175
### Patch Changes
- Updated dependencies [08ae9b2]
- Updated dependencies [fe6f561]
- jazz-inspector@0.13.5
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.174
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
- jazz-inspector@0.13.4
## 0.0.173
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- Updated dependencies [017f6c8]
- jazz-tools@0.13.3
- jazz-inspector@0.13.3
- jazz-react@0.13.3
## 0.0.172
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.172",
"version": "0.0.175",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
playwright-report

View File

@@ -1,5 +1,34 @@
# minimal-auth-clerk
## 0.0.74
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
- jazz-react-auth-clerk@0.13.5
## 0.0.73
### Patch Changes
- Updated dependencies [3129982]
- Updated dependencies [3129982]
- jazz-react-auth-clerk@0.13.4
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.72
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
- jazz-react-auth-clerk@0.13.3
## 0.0.71
### Patch Changes

View File

@@ -1,14 +1,16 @@
{
"name": "clerk",
"private": true,
"version": "0.0.71",
"version": "0.0.74",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write"
"format-and-lint:fix": "biome check . --write",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
@@ -19,6 +21,7 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@biomejs/biome": "1.9.4",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",

View File

@@ -0,0 +1,53 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm preview --port 5173",
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -1,8 +1,8 @@
import { SignInButton } from "@clerk/clerk-react";
import { SignInButton, SignOutButton } from "@clerk/clerk-react";
import { useAccount, useIsAuthenticated } from "jazz-react";
function App() {
const { me, logOut } = useAccount();
const { me } = useAccount();
const isAuthenticated = useIsAuthenticated();
@@ -11,7 +11,7 @@ function App() {
<div className="container">
<h1>You're logged in</h1>
<p>Welcome back, {me?.profile?.name}</p>
<button onClick={() => logOut()}>Logout</button>
<SignOutButton>Logout</SignOutButton>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { ClerkProvider, useClerk } from "@clerk/clerk-react";
import { ClerkProvider, SignOutButton, useClerk } from "@clerk/clerk-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
@@ -28,12 +28,23 @@ function JazzProvider({ children }: { children: React.ReactNode }) {
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<JazzProvider>
<App />
</JazzProvider>
</ClerkProvider>
</StrictMode>,
);
// Route to test that when the Clerk user expires, the app is logged out
if (location.search.includes("expirationTest")) {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<SignOutButton>Simulate expiration</SignOutButton>
</ClerkProvider>
</StrictMode>,
);
} else {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<JazzProvider>
<App />
</JazzProvider>
</ClerkProvider>
</StrictMode>,
);
}

View File

@@ -0,0 +1,36 @@
import { expect, test } from "@playwright/test";
test("login & expiration", async ({ page }) => {
await page.goto("/");
expect(page.getByText("You're not logged in")).toBeVisible();
await page.getByRole("button", { name: "Sign in" }).click();
await page
.getByRole("textbox", { name: "Email address" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page
.getByRole("textbox", { name: "Password" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page.waitForURL("/");
await page.getByText("You're logged in").waitFor({ state: "visible" });
expect(page.getByText("You're logged in")).toBeVisible();
await page.goto("/?expirationTest");
// Simulate expiration by logging out outside of Jazz
await page.getByRole("button", { name: "Simulate expiration" }).click();
await page.goto("/");
await page.getByText("You're not logged in").waitFor({ state: "visible" });
});

View File

@@ -0,0 +1,33 @@
import { expect, test } from "@playwright/test";
test("login & logout", async ({ page }) => {
await page.goto("/");
expect(page.getByText("You're not logged in")).toBeVisible();
await page.getByRole("button", { name: "Sign in" }).click();
await page
.getByRole("textbox", { name: "Email address" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page
.getByRole("textbox", { name: "Password" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page.waitForURL("/");
await page.getByText("You're logged in").waitFor({ state: "visible" });
expect(page.getByText("You're logged in")).toBeVisible();
await page.getByRole("button", { name: "Logout" }).click();
await page.getByText("You're not logged in").waitFor({ state: "visible" });
expect(page.getByText("You're not logged in")).toBeVisible();
});

View File

@@ -0,0 +1,33 @@
import { expect, test } from "@playwright/test";
test("login & reload", async ({ page }) => {
await page.goto("/");
expect(page.getByText("You're not logged in")).toBeVisible();
await page.getByRole("button", { name: "Sign in" }).click();
await page
.getByRole("textbox", { name: "Email address" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page
.getByRole("textbox", { name: "Password" })
.fill("guido+clerk-test@garden.co");
await page.keyboard.press("Enter");
await page.waitForURL("/");
await page.getByText("You're logged in").waitFor({ state: "visible" });
expect(page.getByText("You're logged in")).toBeVisible();
await page.reload();
await page.getByText("You're logged in").waitFor({ state: "visible" });
expect(page.getByText("You're logged in")).toBeVisible();
});

View File

@@ -1,5 +1,30 @@
# file-share-svelte
## 0.0.58
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-svelte@0.13.5
## 0.0.57
### Patch Changes
- Updated dependencies [3129982]
- jazz-tools@0.13.4
- jazz-svelte@0.13.4
## 0.0.56
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-svelte@0.13.3
## 0.0.55
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.55",
"version": "0.0.58",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,35 @@
# jazz-tailwind-demo-auth-starter
## 0.0.14
### Patch Changes
- Updated dependencies [08ae9b2]
- Updated dependencies [fe6f561]
- jazz-inspector@0.13.5
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.13
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
- jazz-inspector@0.13.4
## 0.0.12
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- Updated dependencies [017f6c8]
- jazz-tools@0.13.3
- jazz-inspector@0.13.3
- jazz-react@0.13.3
## 0.0.11
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "filestream",
"private": true,
"version": "0.0.11",
"version": "0.0.14",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# form
## 0.1.16
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.1.15
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.1.14
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.1.13
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.1.13",
"version": "0.1.16",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# image-upload
## 0.0.72
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.71
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.70
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.69
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.69",
"version": "0.0.72",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# jazz-example-inspector
## 0.0.125
### Patch Changes
- Updated dependencies [08ae9b2]
- Updated dependencies [e090b39]
- jazz-inspector@0.13.5
- cojson@0.13.5
- cojson-transport-ws@0.13.5
## 0.0.124
### Patch Changes
- jazz-inspector@0.13.4
## 0.0.123
### Patch Changes
- Updated dependencies [b19cab7]
- Updated dependencies [017f6c8]
- cojson-transport-ws@0.13.3
- jazz-inspector@0.13.3
## 0.0.122
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.122",
"version": "0.0.125",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# multi-cursors
## 0.0.68
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.67
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.66
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.65
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multi-cursors",
"private": true,
"version": "0.0.65",
"version": "0.0.68",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,34 @@
# multiauth
## 0.0.15
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
- jazz-react-auth-clerk@0.13.5
## 0.0.14
### Patch Changes
- Updated dependencies [3129982]
- Updated dependencies [3129982]
- jazz-react-auth-clerk@0.13.4
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.13
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
- jazz-react-auth-clerk@0.13.3
## 0.0.12
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multiauth",
"private": true,
"version": "0.0.12",
"version": "0.0.15",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,35 @@
# jazz-example-musicplayer
## 0.0.96
### Patch Changes
- Updated dependencies [08ae9b2]
- Updated dependencies [fe6f561]
- jazz-inspector@0.13.5
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.95
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
- jazz-inspector@0.13.4
## 0.0.94
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- Updated dependencies [017f6c8]
- jazz-tools@0.13.3
- jazz-inspector@0.13.3
- jazz-react@0.13.3
## 0.0.93
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.93",
"version": "0.0.96",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -0,0 +1,54 @@
import { BrowserContext, test } from "@playwright/test";
import { HomePage } from "./pages/HomePage";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function mockAuthenticator(context: BrowserContext) {
await context.addInitScript(() => {
Object.defineProperty(window.navigator, "credentials", {
value: {
...window.navigator.credentials,
create: async () => ({
type: "public-key",
id: new Uint8Array([1, 2, 3, 4]),
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
clientDataJSON: new Uint8Array([1]),
attestationObject: new Uint8Array([2]),
},
}),
get: async () => ({
type: "public-key",
id: new Uint8Array([1, 2, 3, 4]),
rawId: new Uint8Array([1, 2, 3, 4]),
response: {
authenticatorData: new Uint8Array([1]),
clientDataJSON: new Uint8Array([2]),
signature: new Uint8Array([3]),
},
}),
},
configurable: true,
});
});
}
// Configure the authenticator
test.beforeEach(async ({ context }) => {
// Enable virtual authenticator environment
await mockAuthenticator(context);
});
test("sign up and log out", async ({ page: marioPage }) => {
await marioPage.goto("/");
const marioHome = new HomePage(marioPage);
await marioHome.signUp("Mario");
await marioHome.logoutButton.waitFor({
state: "visible",
});
await marioHome.logOut();
});

View File

@@ -11,8 +11,12 @@ export class HomePage {
name: "Playlist title",
});
loginButton = this.page.getByRole("button", {
name: "Sign up",
});
logoutButton = this.page.getByRole("button", {
name: "Logout",
name: "Sign out",
});
async expectActiveTrackPlaying() {
@@ -131,12 +135,21 @@ export class HomePage {
await this.page
.getByRole("button", { name: "Sign up with passkey" })
.click();
await expect(
this.page.getByRole("button", { name: "Sign out" }),
).toBeVisible();
await this.logoutButton.waitFor({
state: "visible",
});
await expect(this.logoutButton).toBeVisible();
}
async logout() {
async logOut() {
await this.logoutButton.click();
await this.loginButton.waitFor({
state: "visible",
});
await expect(this.loginButton).toBeVisible();
}
}

View File

@@ -1,5 +1,30 @@
# organization
## 0.0.68
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.67
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.66
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.65
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.65",
"version": "0.0.68",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,23 @@
# passkey-svelte
## 0.0.62
### Patch Changes
- jazz-svelte@0.13.5
## 0.0.61
### Patch Changes
- jazz-svelte@0.13.4
## 0.0.60
### Patch Changes
- jazz-svelte@0.13.3
## 0.0.59
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.59",
"version": "0.0.62",
"type": "module",
"private": true,
"scripts": {

View File

@@ -1,5 +1,30 @@
# minimal-auth-passkey
## 0.0.73
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.72
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.71
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.70
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.70",
"version": "0.0.73",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# passphrase
## 0.0.70
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.69
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.68
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.67
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passphrase",
"private": true,
"version": "0.0.67",
"version": "0.0.70",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# jazz-password-manager
## 0.0.94
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.93
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.92
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.91
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.91",
"version": "0.0.94",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# jazz-example-pets
## 0.0.192
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.191
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.190
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.189
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.189",
"version": "0.0.192",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# reactions
## 0.0.72
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.71
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.70
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.69
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.69",
"version": "0.0.72",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,33 @@
# todo-vue
## 0.0.76
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-browser@0.13.5
- jazz-vue@0.13.5
## 0.0.75
### Patch Changes
- Updated dependencies [3129982]
- jazz-browser@0.13.4
- jazz-tools@0.13.4
- jazz-vue@0.13.4
## 0.0.74
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-browser@0.13.3
- jazz-vue@0.13.3
## 0.0.73
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.73",
"version": "0.0.76",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,30 @@
# jazz-example-todo
## 0.0.191
### Patch Changes
- Updated dependencies [fe6f561]
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.190
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
## 0.0.189
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- jazz-tools@0.13.3
- jazz-react@0.13.3
## 0.0.188
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.188",
"version": "0.0.191",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,35 @@
# version-history
## 0.0.69
### Patch Changes
- Updated dependencies [08ae9b2]
- Updated dependencies [fe6f561]
- jazz-inspector@0.13.5
- jazz-tools@0.13.5
- jazz-react@0.13.5
## 0.0.68
### Patch Changes
- Updated dependencies [3129982]
- jazz-react@0.13.4
- jazz-tools@0.13.4
- jazz-inspector@0.13.4
## 0.0.67
### Patch Changes
- Updated dependencies [12f8bfa]
- Updated dependencies [bd57177]
- Updated dependencies [017f6c8]
- jazz-tools@0.13.3
- jazz-inspector@0.13.3
- jazz-react@0.13.3
## 0.0.66
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.66",
"version": "0.0.69",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -0,0 +1,30 @@
import clsx from "clsx";
export function Icon404({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 556 517"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={clsx(className)}
>
<path
d="M449.244 300.893C406.744 291.059 313.398 281.691 246.244 308.893C167.244 340.893 125.077 395.393 108.244 417.893"
stroke="currentColor"
strokeWidth="20"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M137.404 194H118.997V295.03C117.252 293.83 115.333 292.831 113.284 292.066C108.621 290.324 103.49 289.869 98.54 290.756C93.5899 291.644 89.043 293.836 85.4742 297.053C81.9054 300.271 79.475 304.371 78.4903 308.834C77.5057 313.297 78.0111 317.924 79.9425 322.128C81.8739 326.332 85.1447 329.925 89.3411 332.454C93.5376 334.982 98.4713 336.331 103.518 336.331C122.233 336.331 137.403 321.161 137.403 302.446L137.404 301.191V194Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M468.404 100H449.997V201.03C448.252 199.83 446.333 198.831 444.284 198.066C439.621 196.324 434.49 195.869 429.54 196.756C424.59 197.644 420.043 199.836 416.474 203.053C412.905 206.271 410.475 210.371 409.49 214.834C408.506 219.297 409.011 223.924 410.942 228.128C412.874 232.332 416.145 235.925 420.341 238.454C424.538 240.982 429.471 242.331 434.518 242.331C453.233 242.331 468.403 227.161 468.403 208.446L468.404 207.191V100Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,12 +1,18 @@
import { clsx } from "clsx";
export function JazzLogo({ className }: { className?: string }) {
export function JazzLogo({
className,
width = undefined,
height = undefined,
}: { className?: string; width?: number; height?: number }) {
return (
<svg
viewBox="0 0 386 146"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={clsx(className, "text-black dark:text-white")}
width={width}
height={height}
>
<path
d="M176.725 33.865H188.275V22.7H176.725V33.865ZM164.9 129.4H172.875C182.72 129.4 188.275 123.9 188.275 114.22V43.6H176.725V109.545C176.725 115.65 173.975 118.51 167.925 118.51H164.9V129.4ZM245.298 53.28C241.613 45.47 233.363 41.95 222.748 41.95C208.998 41.95 200.748 48.44 197.888 58.615L208.613 61.915C210.648 55.315 216.368 52.565 222.638 52.565C231.933 52.565 235.673 56.415 236.058 64.61C226.433 65.93 216.643 67.195 209.768 69.23C200.583 72.145 195.743 77.865 195.743 86.83C195.743 96.51 202.673 104.65 215.818 104.65C225.443 104.65 232.318 101.35 237.213 94.365V103H247.388V66.425C247.388 61.475 247.168 57.185 245.298 53.28ZM217.853 95.245C210.483 95.245 207.128 91.34 207.128 86.72C207.128 82.045 210.593 79.515 215.323 77.92C220.328 76.435 226.983 75.5 235.948 74.18C235.893 76.93 235.673 80.725 234.738 83.475C233.418 89.25 227.643 95.245 217.853 95.245ZM251.22 103H301.545V92.715H269.535L303.195 45.47V43.6H254.3V53.885H284.935L251.22 101.185V103ZM304.815 103H355.14V92.715H323.13L356.79 45.47V43.6H307.895V53.885H338.53L304.815 101.185V103Z"

View File

@@ -57,7 +57,9 @@ export function Footer({
key={index}
className="flex flex-col gap-2 text-sm col-span-6 md:col-span-2"
>
<h2 className="font-medium">{section.title}</h2>
<h2 className="font-medium dark:text-stone-700 cursor-default">
{section.title}
</h2>
{section.links.map((link, linkIndex) => (
<FooterLink
key={linkIndex}

View File

@@ -0,0 +1,49 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { ImageResponse } from "next/og";
import { JazzLogo } from "../atoms/logos/JazzLogo";
export const imageSize = {
width: 1200,
height: 630,
};
export const imageContentType = "image/png";
export default async function OpenGraphImage({ title }: { title: string }) {
const manropeSemiBold = await readFile(
join(process.cwd(), "public/fonts/Manrope-SemiBold.ttf"),
);
return new ImageResponse(
<div
style={{
fontSize: "7em",
background: "white",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: "77px",
letterSpacing: "-0.05em",
}}
>
{title}
<div
style={{ display: "flex", position: "absolute", bottom: 35, right: 45 }}
>
<JazzLogo width={193} height={73} />
</div>
</div>,
{
...imageSize,
fonts: [
{
name: "Manrope",
data: manropeSemiBold,
},
],
},
);
}

View File

@@ -1,5 +1,5 @@
import { PackageDocs } from "@/components/docs/packageDocs";
import { packages } from "@/lib/packages";
import { packages } from "@/content/packages";
import { notFound } from "next/navigation";
interface Props {

View File

@@ -1,4 +1,3 @@
import { TocProvider } from "@/components/TocProvider";
import { ApiNav } from "@/components/docs/ApiNav";
import DocsLayout from "@/components/docs/DocsLayout";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
@@ -9,10 +8,8 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<TocProvider>
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
</DocsLayout>
</TocProvider>
<DocsLayout nav={<ApiNav />} navIcon="package" navName="API Ref">
<Prose className="overflow-x-hidden lg:flex-1 py-10">{children}</Prose>
</DocsLayout>
);
}

View File

@@ -1,4 +1,4 @@
import { packages } from "@/lib/packages";
import { packages } from "@/content/packages";
import { clsx } from "clsx";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
import type { Metadata } from "next";

View File

@@ -1,413 +0,0 @@
export const metadata = { title: "Authentication methods" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Authentication in Jazz
Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user.
When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally.
Without any additional steps the user can use Jazz normally, but they would be limited to use on only one device.
To make Accounts work across devices, you can store/retrieve the account keys from an authentication method by using the corresponding hooks and providers.
<ContentByFramework framework={["react", "vue", "svelte"]}>
## Authentication with passkeys [!framework=react,vue,svelte]
Passkey authentication is fully local-first and the most secure of the auth methods that Jazz provides (because keys are managed by the device/operating system itself).
It is based on the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) and is both very easy to use (using familiar FaceID/TouchID flows) and widely supported.
<ContentByFramework framework="react">
Using passkeys in Jazz is as easy as this:
<CodeGroup>
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const auth = usePasskeyAuth({ // Must be inside the JazzProvider!
appName: "My super-cool web app",
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return <div>You are already signed in</div>;
}
const handleSignUp = async () => {
await auth.signUp(username);
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn();
onOpenChange(false);
};
return (
<div>
<button onClick={handleLogIn}>Log in</button>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
<button onClick={handleSignUp}>Sign up</button>
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
You can try our passkey authentication using our [passkey example](https://passkey-demo.jazz.tools/) or the [music player demo](https://music-demo.jazz.tools/).
</ContentByFramework>
## Passphrase-based authentication
Passphrase authentication lets users log into any device using a Bitcoin-style passphrase. This means users are themselves responsible for storing the passphrase safely.
The passphrase is generated from the local account certificate using a wordlist of your choice.
You can find a set of ready-to-use wordlists in the [bip39](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) repository.
<ContentByFramework framework="react">
For example:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { englishWordlist } from "./wordlist"
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({ // Must be inside the JazzProvider!
wordlist: englishWordlist,
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return <div>You are already signed in</div>;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
<div>
<label>
Your current passphrase
<textarea
readOnly
value={auth.passphrase}
rows={5}
/>
</label>
<button onClick={handleSignUp}>I have stored my passphrase</button>
<label>
Log in with your passphrase
<textarea
value={loginPassphrase}
onChange={(e) => setLoginPassphrase(e.target.value)}
placeholder="Enter your passphrase"
rows={5}
required
/>
</label>
<button onClick={handleLogIn}>Log in</button>
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework={["react-native", "react-native-expo"]}>
For example:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { View, TextInput, Button, Text } from 'react-native';
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({
wordlist: englishWordlist,
});
if (auth.state === "signedIn") {
return <Text>You are already signed in</Text>;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
<View>
<View>
<Text>Your current passphrase</Text>
<TextInput
editable={false}
value={auth.passphrase}
multiline
numberOfLines={5}
/>
</View>
<Button
title="I have stored my passphrase"
onPress={handleSignUp}
/>
<View>
<Text>Log in with your passphrase</Text>
<TextInput
value={loginPassphrase}
onChangeText={setLoginPassphrase}
placeholder="Enter your passphrase"
multiline
numberOfLines={5}
required
/>
</View>
<Button
title="Log in"
onPress={handleLogIn}
/>
</View>
);
}
```
</CodeGroup>
</ContentByFramework>
You can try our passphrase authentication using our [passphrase example](https://passphrase-demo.jazz.tools/) or the [todo list demo](https://todo-demo.jazz.tools/).
<ContentByFramework framework={["react", "react-native-expo"]}>
## Integration with Clerk
Jazz can be used with [Clerk](https://clerk.com/) to authenticate users.
This authentication method is not fully local-first, because the login and signup need to be done while online. Clerk and anyone who is an admin in the app's Clerk org are trusted with the user's key secret and could impersonate them.
However, once authenticated, your users won't need to interact with Clerk anymore, and are able to use all of Jazz's features without needing to be online.
</ContentByFramework>
<ContentByFramework framework="react">
The clerk provider is not built into `jazz-react` and needs the `jazz-react-auth-clerk` package to be installed.
After installing the package you can use the `JazzProviderWithClerk` component to wrap your app:
</ContentByFramework>
<ContentByFramework framework="react-native-expo">
You can use the `JazzProviderWithClerk` component to wrap your app. Note the `__experimental_resourceCache` option. This helps render Clerk components when offline.
</ContentByFramework>
<ContentByFramework framework="react">
<CodeGroup>
```tsx
import { JazzProviderWithClerk } from "jazz-react-auth-clerk";
function JazzProvider({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
<JazzProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzProviderWithClerk>
);
}
createRoot(document.getElementById("root")!).render(
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<JazzProvider>
<App />
</JazzProvider>
</ClerkProvider>
);
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native-expo">
<CodeGroup>
```tsx
import { JazzProviderWithClerk } from "jazz-expo/auth/clerk";
import { secureStore } from "@clerk/clerk-expo/secure-store";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
<JazzProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzProviderWithClerk>
);
}
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!publishableKey) {
throw new Error(
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
);
}
return (
<ClerkProvider
tokenCache={tokenCache}
publishableKey={publishableKey}
__experimental_resourceCache={secureStore}
>
<ClerkLoaded>
<JazzAndAuth>
<Slot />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
);
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react">
Then you can use the [Clerk auth methods](https://clerk.com/docs/references/react/overview) to log in and sign up:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { SignInButton } from "@clerk/clerk-react";
import { useAccount, useIsAuthenticated } from "jazz-react";
export function AuthButton() {
const { logOut } = useAccount();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <button onClick={() => logOut()}>Logout</button>;
}
return <SignInButton />;
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native-expo">
Then you can use the [Clerk auth methods](https://clerk.com/docs/references/expo/overview) to log in and sign up:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { SignInButton } from "@clerk/clerk-expo";
import { useAccount, useIsAuthenticated } from "jazz-expo";
export function AuthButton() {
const { logOut } = useAccount();
const { signIn, setActive, isLoaded } = useSignIn();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <button onClick={() => logOut()}>Logout</button>;
}
// Login code with Clerk Expo
}
```
</CodeGroup>
</ContentByFramework>
## Migrating data from anonymous to authenticated account
You may want allow your users to use your app without authenticating (a poll response for example). When *signing up* their anonymous account is transparently upgraded using the provided auth method, keeping the data stored in the account intact.
However, a user may realise that they already have an existing account *after using the app anonymously and having already stored data in the anonymous account*.
When they now *log in*, by default the anonymous account will be discarded and this could lead to unexpected data loss.
To avoid this situation we provide the `onAnonymousAccountDiscarded` handler to migrate the data from the anonymous account to the existing authenticated one.
This is an example from our [music player](https://github.com/garden-co/jazz/tree/main/examples/music-player):
<CodeGroup>
```ts
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
root: {
rootPlaylist: {
tracks: [{}],
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
},
},
});
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}
}
```
</CodeGroup>
To see how this works in reality we suggest you to try
to upload a song in the [music player demo](https://music-demo.jazz.tools/) and then
try to log in with an existing account.
## Disable network sync for anonymous users
You can disable network sync to make your app local-only under specific circumstances.
For example, you may want to give the opportunity to non-authenticated users to try your app locally-only (incurring no sync traffic), then enable the network sync only when the user is authenticated:
<CodeGroup>
```tsx
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
// This makes the app work in local mode when the user is anonymous
when: "signedUp",
}}
>
<App />
</JazzProvider>
```
</CodeGroup>
For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`.

View File

@@ -1,117 +0,0 @@
import { TocItemsSetter } from "@/components/docs/TocItemsSetter";
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { Framework, frameworks } from "@/lib/framework";
import type { Toc } from "@stefanprobst/rehype-extract-toc";
async function getMdxSource(framework: string, slugPath?: string) {
// Try to import the framework-specific file first
try {
if (!slugPath) {
return await import("./index.mdx");
}
return await import(`./${slugPath}/${framework}.mdx`);
} catch (error) {
// Fallback to vanilla
return await import(`./${slugPath}.mdx`);
}
}
export async function generateMetadata({
params,
}: { params: Promise<{ slug: string[]; framework: string }> }) {
const { slug, framework } = await params;
const slugPath = slug?.join("/");
try {
const mdxSource = await getMdxSource(framework, slugPath);
const title = mdxSource.tableOfContents?.[0].value || "Documentation";
return {
title,
openGraph: {
title,
},
};
} catch (error) {
return {
title: "Documentation",
openGraph: {
title: "Documentation",
},
};
}
}
export default async function Page({
params,
}: { params: Promise<{ slug: string[]; framework: string }> }) {
const { slug, framework } = await params;
const slugPath = slug?.join("/");
try {
const mdxSource = await getMdxSource(framework, slugPath);
const {
default: Content,
tableOfContents,
headingsFrameworkVisibility,
test,
} = mdxSource;
// Remove items that should not be shown for the current framework
const tocItems = (tableOfContents as Toc).filter(({ id }) =>
id && id in headingsFrameworkVisibility
? headingsFrameworkVisibility[id]?.includes(framework)
: true,
);
return (
<>
<TocItemsSetter items={tocItems} />
<Content />
</>
);
} catch (error) {
return (
<>
<TocItemsSetter items={[]} />
<ComingSoonPage />
</>
);
}
}
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
export const dynamicParams = false;
export const dynamic = "force-static";
export async function generateStaticParams() {
const paths: Array<{ slug?: string[]; framework: Framework }> = [];
for (const framework of frameworks) {
paths.push({
framework,
slug: [],
});
for (const heading of docNavigationItems) {
for (const item of heading?.items) {
if (item.href && item.href.startsWith("/docs")) {
const slug = item.href
.replace("/docs", "")
.split("/")
.filter(Boolean);
if (slug.length) {
paths.push({
slug,
framework,
});
}
}
}
}
}
return paths;
}

View File

@@ -0,0 +1,57 @@
import { docNavigationItems } from "@/content/docs/docNavigationItems.js";
import { Framework, frameworks } from "@/content/framework";
import { DocPage, getDocMetadata } from "@/lib/docMdxContent";
export async function generateMetadata({
params,
}: {
params: Promise<{ topic: string; subtopic: string; framework: string }>;
}) {
const { topic, subtopic, framework } = await params;
return getDocMetadata(framework, [topic, subtopic]);
}
export default async function Page({
params,
}: {
params: Promise<{ topic: string; subtopic: string; framework: string }>;
}) {
const { topic, subtopic, framework } = await params;
return <DocPage framework={framework} slug={[topic, subtopic]} />;
}
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
export const dynamicParams = false;
export const dynamic = "force-static";
export async function generateStaticParams() {
const paths: Array<{
topic?: string;
subtopic?: string;
framework: Framework;
}> = [];
for (const framework of frameworks) {
for (const heading of docNavigationItems) {
for (const item of heading?.items) {
if (item.href && item.href.startsWith("/docs")) {
const [topic, subtopic] = item.href
.replace("/docs", "")
.split("/")
.filter(Boolean);
if (topic && subtopic) {
paths.push({
topic,
subtopic,
framework,
});
}
}
}
}
}
return paths;
}

View File

@@ -0,0 +1,52 @@
import { docNavigationItems } from "@/content/docs/docNavigationItems.js";
import { Framework, frameworks } from "@/content/framework";
import { DocPage, getDocMetadata } from "@/lib/docMdxContent";
export async function generateMetadata({
params,
}: {
params: Promise<{ topic: string; framework: string }>;
}) {
const { topic, framework } = await params;
return getDocMetadata(framework, [topic]);
}
export default async function Page({
params,
}: {
params: Promise<{ topic: string; framework: string }>;
}) {
const { topic, framework } = await params;
return <DocPage framework={framework} slug={[topic]} />;
}
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
export const dynamicParams = false;
export const dynamic = "force-static";
export async function generateStaticParams() {
const paths: Array<{ topic?: string; framework: Framework }> = [];
for (const framework of frameworks) {
for (const heading of docNavigationItems) {
for (const item of heading?.items) {
if (item.href && item.href.startsWith("/docs")) {
const [topic] = item.href
.replace("/docs", "")
.split("/")
.filter(Boolean);
if (topic) {
paths.push({
topic,
framework,
});
}
}
}
}
}
return paths;
}

View File

@@ -0,0 +1,38 @@
import { Framework, frameworks } from "@/content/framework";
import { DocPage, getDocMetadata } from "@/lib/docMdxContent";
export async function generateMetadata({
params,
}: {
params: Promise<{ framework: string }>;
}) {
const { framework } = await params;
return getDocMetadata(framework, []);
}
export default async function Page({
params,
}: {
params: Promise<{ framework: string }>;
}) {
const { framework } = await params;
return <DocPage framework={framework} slug={[]} />;
}
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
export const dynamicParams = false;
export const dynamic = "force-static";
export async function generateStaticParams() {
const paths: Array<{ framework: Framework }> = [];
for (const framework of frameworks) {
paths.push({
framework,
});
}
return paths;
}

View File

@@ -1,16 +0,0 @@
import { TocProvider } from "@/components/TocProvider";
import DocsLayout from "@/components/docs/DocsLayout";
import { DocNav } from "@/components/docs/nav";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<TocProvider>
<DocsLayout nav={<DocNav />}>
<Prose className="overflow-x-hidden lg:flex-1 py-10 max-w-3xl mx-auto">
{children}
</Prose>
</DocsLayout>
</TocProvider>
);
}

View File

@@ -5,7 +5,7 @@ import { ReactLogo } from "@/components/icons/ReactLogo";
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
import { SvelteLogo } from "@/components/icons/SvelteLogo";
import { VueLogo } from "@/components/icons/VueLogo";
import { Example, features, tech } from "@/lib/example";
import { Example, features, tech } from "@/content/example";
import { clsx } from "clsx";
import { H2 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";

View File

@@ -1,4 +1,4 @@
import { products } from "@/lib/showcase";
import { products } from "@/content/showcase";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import type { Metadata } from "next";
import Image from "next/image";

View File

@@ -2,12 +2,11 @@ import LatencyChart from "@/components/LatencyChart";
import { clsx } from "clsx";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import type { Metadata } from "next";
import dynamic from "next/dynamic";
import { Fragment } from "react";
const title = "Status";
export const revalidate = 300;
export const dynamic = "force-static";
export const metadata: Metadata = {
title,
@@ -41,6 +40,10 @@ interface DataRow {
const query = async () => {
const res = await fetch("https://gcmp.grafana.net/api/ds/query", {
cache: "force-cache",
next: {
revalidate: 300,
},
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -6,6 +6,7 @@ import localFont from "next/font/local";
import { ThemeProvider } from "@/components/ThemeProvider";
import { JazzFooter } from "@/components/footer";
import { marketingCopy } from "@/content/marketingCopy";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
@@ -40,9 +41,8 @@ const commitMono = localFont({
});
const metaTags = {
title: "Jazz - Whip up an app",
description:
"Jazz gives you data without needing a database — plus auth, permissions, files and multiplayer without needing a backend. Do everything right from the frontend and ship better apps, faster.",
title: `Jazz - ${marketingCopy.headline}`,
description: marketingCopy.description,
url: "https://jazz.tools",
};
@@ -63,13 +63,6 @@ export const metadata: Metadata = {
description: metaTags.description,
url: metaTags.url,
siteName: "Jazz",
images: [
{
url: "/social-image.png",
width: 1200,
height: 630,
},
],
},
};

View File

@@ -0,0 +1,13 @@
import { Icon404 } from "gcmp-design-system/src/app/components/atoms/icons/404";
export default function NotFound() {
const text = "Don't Worry 'Bout Me";
return (
<div className="flex flex-col items-flex-start justify-center min-h-[95vh] w-auto pb-32">
<Icon404 className="w-75" />
<h1 className="text-3xl font-semibold my-7">{text}</h1>
<p>Page not found.</p>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { marketingCopy } from "@/content/marketingCopy";
import OpenGraphImage, {
imageSize,
imageContentType,
} from "gcmp-design-system/src/app/components/organisms/OpenGraphImage";
export const title = marketingCopy.headline;
export const size = imageSize;
export const contentType = imageContentType;
export const alt = marketingCopy.headline;
export default async function Image() {
return OpenGraphImage({ title });
}

View File

@@ -1,6 +1,6 @@
import { SideNavHeader } from "@/components/SideNavHeader";
import { SideNavItem } from "@/components/SideNavItem";
import { Framework } from "@/lib/framework";
import { Framework } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
import { clsx } from "clsx";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";

View File

@@ -1,15 +0,0 @@
"use client";
import { TocContext } from "@/lib/TocContext";
import type { Toc, TocEntry } from "@stefanprobst/rehype-extract-toc";
import { useState } from "react";
export function TocProvider({ children }: { children: React.ReactNode }) {
const [tocItems, setTocItems] = useState<Toc>();
return (
<TocContext.Provider value={{ tocItems, setTocItems }}>
{children}
</TocContext.Provider>
);
}

View File

@@ -1,6 +1,6 @@
import { SideNavHeader } from "@/components/SideNavHeader";
import { SideNavItem } from "@/components/SideNavItem";
import { packages } from "@/lib/packages";
import { packages } from "@/content/packages";
import { clsx } from "clsx";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
import Link from "next/link";

View File

@@ -2,7 +2,7 @@
import { TableOfContents } from "@/components/docs/TableOfContents";
import { JazzNav } from "@/components/nav";
import { useTocItems } from "@/lib/TocContext";
import { TocEntry } from "@stefanprobst/rehype-extract-toc";
import { clsx } from "clsx";
import type { IconName } from "gcmp-design-system/src/app/components/atoms/Icon";
import type { NavSection } from "gcmp-design-system/src/app/components/organisms/Nav";
@@ -12,14 +12,14 @@ export default function DocsLayout({
nav,
navName,
navIcon,
tocItems,
}: {
children: React.ReactNode;
nav?: React.ReactNode;
navName?: string;
navIcon?: IconName;
tocItems?: TocEntry[];
}) {
const { tocItems } = useTocItems();
const navSections: NavSection[] = [
{
name: navName || "Docs",

View File

@@ -1,6 +1,6 @@
"use client";
import { isValidFramework } from "@/lib/framework";
import { isValidFramework } from "@/content/framework";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";

View File

@@ -1,6 +1,6 @@
"use client";
import { Framework } from "@/lib/framework";
import { Framework } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";

View File

@@ -1,15 +0,0 @@
"use client";
import { useTocItems } from "@/lib/TocContext";
import type { TocEntry } from "@stefanprobst/rehype-extract-toc";
import { useEffect } from "react";
export function TocItemsSetter({ items }: { items: TocEntry[] | undefined }) {
const { setTocItems } = useTocItems();
useEffect(() => {
setTocItems(items);
}, [items, setTocItems]);
return null;
}

View File

@@ -3,7 +3,7 @@
import { SideNav } from "@/components/SideNav";
import { SideNavHeader } from "@/components/SideNavHeader";
import { FrameworkSelect } from "@/components/docs/FrameworkSelect";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { docNavigationItems } from "@/content/docs/docNavigationItems.js";
import { useFramework } from "@/lib/use-framework";
import { clsx } from "clsx";

View File

@@ -1,6 +1,6 @@
import { ExampleLinks } from "@/components/examples/ExampleLinks";
import { ExampleTags } from "@/components/examples/ExampleTags";
import { Example } from "@/lib/example";
import { Example } from "@/content/example";
import { clsx } from "clsx";
export function ExampleCard({

View File

@@ -1,7 +1,7 @@
import { CodeExampleTabs } from "@/components/examples/CodeExampleTabs";
import { ExampleLinks } from "@/components/examples/ExampleLinks";
import { ExampleTags } from "@/components/examples/ExampleTags";
import { Example } from "@/lib/example";
import { Example } from "@/content/example";
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
export function ExampleDemo({ example }: { example: Example }) {

View File

@@ -1,6 +1,6 @@
"use client";
import { Example } from "@/lib/example";
import { Example } from "@/content/example";
import { InterpolateInCode } from "@/mdx-components";
import { DialogDescription } from "@headlessui/react";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";

View File

@@ -1,4 +1,4 @@
import { Example } from "@/lib/example";
import { Example } from "@/content/example";
export function ExampleTags({ example }: { example: Example }) {
const { tech, features } = example;

View File

@@ -1,5 +1,5 @@
import { ThemeToggle } from "@/components/ThemeToggle";
import { socials } from "@/lib/socials";
import { socials } from "@/content/socials";
import { GcmpLogo } from "gcmp-design-system/src/app/components/atoms/logos/GcmpLogo";
import { Footer } from "gcmp-design-system/src/app/components/organisms/Footer";

View File

@@ -1,4 +1,5 @@
import CreateJazzApp from "@/components/home/CreateJazzApp.mdx";
import { marketingCopy } from "@/content/marketingCopy";
import { H1 } from "gcmp-design-system/src/app/components/atoms/Headings";
import {
Icon,
@@ -54,7 +55,7 @@ export function HeroSection() {
<div className="flex flex-col justify-center gap-5 lg:col-span-2 lg:gap-8">
<Kicker>Toolkit for backendless apps</Kicker>
<H1>
<span className="inline-block">Whip up an app.</span>
<span className="inline-block">{marketingCopy.headline}</span>
</H1>
<Prose size="lg" className="text-pretty max-w-2xl dark:text-stone-200">

View File

@@ -1,5 +1,5 @@
import { ThemeToggle } from "@/components/ThemeToggle";
import { socials } from "@/lib/socials";
import { socials } from "@/content/socials";
import { JazzLogo } from "gcmp-design-system/src/app/components/atoms/logos/JazzLogo";
import {
Nav,

View File

@@ -0,0 +1,227 @@
export const metadata = { title: "Authentication States" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Authentication States
Jazz provides three distinct authentication states that determine how users interact with your app: **Anonymous Authentication**, **Guest Mode**, and **Authenticated Account**.
## Anonymous Authentication
When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally:
- Users have full accounts with unique IDs
- Data persists between sessions on the same device
- Can be upgraded to a full account (passkey, passphrase, etc.)
- Data syncs across the network (if enabled)
## Authenticated Account
**Authenticated Account** provides full multi-device functionality:
- Persistent identity across multiple devices
- Full access to all application features
- Data can sync across all user devices
- Multiple authentication methods available
## Guest Mode
**Guest Mode** provides a completely accountless context:
- No persistent identity or account
- Only provides access to publicly readable content
- Cannot save or sync user-specific data
- Suitable for read-only access to public resources
## Detecting Authentication State
You can detect the current authentication state using `useAccountOrGuest` and `useIsAuthenticated`.
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import * as React from "react";
// ---cut---
import { useAccountOrGuest, useIsAuthenticated } from "jazz-react";
function AuthStateIndicator() {
const { me } = useAccountOrGuest();
const isAuthenticated = useIsAuthenticated();
// Check if guest mode is enabled in JazzProvider
const isGuest = me._type !== "Account"
// Anonymous authentication: has an account but not fully authenticated
const isAnonymous = me._type === "Account" && !isAuthenticated;
return (
<div>
{isGuest && <span>Guest Mode</span>}
{isAnonymous && <span>Anonymous Account</span>}
{isAuthenticated && <span>Authenticated</span>}
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
## Migrating data from anonymous to authenticated account
When a user signs up, their anonymous account is transparently upgraded to an authenticated account, preserving all their data.
However, if a user has been using your app anonymously and later logs in with an existing account, their anonymous account data would normally be discarded. To prevent data loss, you can use the `onAnonymousAccountDiscarded` handler.
This example from our [music player example app](https://github.com/garden-co/jazz/tree/main/examples/music-player) shows how to migrate data:
<CodeGroup>
```ts twoslash
import { Account, Group, CoMap, co, CoList } from "jazz-tools";
class MusicTrack extends CoMap {
title = co.string;
duration = co.number;
isExampleTrack = co.optional.boolean;
}
class ListOfTracks extends CoList.Of(co.ref(MusicTrack)) {}
class Playlist extends CoMap {
title = co.string;
tracks = co.ref(ListOfTracks);
}
class MusicaAccountRoot extends CoMap {
rootPlaylist = co.ref(Playlist);
}
class MusicaAccount extends Account {
root = co.ref(MusicaAccountRoot);
}
// ---cut---
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: {
$each: true,
},
},
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
},
},
});
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}
}
```
</CodeGroup>
To see how this works, try uploading a song in the [music player demo](https://music-demo.jazz.tools/) and then log in with an existing account.
## Controlling sync for different authentication states
You can control network sync with [Providers](/docs/project-setup/providers) based on authentication state:
- `when: "always"`: Sync is enabled for both Anonymous Authentication and Authenticated Account
- `when: "signedUp"`: Sync is enabled when the user is authenticated
- `when: "never"`: Sync is disabled, content stays local
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { JazzProvider } from "jazz-react";
const apiKey = "you@example.com";
function App() {
return <div>Hello World</div>;
}
// ---cut---
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
// Controls when sync is enabled for
// both Anonymous Authentication and Authenticated Account
when: "always", // or "signedUp" or "never"
}}
>
<App />
</JazzProvider>
```
</CodeGroup>
</ContentByFramework>
### Disable sync for Anonymous Authentication
You can disable network sync to make your app local-only under specific circumstances.
For example, you may want to give users with Anonymous Authentication the opportunity to try your app locally-only (incurring no sync traffic), then enable network sync only when the user is fully authenticated.
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { JazzProvider } from "jazz-react";
const apiKey = "you@example.com";
function App() {
return <div>Hello World</div>;
}
// ---cut---
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
// This makes the app work in local mode when using Anonymous Authentication
when: "signedUp",
}}
>
<App />
</JazzProvider>
```
</CodeGroup>
</ContentByFramework>
### Configuring Guest Mode Access
You can configure Guest Mode access with the `guestMode` prop for [Providers](/docs/project-setup/providers).
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { JazzProvider } from "jazz-react";
const apiKey = "you@example.com";
function App() {
return <div>Hello World</div>;
}
// ---cut---
<JazzProvider
// Enable Guest Mode for public content
guestMode={true}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
// Only sync for authenticated users
when: "signedUp",
}}
>
<App />
</JazzProvider>
```
</CodeGroup>
</ContentByFramework>
For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`.

View File

@@ -0,0 +1,221 @@
export const metadata = { title: "Clerk Authentication" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Clerk Authentication
Jazz can be integrated with [Clerk](https://clerk.com/) to authenticate users. This method combines Clerk's comprehensive authentication services with Jazz's local-first capabilities.
## How it works
When using Clerk authentication:
1. Users sign up or sign in through Clerk's authentication system
2. Jazz securely stores the user's account keys with Clerk
3. When logging in, Jazz retrieves these keys from Clerk
4. Once authenticated, users can work offline with full Jazz functionality
This authentication method is not fully local-first, as login and signup need to be done online, but once authenticated, users can use all of Jazz's features without needing to be online.
## Key benefits
- **Rich auth options**: Email/password, social logins, multi-factor authentication
- **User management**: Complete user administration dashboard
- **Familiar sign-in**: Standard auth flows users already know
- **OAuth providers**: Google, GitHub, and other popular providers
- **Enterprise features**: SSO, SAML, and other advanced options
## Implementation
<ContentByFramework framework="react">
We offer Clerk integration through our package: [`jazz-react-auth-clerk`](https://npmjs.com/package/jazz-react-auth-clerk).
After installing, use `<JazzProviderWithClerk />` to wrap your app:
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { createRoot } from "react-dom/client";
const apiKey = "you@example.com";
const PUBLISHABLE_KEY = "fake_key";
function App() {
return <div>Hello World</div>;
}
// ---cut---
import { useClerk, ClerkProvider } from '@clerk/clerk-react';
import { JazzProviderWithClerk } from "jazz-react-auth-clerk";
function JazzProvider({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
<JazzProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzProviderWithClerk>
);
}
createRoot(document.getElementById("root")!).render(
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<JazzProvider>
<App />
</JazzProvider>
</ClerkProvider>
);
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native-expo">
We offer Clerk integration through our package: [`jazz-expo`](https://npmjs.com/package/jazz-expo).
After installing, use `<JazzProviderWithClerk />` to wrap your app.
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { Slot } from "expo-router";
const apiKey = "you@example.com";
const tokenCache = {
getToken: async (key: string) => {
return null;
},
saveToken: async (key: string, token: string) => {},
clearToken: async (key: string) => {},
};
// ---cut---
import { useClerk, ClerkProvider, ClerkLoaded } from '@clerk/clerk-expo';
import { secureStore } from "@clerk/clerk-expo/secure-store";
import { JazzProviderWithClerk } from "jazz-expo/auth/clerk";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
<JazzProviderWithClerk
clerk={clerk}
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzProviderWithClerk>
);
}
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!publishableKey) {
throw new Error(
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
);
}
return (
<ClerkProvider
tokenCache={tokenCache}
publishableKey={publishableKey}
__experimental_resourceCache={secureStore}
>
<ClerkLoaded>
<JazzAndAuth>
<Slot />
</JazzAndAuth>
</ClerkLoaded>
</ClerkProvider>
);
}
```
</CodeGroup>
</ContentByFramework>
Once set up, you can use Clerk's auth methods for login and signup:
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import * as React from "react";
// ---cut---
import { SignInButton } from "@clerk/clerk-react";
import { useAccount, useIsAuthenticated } from "jazz-react";
export function AuthButton() {
const { logOut } = useAccount();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <button onClick={() => logOut()}>Logout</button>;
}
return <SignInButton />;
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native-expo">
<CodeGroup>
```tsx twoslash
import * as React from "react";
// ---cut---
import { useSignIn } from "@clerk/clerk-expo";
import { useAccount, useIsAuthenticated } from "jazz-expo";
import { Button, Text } from 'react-native';
export function AuthButton() {
const { logOut } = useAccount();
const { signIn, setActive, isLoaded } = useSignIn();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <Button title="Logout" onPress={() => logOut()} />;
}
const onSignInPress = async () => {
if (!isLoaded) return;
const signInAttempt = await signIn.create({
identifier: "you@example.com",
password: "password",
});
if (signInAttempt.status === "complete") {
await setActive({ session: signInAttempt.createdSessionId });
}
};
return <Button title="Sign In" onPress={onSignInPress} />;
}
```
</CodeGroup>
</ContentByFramework>
## Examples
You can explore Jazz with Clerk integration in our [example projects](/docs/examples). For more Clerk-specific demos, visit [Clerk's documentation](https://clerk.com/docs).
## When to use Clerk
Clerk authentication is ideal when:
- You need an existing user management system
- You want to integrate with other Clerk features (roles, permissions)
- You require email/password authentication with verification
- You need OAuth providers (Google, GitHub, etc.)
- You want to avoid users having to manage passphrases
## Limitations and considerations
- **Online requirement**: Initial signup/login requires internet connectivity
- **Third-party dependency**: Relies on Clerk's services for authentication
- **Not fully local-first**: Initial authentication requires a server
- **Platform support**: Not available on all platforms
## Additional resources
- [Clerk Documentation](https://clerk.com/docs)
- [Jazz React Auth Clerk Package](https://npmjs.com/package/jazz-react-auth-clerk)

View File

@@ -0,0 +1,3 @@
# Clerk Authentication
We do not currently support Clerk in React Native, but we do have support for [React Native Expo](/docs/react-native-expo/authentication/clerk).

View File

@@ -0,0 +1,37 @@
export const metadata = { title: "Authentication methods" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Authentication in Jazz
Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user.
## Authentication Flow
When a user first opens your app, they'll be in one of these states:
- **Anonymous Authentication**: Default starting point where Jazz automatically creates a local account on first visit. Data persists on one device and can be upgraded to a full account.
- **Authenticated Account**: Full account accessible across multiple devices using [passkeys](./passkey), [passphrases](./passphrase), or third-party authentications, such as [Clerk](./clerk).
- **Guest Mode**: No account, read-only access to public content. Users can browse but can't save data or sync.
Learn more about these states in the [Authentication States](./authentication-states) documentation.
Without authentication, users are limited to using the application on only one device.
When a user logs out of an Authenticated Account, they return to the Anonymous Authentication state with a new local account.
Here's what happens during registration and login:
- **Register**: When a user registers with an authentication provider, their Anonymous account credentials are stored in the auth provider, and the account is marked as Authenticated. The user keeps all their existing data.
- **Login**: When a user logs in with an authentication provider, their Anonymous account is discarded and the credentials are loaded from the auth provider. Data from the Anonymous account can be transferred using the [onAnonymousAccountDiscarded handler](./authentication-states#migrating-data-from-anonymous-to-authenticated-account).
## Available Authentication Methods
Jazz provides several ways to authenticate users:
- [**Passkeys**](./passkey): Secure, biometric authentication using WebAuthn
- [**Passphrases**](./passphrase): Bitcoin-style word phrases that users store
- [**Clerk Integration**](./clerk): Third-party authentication service with OAuth support

View File

@@ -0,0 +1,97 @@
export const metadata = { title: "Passkey Authentication" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Passkey Authentication
Passkey authentication is fully local-first and the most secure of the auth methods that Jazz provides because keys are managed by the device/operating system itself.
## How it works
Passkey authentication is based on the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) and uses familiar FaceID/TouchID flows that users already know how to use.
## Key benefits
- **Most secure**: Keys are managed by the device/OS
- **User-friendly**: Uses familiar biometric verification (FaceID/TouchID)
- **Cross-device**: Works across devices with the same biometric authentication
- **No password management**: Users don't need to remember or store anything
- **Wide support**: Available in most modern browsers
## Implementation
<ContentByFramework framework="react">
Using passkeys in Jazz is as easy as this:
<CodeGroup>
```tsx twoslash
import * as React from "react";
import { useState } from "react";
import { usePasskeyAuth } from "jazz-react";
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const auth = usePasskeyAuth({ // Must be inside the JazzProvider!
appName: "My super-cool web app",
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return <div>You are already signed in</div>;
}
const handleSignUp = async () => {
await auth.signUp(username);
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn();
onOpenChange(false);
};
return (
<div>
<button onClick={handleLogIn}>Log in</button>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
<button onClick={handleSignUp}>Sign up</button>
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
## Examples
You can try passkey authentication using our [passkey example](https://passkey-demo.jazz.tools/) or the [music player demo](https://music-demo.jazz.tools/).
## When to use Passkeys
Passkeys are ideal when:
- Security is a top priority
- You want the most user-friendly authentication experience
- You're targeting modern browsers and devices
- You want to eliminate the risk of password-based attacks
## Limitations and considerations
- Requires hardware/OS support for biometric authentication
- Not supported in older browsers (see browser support below)
- Requires a fallback method for unsupported environments
### Browser Support
[Passkeys are supported in most modern browsers](https://caniuse.com/passkeys).
For older browsers, we recommend using [passphrase authentication](./passphrase) as a fallback.
## Additional resources
For more information about the Web Authentication API and passkeys:
- [WebAuthn.io](https://webauthn.io/)
- [MDN Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)

View File

@@ -0,0 +1,194 @@
export const metadata = { title: "Passphrase Authentication" };
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
# Passphrase Authentication
Passphrase authentication lets users log into any device using a recovery phrase consisting of multiple words (similar to cryptocurrency wallets). Users are responsible for storing this passphrase safely.
## How it works
When a user creates an account with passphrase authentication:
1. Jazz generates a unique recovery phrase derived from the user's cryptographic keys
2. This phrase consists of words from a wordlist
3. Users save this phrase and enter it when logging in on new devices
You can use one of the ready-to-use wordlists from the [BIP39 repository](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) or create your own.
## Key benefits
- **Portable**: Works across any device, even without browser or OS support
- **User-controlled**: User manages their authentication phrase
- **Flexible**: Works with any wordlist you choose
- **Offline capable**: No external dependencies
## Implementation
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
// @filename: wordlist.ts
export const wordlist = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lemon", "mango", "orange", "pear", "quince", "raspberry", "strawberry", "tangerine", "uva", "watermelon", "xigua", "yuzu", "zucchini"];
// @filename: AuthModal.tsx
import * as React from "react";
import { useState } from "react";
import { usePassphraseAuth } from "jazz-react";
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
import { wordlist } from "./wordlist"
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({ // Must be inside the JazzProvider!
wordlist: wordlist,
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return <div>You are already signed in</div>;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
<div>
<label>
Your current passphrase
<textarea
readOnly
value={auth.passphrase}
rows={5}
/>
</label>
<button onClick={handleSignUp}>I have stored my passphrase</button>
<label>
Log in with your passphrase
<textarea
value={loginPassphrase}
onChange={(e) => setLoginPassphrase(e.target.value)}
placeholder="Enter your passphrase"
rows={5}
required
/>
</label>
<button onClick={handleLogIn}>Log in</button>
</div>
);
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework={["react-native", "react-native-expo"]}>
<CodeGroup>
```tsx twoslash
// @filename: wordlist.ts
export const wordlist = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lemon", "mango", "orange", "pear", "quince", "raspberry", "strawberry", "tangerine", "uva", "watermelon", "xigua", "yuzu", "zucchini"];
// @filename: AuthModal.tsx
import * as React from "react";
import { View, TextInput, Button, Text } from 'react-native';
import { useState } from "react";
import { usePassphraseAuth } from "jazz-react-native";
type AuthModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ---cut---
import { wordlist } from "./wordlist"
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({
wordlist: wordlist,
});
if (auth.state === "signedIn") {
return <Text>You are already signed in</Text>;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
<View>
<View>
<Text>Your current passphrase</Text>
<TextInput
editable={false}
value={auth.passphrase}
multiline
numberOfLines={5}
/>
</View>
<Button
title="I have stored my passphrase"
onPress={handleSignUp}
/>
<View>
<Text>Log in with your passphrase</Text>
<TextInput
value={loginPassphrase}
onChangeText={setLoginPassphrase}
placeholder="Enter your passphrase"
multiline
numberOfLines={5}
/>
</View>
<Button
title="Log in"
onPress={handleLogIn}
/>
</View>
);
}
```
</CodeGroup>
</ContentByFramework>
## Examples
You can see passphrase authentication in our [passphrase example](https://passphrase-demo.jazz.tools/) or the [todo list demo](https://todo-demo.jazz.tools/).
## When to use Passphrases
Passphrase authentication is ideal when:
- You need to support older browsers without WebAuthn capabilities
- Your users need to access the app on many different devices
- You want a fallback authentication method alongside passkeys
## Limitations and considerations
- **User responsibility**: Users must securely store their passphrase
- **Recovery concerns**: If a user loses their passphrase, they cannot recover their account
- **Security risk**: Anyone with the passphrase can access the account
- **User experience**: Requires users to enter a potentially long phrase
Make sure to emphasize to your users:
1. Store the passphrase in a secure location (password manager, written down in a safe place)
2. The passphrase is the only way to recover their account
3. Anyone with the passphrase can access the account

Some files were not shown because too many files have changed in this diff Show More