Compare commits

...

98 Commits

Author SHA1 Message Date
Guido D'Orsi
f519cd7356 Merge pull request #1337 from garden-co/jazz-689-group-extension-with-role-mapping
feat: add role mapping to group extension
2025-02-11 15:36:15 +01:00
Guido D'Orsi
20babc2e41 docs: add Group suffix on the group variables 2025-02-11 15:26:39 +01:00
Guido D'Orsi
6fc8efc1b5 Apply suggestions from code review
Co-authored-by: Anselm Eickhoff <anselm.eickhoff@gmail.com>
2025-02-11 15:22:05 +01:00
Guido D'Orsi
0b9cc2e0aa Merge branch 'authv2' into jazz-689-group-extension-with-role-mapping 2025-02-11 14:36:06 +01:00
Guido D'Orsi
610543c5b8 chore: changeset 2025-02-11 14:35:41 +01:00
Guido D'Orsi
53dbfee385 fix: fix Svelte tests preprocessing 2025-02-11 14:34:28 +01:00
Guido D'Orsi
9c577b98cd Apply suggestions from code review
Co-authored-by: Anselm Eickhoff <anselm.eickhoff@gmail.com>
2025-02-11 14:33:38 +01:00
Guido D'Orsi
b2c03ca8c4 chore: restore shell: bash on action.yml 2025-02-11 13:01:19 +01:00
Guido D'Orsi
3feb3e82ff docs: small improvement on the wrapper suggestion 2025-02-11 12:59:06 +01:00
Guido D'Orsi
95e9bdb624 chore: update action.yaml to use blacksmith 2025-02-11 12:57:57 +01:00
Guido D'Orsi
68127a95d0 Merge remote-tracking branch 'origin/main' into authv2 2025-02-11 12:56:21 +01:00
Anselm Eickhoff
690e65ac06 Merge pull request #1352 from garden-co/blacksmith-migration-44f38f3
blacksmith.sh: Migrate workflows to Blacksmith
2025-02-11 11:42:01 +00:00
Guido D'Orsi
6f007a9470 Merge remote-tracking branch 'origin/authv2' into jazz-689-group-extension-with-role-mapping 2025-02-11 12:37:55 +01:00
Guido D'Orsi
23c588c717 docs: improve upgrade docs
Co-authored-by: Benjamin S. Leveritt <benjamin@leveritt.co.uk>
2025-02-11 12:34:38 +01:00
blacksmith-sh[bot]
494d902e79 Migrate workflows to Blacksmith 2025-02-11 11:25:06 +00:00
Guido D'Orsi
eebee4db6a docs: group inheritance and upgrade docs 2025-02-11 12:18:40 +01:00
Guido D'Orsi
44f38f3438 Merge pull request #1346 from garden-co/jazz-697-fix-copy-button-alignment
Fix copy button alignment
2025-02-11 11:45:06 +01:00
Guido D'Orsi
950db502c2 Merge pull request #1313 from boorad/feat/maestro
feat: add Maestro e2e tests for RN
2025-02-11 11:39:06 +01:00
Trisha Lim
33293f3b64 Fix copy button alignment 2025-02-11 16:20:55 +07:00
Guido D'Orsi
d5d448670d feat: add role mapping to group extension 2025-02-10 17:31:25 +01:00
Brad Anderson
e3c3913c74 test: don't forget self 2025-02-10 10:14:49 -05:00
Brad Anderson
2ee2fcaa27 test: more selective trigger 2025-02-10 10:13:56 -05:00
Brad Anderson
edc77c4bd5 test: PR feedback 2025-02-10 10:06:41 -05:00
Guido D'Orsi
0ce25f143b Merge pull request #1333 from garden-co/authv2-frameworks-docs
docs: update the upgrade guide & authentication docs for the other frameworks
2025-02-10 14:57:34 +01:00
Trisha Lim
e35018a5fa Redesign mobile nav (#1177) 2025-02-10 20:53:33 +07:00
Guido D'Orsi
0d05c927f1 docs: update the upgrade guide & authentication docs for the other frameworks 2025-02-10 14:44:09 +01:00
Guido D'Orsi
4ef43e3477 Merge pull request #1321 from garden-co/changeset-release/main
Version Packages
2025-02-10 11:34:24 +01:00
Guido D'Orsi
d7ce078f17 Merge pull request #1311 from garden-co/authv2-docs
Update docs for the new auth flow
2025-02-08 13:10:25 +01:00
Brad Anderson
95349d59f8 test: opt out of analytics 2025-02-07 16:31:24 -05:00
Brad Anderson
bb13b6e41d test: stop erase_text forever loop 2025-02-07 15:00:34 -05:00
Brad Anderson
3c2da2f0db gha 2025-02-07 13:34:44 -05:00
Brad Anderson
7bac26cf7f gha 2025-02-07 13:01:55 -05:00
Brad Anderson
d3d9200acf gha 2025-02-07 12:39:47 -05:00
Guido D'Orsi
60558ee664 Merge pull request #1325 from garden-co/authv2-group-api
fix: make Group.removeMember return the promise from CoJSON
2025-02-07 18:35:31 +01:00
Guido D'Orsi
6354135af4 Merge pull request #1327 from garden-co/reduce-max-retry-not-found
fix: reduce the retries on coValue not found to two
2025-02-07 18:34:58 +01:00
Guido D'Orsi
69a0befb7b chore: fix type issues on tests 2025-02-07 18:34:17 +01:00
Brad Anderson
c48aa7358a gha 2025-02-07 12:28:18 -05:00
Guido D'Orsi
01517d1463 Merge pull request #1326 from garden-co/authv2upgrade-guide
docs: upgrade guide for react
2025-02-07 18:14:31 +01:00
Brad Anderson
cdd7e6454f gha 2025-02-07 12:09:55 -05:00
Brad Anderson
2e030ea942 fix: no-go on xlarge - billing put their hand out 2025-02-07 12:09:55 -05:00
Brad Anderson
5207c00582 fix: try xlarge 2025-02-07 12:09:55 -05:00
Brad Anderson
22fa01b9c5 fix: startup timeout 5 mins 👀 2025-02-07 12:09:55 -05:00
Brad Anderson
f5a1e42dd6 fix: maestro path and startup timeout 2025-02-07 12:09:55 -05:00
Brad Anderson
58fe07351e test: missed a couple workflows for source code action 2025-02-07 12:09:54 -05:00
Brad Anderson
25e79fd001 fix: add shell to action 2025-02-07 12:09:54 -05:00
Brad Anderson
53868a4d50 test: corepack and maestro bin 2025-02-07 12:09:54 -05:00
Brad Anderson
21f6f4d933 test: switch order to give app time to install/launch 2025-02-07 12:09:54 -05:00
Brad Anderson
21f691b6da test: enable Maestro install for non-local runs 2025-02-07 12:09:54 -05:00
Brad Anderson
45c46ff14a test: add shell to composite action 2025-02-07 12:09:54 -05:00
Brad Anderson
6dad0878da test: share source code action, run e2e rn tests 2025-02-07 12:09:54 -05:00
Brad Anderson
3b6552d728 feat: add Maestro e2e tests for RN 2025-02-07 12:09:54 -05:00
Guido D'Orsi
e83ce5d6c8 docs: small typos and corrections 2025-02-07 18:03:00 +01:00
Guido D'Orsi
b6c672a1ad docs: improvments on the auth docs based on PR feedback 2025-02-07 17:43:05 +01:00
Guido D'Orsi
e2cfa60caa docs: improvments on the auth docs based on PR feedback 2025-02-07 17:28:27 +01:00
Guido D'Orsi
fa8f4e03d2 docs: improvments on the upgrade guide based on PR feedback 2025-02-07 17:18:58 +01:00
Guido D'Orsi
ac3d9fa2c3 fix: educe the retries on coValue not found to one 2025-02-07 15:06:10 +01:00
Guido D'Orsi
ff325ebf2a docs: improve the React authentication docs 2025-02-07 14:50:24 +01:00
Guido D'Orsi
71825cb90a feat: simplify Passphrase auth consumer API 2025-02-07 14:32:32 +01:00
Guido D'Orsi
d1281e560a docs: add the loading API and the changes on the workers 2025-02-06 17:42:37 +01:00
Guido D'Orsi
a1dc7f38ed docs: upgrade guide for react on auth & local-only 2025-02-06 16:43:14 +01:00
Guido D'Orsi
581118b918 fix: rename getCurrentUserPassphrase into getCurrentAccountPassphrase 2025-02-06 12:04:48 +01:00
github-actions[bot]
ab378b3d16 Version Packages 2025-02-06 09:41:56 +00:00
Guido D'Orsi
dd034643da fix: make Group.removeMember return the promise from CoJSON 2025-02-06 10:41:29 +01:00
Guido D'Orsi
bc2e531c05 Merge pull request #1323 from garden-co/fix/garden-copyright
Update copyright to 2025
2025-02-06 10:40:41 +01:00
Guido D'Orsi
334f6cf2d8 Merge pull request #1324 from garden-co/authv2-browser-integration-tests
test: cover browser sync scenarios with tests
2025-02-06 10:31:43 +01:00
Guido D'Orsi
31296a6ce2 test: moar isolation 2025-02-06 10:31:14 +01:00
Guido D'Orsi
6fc8ae8293 test: skip flaky test 2025-02-06 10:27:23 +01:00
Guido D'Orsi
a1d6e7147b test: close sync servers on test end 2025-02-06 10:25:11 +01:00
Guido D'Orsi
0d111b15f8 test: do not close the context 2025-02-06 10:15:33 +01:00
Guido D'Orsi
95b9c4825f test: increase timeout on CI 2025-02-05 19:41:33 +01:00
Guido D'Orsi
65e26d9ba4 test: cover file download from storage 2025-02-05 19:25:59 +01:00
Guido D'Orsi
cabce37133 fix: correctly handle cd when the project name has spaces 2025-02-05 18:56:47 +01:00
Guido D'Orsi
20c4484066 feat: better solution to isolate IndexedDB databases 2025-02-05 18:55:24 +01:00
Guido D'Orsi
17dc5fc37f chore: little context on what are the commands 2025-02-05 18:45:31 +01:00
Guido D'Orsi
05d907b675 test: add tests for check sync on unstable connections 2025-02-05 18:36:10 +01:00
Guido D'Orsi
62cb650356 feat: make possible to disable storage 2025-02-05 18:22:49 +01:00
Guido D'Orsi
9f833b3931 test: fix failing clerk test 2025-02-05 17:31:04 +01:00
Guido D'Orsi
f0d123979c test: cover browser sync scenarios with tests 2025-02-05 17:15:39 +01:00
Guido D'Orsi
90078a4bce fix(logOut): return a Promise to wait for the credentials clear 2025-02-05 17:15:17 +01:00
Guido D'Orsi
2d3aa76bf0 fix: use addListener to catch the clerk auth changes 2025-02-05 15:21:46 +01:00
Guido D'Orsi
86bd86b966 docs: update docs with the new auth flow and changes 2025-02-05 13:04:37 +01:00
Guido D'Orsi
bebd0ede64 chore: rename onAnonymousUserDiscarded into onAnonymousAccountDiscarded 2025-02-05 12:43:37 +01:00
Trisha Lim
14bdd496f7 Update copyright to 2025 2025-02-05 18:36:15 +07:00
Guido D'Orsi
91fcb3f6b6 Merge pull request #1309 from garden-co/fix/rotate-before-revoke
fix: move access revoke after key rotation
2025-02-05 11:19:18 +01:00
Guido D'Orsi
ff94cadd7b Merge pull request #1319 from garden-co/feat/throw-ensure-loaded-mis
feat: make ensureLoaded throw when the resolved value is undefined
2025-02-05 11:15:17 +01:00
Guido D'Orsi
60fe8f9106 Merge pull request #1320 from garden-co/authv2-vitest-upgrade
feat: upgrade vitest to v3 and vite to v6
2025-02-05 11:15:05 +01:00
Guido D'Orsi
5158581b0c chore(ci): install playwright deps 2025-02-04 18:12:16 +01:00
Guido D'Orsi
ee511b4934 test: run indexeddb tests on browser mode 2025-02-04 18:10:15 +01:00
Guido D'Orsi
bc13653236 fix: playwrite duplicate versions 2025-02-04 17:43:07 +01:00
Guido D'Orsi
fe28e254a3 feat: upgrade vitest to v3 and vite to v6 2025-02-04 17:24:56 +01:00
Guido D'Orsi
82179812de feat: drop node 14 polyfill for globalThis.crypto 2025-02-04 17:22:08 +01:00
Guido D'Orsi
d42c2aa014 feat: make ensureLoaded throw when the resolved value is undefined 2025-02-04 14:55:48 +01:00
Guido D'Orsi
ff92316488 Merge pull request #1318 from garden-co/pre-release
feat: add a pre-release action
2025-02-04 11:45:58 +01:00
Guido D'Orsi
a93df6fcb2 Merge branch 'main' into authv2 2025-02-04 11:19:52 +01:00
Guido D'Orsi
b5962b4f81 chore(ci): fix syntax errors 2025-02-04 11:19:07 +01:00
Guido D'Orsi
f038c2d378 Merge pull request #1315 from garden-co/jazz-686-use-latest-corepack
Uses latest corepack as part of CI build
2025-02-04 10:10:06 +01:00
Benjamin S. Leveritt
febafecfdd Uses latest corepack as part of CI build 2025-02-04 09:08:23 +00:00
Guido D'Orsi
70c9a5db88 fix: move access revoke after key rotation 2025-01-30 19:01:28 +01:00
249 changed files with 6672 additions and 2191 deletions

View File

@@ -0,0 +1,5 @@
---
"cojson": patch
---
Drop node 14 polyfill for globalThis.crypto

View File

@@ -0,0 +1,5 @@
---
"jazz-tools": patch
---
Make ensureLoaded throw when the resolved value is undefined

View File

@@ -0,0 +1,5 @@
---
"create-jazz-app": patch
---
Correctly handle cd when the project name has spaces

View File

@@ -0,0 +1,6 @@
---
"jazz-tools": minor
---
Group.addMember and Group.removeMember are not chainable anymore.
Group.removeMember now returns the internal promise.

View File

@@ -0,0 +1,5 @@
---
"cojson": patch
---
Reduce the retries on coValue not found to two

View File

@@ -0,0 +1,5 @@
---
"cojson": patch
---
Add role mapping to Group.extend

36
.github/actions/source-code/action.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Get and Build Source Code
runs:
using: "composite"
steps:
- name: Enable latestcorepack
shell: bash
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: useblacksmith/setup-node@v5
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: useblacksmith/cache@v5
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

View File

@@ -6,7 +6,7 @@ on:
jobs:
build-examples:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
example: [
@@ -23,41 +23,13 @@ jobs:
]
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: |

View File

@@ -2,56 +2,25 @@ name: Build Starters
on:
push:
branches: [ "main" ]
branches: ["main"]
jobs:
build-starters:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
starter: [
"react-demo-auth-tailwind",
]
starter: ["react-demo-auth-tailwind"]
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: actions/checkout@v4
with:
submodules: true
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Pnpm Build
run: |
pnpm install
pnpm turbo build;
working-directory: ./starters/${{ matrix.starter }}
- name: Pnpm Build
run: |
pnpm install
pnpm turbo build;
working-directory: ./starters/${{ matrix.starter }}

View File

@@ -6,7 +6,7 @@ on:
jobs:
quality:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4

91
.github/workflows/e2e-rn-test.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: End-to-End Tests for React Native
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ".github/workflows/e2e-rn-test.yml"
- "examples/chat-rn/**"
- "examples/chat-rn-clerk/**"
- "packages/jazz-react-native*/**"
jobs:
e2e-tests:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Create Output Directory
run: |
mkdir -p ~/output
- name: Pnpm Build
run: pnpm turbo build --filter="./packages/*"
- name: iOS Simulator
id: ios-simulator
uses: futureware-tech/simulator-action@v4
with:
os: iOS
wait_for_boot: true
- name: chat-rn App Pre Build
working-directory: ./examples/chat-rn
run: |
pnpm build
pnpm expo prebuild --clean
- name: chat-rn App Build
working-directory: ./examples/chat-rn/ios
run: |
xcodebuild -scheme "jazzchatrn" \
-workspace jazzchatrn.xcworkspace \
-archivePath $RUNNER_TEMP/jazzchatrn.xcarchive \
-derivedDataPath $RUNNER_TEMP/build \
-destination "id=${{ steps.ios-simulator.outputs.udid }}" \
-configuration Release \
-sdk iphonesimulator \
build
xcrun simctl install booted $RUNNER_TEMP/build/Build/Products/Release-iphonesimulator/jazzchatrn.app
xcrun simctl spawn booted log stream --level debug | tee ~/output/sim.log &
- name: Install Maestro
run: |
curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: chat-rn App Test
id: e2e_test
working-directory: ./examples/chat-rn
continue-on-error: true
run: |
export PATH="$PATH":"$HOME/.maestro/bin"
export MAESTRO_DRIVER_STARTUP_TIMEOUT=300000 # setting to 5 mins 👀
export MAESTRO_CLI_NO_ANALYTICS=1
maestro test test/e2e/flow.yml
- name: Copy Maestro and Diagnostic Files
if: steps.e2e_test.outcome != 'success'
run: |
cp -r ~/Library/Logs/DiagnosticReports/* ~/output
cp -r ~/.maestro/tests/* ~/output
- name: Upload Output Files
if: steps.e2e_test.outcome != 'success'
uses: actions/upload-artifact@v4
with:
name: e2e-test-output
path: ~/output/*
retention-days: 5
- name: Exit with Test Result
if: always()
run: |
if [ "${{ steps.e2e_test.outcome }}" != "success" ]; then
exit 1
fi

View File

@@ -8,43 +8,16 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Build jazz-run
run: pnpm exec turbo build && chmod +x dist/index.js;
@@ -53,4 +26,3 @@ jobs:
- name: Run create account
run: ./dist/index.js account create --name "Jazz Run CI test"
working-directory: ./packages/jazz-run

View File

@@ -9,46 +9,19 @@ on:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
continue-on-error: true
strategy:
matrix:
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "examples/onboarding", "starters/react-demo-auth-tailwind"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: pnpm turbo build

View File

@@ -12,35 +12,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: pnpm turbo build --filter="./packages/*"

View File

@@ -17,40 +17,13 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Create Release Pull Request or Publish to npm
id: changesets

View File

@@ -9,44 +9,20 @@ on:
jobs:
unit-tests:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: pnpm turbo build --filter="./packages/*"
- name: Install Playwright Browsers
run: pnpm exec playwright install
- name: Unit Tests
run: pnpm test:ci

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ test-results
.husky
.vscode/settings.json
.svelte-kit

View File

@@ -1,4 +1,4 @@
Copyright 2024, Garden Computing, Inc.
Copyright 2025, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@@ -17,4 +17,4 @@ For community and support, please join our [Discord](https://discord.gg/utDMjHYg
- Community & support: [Discord](https://discord.gg/utDMjHYg42)
- Updates: [X](https://x.com/jazz_tools) & [Email](https://garden.co/news)
Copyright 2024 &mdash; Garden Computing, Inc.
Copyright 2025 &mdash; Garden Computing, Inc.

View File

@@ -1,5 +1,14 @@
# chat-rn-clerk
## 1.0.64
### Patch Changes
- jazz-react-native@0.9.23
- jazz-react-native-auth-clerk@0.9.23
- jazz-tools@0.9.23
- jazz-react-native-media-images@0.9.23
## 1.0.63
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.63",
"version": "1.0.64",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
@@ -72,7 +72,7 @@
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"react-test-renderer": "18.2.0",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2"
},
"private": true

View File

@@ -1,5 +1,12 @@
# chat-rn
## 1.0.61
### Patch Changes
- jazz-react-native@0.9.23
- jazz-tools@0.9.23
## 1.0.60
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.60",
"version": "1.0.61",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
@@ -46,7 +46,7 @@
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "^18.3.12",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2"
},
"private": true

View File

@@ -148,6 +148,7 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
}}
textAlignVertical="center"
onSubmitEditing={sendMessage}
testID="username-input"
/>
<TouchableOpacity
onPress={createChat}
@@ -190,10 +191,12 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
placeholder="Type a message..."
textAlignVertical="center"
onSubmitEditing={sendMessage}
testID="message-input"
/>
<TouchableOpacity
onPress={sendMessage}
className="bg-gray-300 text-white rounded-full h-8 w-8 items-center justify-center"
testID="send-button"
>
<Text></Text>
</TouchableOpacity>

View File

@@ -0,0 +1,16 @@
# this sub-flow exists to work around an ios issue where the text field is not
# fully erased. The tap into the input field hits the middle, and clears all
# text to the left. If there's more to the right, it slides left, and thus we
# repeat this step. https://maestro.mobile.dev/api-reference/commands/erasetext
appId: com.jazz.chatrn
---
- copyTextFrom:
id: ${id}
- repeat:
times: 4
commands:
- tapOn:
id: ${id}
- eraseText
- copyTextFrom:
id: ${id}

View File

@@ -0,0 +1,47 @@
appId: com.jazz.chatrn
---
- launchApp
# # handle Expo screens (for local dev)
# - assertVisible: "Continue"
# - tapOn: "Continue"
# - assertVisible: "Reload"
# - tapOn: "Reload"
# login
- assertVisible: "Anonymous user"
- runFlow:
label: "Erase existing username"
file: erase_text.yml
env:
id: "username-input"
- inputText: "boorad"
- assertVisible: "boorad"
# start new chat
- tapOn: "Start new chat"
- assertVisible: "Share"
- assertVisible: "Jazz Chat"
- assertVisible: "Logout"
# send a message
- runFlow:
label: "Erase existing message"
file: erase_text.yml
env:
id: "message-input"
- inputText: "bro, low key, it do be like that tho"
- tapOn:
id: "send-button"
- assertVisible: "bro, low key, it do be like that tho"
# get invite code
- tapOn: "Share"
- assertVisible: "Copied to clipboard"
- tapOn: "OK"
# this assert doesn't work. maestro.copiedText only populates from `copyTextFrom`
# - assertTrue: ${maestro.copiedText.startsWith("co_z")}
# logout
- tapOn: "Logout"
- assertVisible: "Anonymous user"

View File

@@ -1,5 +1,13 @@
# chat-vue
## 0.0.48
### Patch Changes
- jazz-browser@0.9.23
- jazz-tools@0.9.23
- jazz-vue@0.9.23
## 0.0.47
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.47",
"version": "0.0.48",
"private": true,
"type": "module",
"scripts": {
@@ -30,9 +30,9 @@
"eslint-plugin-vue": "^9.28.0",
"npm-run-all2": "^6.2.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.4.6",
"vue-tsc": "^2.1.6"
}

View File

@@ -1,5 +1,13 @@
# jazz-example-chat
## 0.0.144
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.143
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.143",
"version": "0.0.144",
"type": "module",
"scripts": {
"dev": "vite",
@@ -23,15 +23,15 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.50.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# minimal-auth-clerk
## 0.0.43
### Patch Changes
- jazz-react@0.9.23
- jazz-react-auth-clerk@0.9.23
- jazz-tools@0.9.23
## 0.0.42
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.42",
"version": "0.0.43",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,7 +13,7 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:0.9.22",
"jazz-react-auth-clerk": "workspace:0.9.23",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -25,6 +25,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,12 @@
# file-share-svelte
## 0.0.28
### Patch Changes
- jazz-svelte@0.9.23
- jazz-tools@0.9.23
## 0.0.27
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.27",
"version": "0.0.28",
"private": true,
"type": "module",
"scripts": {
@@ -18,8 +18,8 @@
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.5.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/is-ci": "^3.0.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
@@ -32,10 +32,10 @@
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.10"
"vite": "^6.0.11"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.15",

View File

@@ -1,5 +1,13 @@
# form
## 0.0.39
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.38
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.0.38",
"version": "0.0.39",
"type": "module",
"scripts": {
"dev": "vite",
@@ -20,7 +20,7 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.9",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
@@ -29,8 +29,8 @@
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# image-upload
## 0.0.41
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.40
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.40",
"version": "0.0.41",
"type": "module",
"scripts": {
"dev": "vite",
@@ -24,6 +24,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# jazz-example-inspector
## 0.0.102
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- cojson-transport-ws@0.9.23
## 0.0.101
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.101",
"version": "0.0.102",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,12 +12,12 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.9.19",
"cojson-transport-ws": "workspace:0.9.22",
"cojson": "workspace:0.9.23",
"cojson-transport-ws": "workspace:0.9.23",
"hash-slash": "workspace:0.2.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
@@ -37,8 +37,8 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# jazz-example-musicplayer
## 0.0.65
### Patch Changes
- jazz-inspector@0.9.23
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.64
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.64",
"version": "0.0.65",
"type": "module",
"scripts": {
"dev": "vite",
@@ -21,9 +21,9 @@
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.9.22",
"jazz-tools": "workspace:0.9.21",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"lucide-react": "^0.274.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -33,14 +33,14 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.50.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -13,7 +13,7 @@ import "./index.css";
import { MusicaAccount } from "@/1_schema";
import { apiKey } from "@/apiKey.ts";
import { JazzProvider } from "jazz-react";
import { onAnonymousUserDiscarded } from "./4_actions";
import { onAnonymousAccountDiscarded } from "./4_actions";
import { useUploadExampleData } from "./lib/useUploadExampleData";
/**
@@ -77,7 +77,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
storage="indexedDB"
AccountSchema={MusicaAccount}
defaultProfileName="Anonymous unicorn"
onAnonymousUserDiscarded={onAnonymousUserDiscarded}
onAnonymousAccountDiscarded={onAnonymousAccountDiscarded}
>
<Main />
<JazzInspector />

View File

@@ -26,17 +26,14 @@ export async function uploadMusicTracks(
files: Iterable<File>,
isExampleTrack: boolean = false,
) {
const me = await MusicaAccount.getMe().ensureLoaded({
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
},
playlists: [],
},
});
if (!me) return;
for (const file of files) {
// The ownership object defines the user that owns the created coValues
// We are creating a group for each CoValue in order to be able to share them via Playlist
@@ -62,19 +59,17 @@ export async function uploadMusicTracks(
// The newly created musicTrack can be associated to the
// user track list using a simple push call
me.root.rootPlaylist.tracks.push(musicTrack);
root.rootPlaylist.tracks.push(musicTrack);
}
}
export async function createNewPlaylist() {
const me = await MusicaAccount.getMe().ensureLoaded({
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
playlists: [],
},
});
if (!me) throw new Error("Current playlist not resolved");
// Since playlists are meant to be shared we associate them
// to a group which will contain the keys required to get
// access to the "owned" values
@@ -90,7 +85,7 @@ export async function createNewPlaylist() {
// Again, we associate the new playlist to the
// user by pushing it into the playlists CoList
me.root.playlists.push(playlist);
root.playlists.push(playlist);
return playlist;
}
@@ -156,32 +151,28 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
}
export async function updateActivePlaylist(playlist?: Playlist) {
const me = await MusicaAccount.getMe().ensureLoaded({
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {
activePlaylist: {},
rootPlaylist: {},
},
});
if (!me) return;
me.root.activePlaylist = playlist ?? me.root.rootPlaylist;
root.activePlaylist = playlist ?? root.rootPlaylist;
}
export async function updateActiveTrack(track: MusicTrack) {
const me = await MusicaAccount.getMe().ensureLoaded({
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {},
});
if (!me) return;
me.root.activeTrack = track;
root.activeTrack = track;
}
export async function onAnonymousUserDiscarded(
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const anonymousAccountWithPlaylist = await anonymousAccount.ensureLoaded({
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
root: {
rootPlaylist: {
tracks: [{}],
@@ -189,9 +180,6 @@ export async function onAnonymousUserDiscarded(
},
});
if (!anonymousAccountWithPlaylist)
throw new Error("Unable to load anonymous account with playlist");
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
@@ -200,15 +188,11 @@ export async function onAnonymousUserDiscarded(
},
});
if (!me) throw new Error("Me not resolved");
const rootPlaylist = anonymousAccountWithPlaylist.root.rootPlaylist;
for (const track of rootPlaylist.tracks) {
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
await trackGroup.addMember(me, "admin");
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}

View File

@@ -19,8 +19,6 @@ export function InvitePage() {
},
});
if (!me) return;
if (
playlist &&
!me.root.playlists.some((item) => playlist.id === item?.id)

View File

@@ -9,8 +9,6 @@ export async function getNextTrack() {
},
});
if (!me) return;
const tracks = me.root.activePlaylist.tracks;
const activeTrack = me.root._refs.activeTrack;
@@ -30,8 +28,6 @@ export async function getPrevTrack() {
},
});
if (!me) return;
const tracks = me.root.activePlaylist.tracks;
const activeTrack = me.root._refs.activeTrack;

View File

@@ -16,8 +16,6 @@ async function uploadOnboardingData() {
root: {},
});
if (!me) throw new Error("Me not resolved");
if (me.root.exampleDataLoaded) return;
me.root.exampleDataLoaded = true;

View File

@@ -1,5 +1,13 @@
# jazz-example-onboarding
## 0.0.45
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.44
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-onboarding",
"private": true,
"version": "0.0.44",
"version": "0.0.45",
"type": "module",
"scripts": {
"dev": "vite",
@@ -21,7 +21,7 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.50.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
@@ -29,8 +29,8 @@
"is-ci": "^3.0.1",
"jazz-run": "workspace:*",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,12 @@
# organization
## 0.0.37
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.36
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.36",
"version": "0.0.37",
"type": "module",
"scripts": {
"dev": "vite",
@@ -28,8 +28,8 @@
"autoprefixer": "^10.4.20",
"globals": "^15.11.0",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,11 @@
# passkey-svelte
## 0.0.32
### Patch Changes
- jazz-svelte@0.9.23
## 0.0.31
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.31",
"version": "0.0.32",
"type": "module",
"private": true,
"scripts": {
@@ -16,8 +16,8 @@
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/eslint": "^9.6.0",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -29,7 +29,7 @@
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.10"
"vite": "^6.0.11"
},
"dependencies": {
"jazz-svelte": "workspace:*"

View File

@@ -1,5 +1,12 @@
# minimal-auth-passkey
## 0.0.42
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.41
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.41",
"version": "0.0.42",
"type": "module",
"scripts": {
"dev": "vite",
@@ -23,6 +23,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -23,6 +23,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,12 @@
# jazz-password-manager
## 0.0.63
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.62
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.62",
"version": "0.0.63",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,8 +12,8 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-react": "workspace:0.9.22",
"jazz-tools": "workspace:0.9.21",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.41.5",
@@ -26,8 +26,8 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# jazz-example-pets
## 0.0.161
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.160
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.160",
"version": "0.0.161",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,13 +15,13 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.9.22",
"jazz-react": "workspace:0.9.22",
"jazz-tools": "workspace:0.9.21",
"jazz-browser-media-images": "workspace:0.9.23",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -33,7 +33,7 @@
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.50.1",
"@types/node": "^22.5.1",
"@types/qrcode": "^1.5.1",
"@types/react": "^18.3.12",
@@ -41,11 +41,11 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"jazz-run": "workspace:0.9.22",
"jazz-run": "workspace:0.9.23",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite": "^6.0.11",
"vite-plugin-top-level-await": "^1.4.4"
}
}

View File

@@ -1,5 +1,13 @@
# reactions
## 0.0.41
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.40
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.40",
"version": "0.0.41",
"type": "module",
"scripts": {
"dev": "vite",
@@ -25,6 +25,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,13 @@
# todo-vue
## 0.0.46
### Patch Changes
- jazz-browser@0.9.23
- jazz-tools@0.9.23
- jazz-vue@0.9.23
## 0.0.45
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.45",
"version": "0.0.46",
"private": true,
"type": "module",
"scripts": {
@@ -30,9 +30,9 @@
"eslint-plugin-vue": "^9.28.0",
"npm-run-all2": "^6.2.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.4.6",
"vue-tsc": "^2.1.6"
}

View File

@@ -1,5 +1,12 @@
# jazz-example-todo
## 0.0.160
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.159
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.159",
"version": "0.0.160",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,12 +12,12 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.9.22",
"jazz-tools": "workspace:0.9.21",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -35,8 +35,8 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -1,5 +1,12 @@
# version-history
## 0.0.38
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.37
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.37",
"version": "0.0.38",
"type": "module",
"scripts": {
"dev": "vite",
@@ -25,6 +25,6 @@
"@vitejs/plugin-react": "^4.3.3",
"globals": "^15.11.0",
"typescript": "~5.6.2",
"vite": "^5.4.10"
"vite": "^6.0.11"
}
}

View File

@@ -0,0 +1,13 @@
export function Copyright({
className,
companyName = "Garden Computing, Inc.",
}: {
companyName?: string;
className?: string;
}) {
return (
<p className={className}>
© {new Date().getFullYear()} {companyName}
</p>
);
}

View File

@@ -26,6 +26,7 @@ import {
MousePointerSquareDashedIcon,
PencilLineIcon,
ScanFace,
ScrollIcon,
SunIcon,
TrashIcon,
UploadCloudIcon,
@@ -67,6 +68,7 @@ const icons = {
permissions: FileLock2Icon,
social: UsersIcon,
spatialPresence: MousePointerSquareDashedIcon,
tableOfContents: ScrollIcon,
touchId: FingerprintIcon,
upload: UploadCloudIcon,
write: PencilLineIcon,

View File

@@ -6,7 +6,7 @@ import { Icon } from "../atoms/Icon";
// TODO: add tabs feature, and remove CodeExampleTabs
function CopyButton({ code, size }: { code: string; size?: "sm" | "md" }) {
function CopyButton({ code, size }: { code: string; size: "md" | "lg" }) {
let [copyCount, setCopyCount] = useState(0);
let copied = copyCount > 0;
@@ -27,8 +27,8 @@ function CopyButton({ code, size }: { code: string; size?: "sm" | "md" }) {
copied
? "bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20"
: "bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5",
size == "sm"
? "right-1.5 top-1.5 py-[2px] pl-1 pr-2"
size == "md"
? "right-[8px] top-[8px] py-[2px] pl-1 pr-2"
: "right-2 top-2 py-1 pl-2 pr-3 ",
)}
onClick={() => {
@@ -48,7 +48,7 @@ function CopyButton({ code, size }: { code: string; size?: "sm" | "md" }) {
name="copy"
size="xs"
className={clsx(
size === "sm" ? "size-2" : "size-3",
size === "md" ? "size-3" : "size-4",
"stroke-stone-500 transition-colors group-hover/button:stroke-stone-600 dark:group-hover/button:stroke-stone-400",
)}
/>
@@ -69,11 +69,11 @@ function CopyButton({ code, size }: { code: string; size?: "sm" | "md" }) {
export function CodeGroup({
children,
size,
size = "md",
className,
}: {
children: React.ReactNode;
size?: "sm" | "md";
size?: "md" | "lg";
className?: string;
}) {
const textRef = useRef<HTMLPreElement | null>(null);
@@ -86,13 +86,14 @@ export function CodeGroup({
}, [children]);
return (
<div className={clsx(className, "group relative")}>
<div className={clsx(className, "not-prose group relative")}>
<pre
className={clsx(
"h-full border p-0 bg-stone-50 dark:bg-stone-925",
"h-full overflow-x-auto",
"border rounded-md p-0 bg-stone-50 dark:bg-stone-925",
"text-black dark:text-white",
{
"text-sm": size === "sm",
"text-sm": size === "md",
},
)}
ref={textRef}

View File

@@ -4,6 +4,7 @@ import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ComponentType, ReactNode } from "react";
import { Copyright } from "../atoms/Copyright";
import { NewsletterForm } from "./NewsletterForm";
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
@@ -18,35 +19,19 @@ type FooterSection = {
type FooterProps = {
logo: ReactNode;
companyName: string;
sections: FooterSection[];
socials: SocialLinksProps;
themeToggle: ComponentType<{ className?: string }>;
};
function Copyright({
className,
companyName,
}: {
companyName: string;
className?: string;
}) {
return (
<p className={clsx(className, "text-sm")}>
© {new Date().getFullYear()} {companyName}
</p>
);
}
export function Footer({
logo,
companyName,
sections,
socials,
themeToggle: ThemeToggle,
}: FooterProps) {
return (
<footer className="w-full py-8 mt-12 md:mt-20">
<footer className="w-full pt-8 pb-20 mt-12 md:mt-20 md:pb-8">
<div className="container grid gap-8 md:gap-12">
<div className="grid grid-cols-12 gap-y-3 sm:items-center pb-8 border-b">
<div className="col-span-full sm:col-span-6 md:col-span-8">
@@ -82,10 +67,7 @@ export function Footer({
</div>
))}
<Copyright
className="order-last col-span-full self-center md:col-span-10 md:order-none"
companyName={companyName}
/>
<Copyright className="text-sm order-last col-span-full self-center md:col-span-10 md:order-none" />
<div className="col-span-full flex items-center justify-between gap-6 md:col-span-2">
<SocialLinks {...socials}></SocialLinks>

View File

@@ -10,14 +10,7 @@ import {
import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
ComponentType,
ReactNode,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { ComponentType, ReactNode, useEffect, useState } from "react";
import { Icon } from "../atoms/Icon";
import { BreadCrumb } from "../molecules/Breadcrumb";
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
@@ -35,10 +28,16 @@ type NavItemProps = {
type NavProps = {
mainLogo: ReactNode;
items: NavItemProps[];
docNav?: ReactNode;
cta?: ReactNode;
socials?: SocialLinksProps;
themeToggle: ComponentType<{ className?: string }>;
sections?: NavSection[];
};
export type NavSection = {
name: string;
content: ReactNode;
icon: string;
};
function NavItem({
@@ -68,7 +67,7 @@ function NavItem({
className,
"text-sm px-2 lg:px-4 py-3 ",
firstOnRight && "ml-auto",
path === href ? "text-black dark:text-white" : "",
path === href ? "text-stone-900 dark:text-white" : "",
)}
{...item}
>
@@ -81,8 +80,8 @@ function NavItem({
<Popover className={clsx("relative", className, firstOnRight && "ml-auto")}>
<PopoverButton
className={clsx(
"flex items-center gap-1.5 text-sm px-2 lg:px-4 py-3 max-sm:w-full text-stone-600 dark:text-stone-400 hover:text-black dark:hover:text-white transition-colors hover:transition-none focus-visible:outline-none",
path === href ? "text-black dark:text-white" : "",
"flex items-center gap-1.5 text-sm px-2 lg:px-4 py-3 max-sm:w-full hover:text-stone-900 dark:hover:text-white transition-colors hover:transition-none focus-visible:outline-none",
path === href ? "text-stone-900 dark:text-white" : "",
)}
>
<span>{title}</span>
@@ -128,25 +127,57 @@ function NavItem({
export function MobileNav({
mainLogo,
items,
docNav,
cta,
socials,
sections,
themeToggle: ThemeToggle,
}: NavProps) {
const [menuOpen, setMenuOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null);
const primarySection = {
name: "Menu",
icon: "menu",
content: (
<>
<div className="flex items-center justify-between border-b p-3">
<Link href="/" className="flex items-center">
{mainLogo}
</Link>
useLayoutEffect(() => {
searchOpen && searchRef.current?.focus();
}, [searchOpen]);
<SocialLinks {...socials} className="ml-auto" />
<ThemeToggle className="ml-4 mr-1" />
</div>
<div className="flex flex-col gap-2 p-3">
{items
.filter((item) => !("icon" in item))
.map((item, i) => (
<NavLink
className="p-1"
key={i}
href={item.href}
onClick={() => setActive(null)}
newTab={item.newTab}
>
{item.title}
</NavLink>
))}
</div>
</>
),
};
const [active, setActive] = useState<string | null>();
const pathname = usePathname();
useEffect(() => {
setMenuOpen(false);
setActive(null);
}, [pathname]);
const toggle = (type: string) => {
setActive(active == type ? null : type);
};
const navSections = [primarySection, ...(sections || [])];
return (
<>
<div className="md:hidden px-4 flex items-center self-stretch dark:text-white">
@@ -155,9 +186,8 @@ export function MobileNav({
</NavLinkLogo>
<button
className="flex gap-2 p-3 rounded-xl items-center"
onMouseDown={() => {
setMenuOpen((o) => !o);
setSearchOpen(false);
onClick={() => {
setActive("Menu");
}}
aria-label="Open menu"
>
@@ -165,80 +195,70 @@ export function MobileNav({
<BreadCrumb items={items} />
</button>
</div>
<div
onClick={() => {
setMenuOpen(false);
setSearchOpen(false);
setActive(null);
}}
className={clsx(
menuOpen || searchOpen ? "block" : "hidden",
"fixed top-0 bottom-0 left-0 right-0 bg-stone-200/80 dark:bg-black/80 w-full h-full z-20",
!!active ? "block" : "hidden",
"md:hidden fixed backdrop-blur-sm top-0 bottom-0 left-0 right-0 bg-stone-200/80 dark:bg-black/80 w-full h-full z-20",
)}
></div>
<nav
<div
className={clsx(
"md:hidden fixed flex flex-col bottom-4 right-4 z-50",
"bg-stone-50 dark:bg-stone-925 border rounded-lg shadow-lg",
menuOpen || searchOpen ? "left-4" : "",
"md:hidden bg-white border fixed z-50",
"dark:bg-stone-925",
{
"rounded-lg right-6 left-6 bottom-6 sm:max-w-lg sm:w-full shadow-md sm:left-1/2 sm:-translate-x-1/2 ":
!!active,
"rounded-full shadow-sm left-1/2 -translate-x-1/2 bottom-7":
!active,
},
)}
>
<div className={clsx(menuOpen ? "block" : "hidden", " px-2 pb-2")}>
<div className="flex items-center w-full border-b">
<NavLinkLogo
prominent
href="/"
className="mr-auto"
onClick={() => setMenuOpen(false)}
>
{mainLogo}
</NavLinkLogo>
<SocialLinks className="px-2 gap-2" {...socials} />
</div>
{pathname.startsWith("/docs") && docNav && (
<div className="max-h-[calc(100dvh-15rem)] p-4 border-b overflow-x-auto">
{docNav}
</div>
)}
<div className="flex flex-wrap justify-end py-2 gap-x-3 gap-y-1 border-b">
{[{ title: "Home", href: "/" }, ...items]
.filter((item) => !("icon" in item))
.map((item, i) => (
<NavLink
className="p-1 text-sm"
key={i}
href={item.href}
onClick={() => setMenuOpen(false)}
newTab={item.newTab}
>
{item.title}
</NavLink>
))}
</div>
</div>
<div className="flex items-center self-stretch justify-between">
{(menuOpen || searchOpen) && <ThemeToggle className="p-3" />}
<button
className="flex gap-2 p-3 rounded-xl items-center"
onMouseDown={() => {
setMenuOpen((o) => !o);
setSearchOpen(false);
}}
aria-label="Close menu"
>
{menuOpen || searchOpen ? (
<Icon name="close" />
) : (
<>
<Icon name="menu" />
<BreadCrumb items={items} />
</>
{active && (
<div
className={clsx(
"max-h-[calc(100vh-16rem)] overflow-y-auto",
active === "Menu" ? "" : "p-3 pb-10",
)}
</button>
>
{navSections.map((section) =>
section.name == active ? section.content : null,
)}
</div>
)}
<div
className={clsx("flex justify-center py-1 px-1.5", {
"border-t py-2": !!active,
})}
>
{navSections.map(
(section) =>
section.content && (
<button
type="button"
className={clsx(
"flex items-center gap-1 px-2 py-1 text-sm rounded-md whitespace-nowrap",
"text-stone-900 dark:text-white",
{
"bg-stone-100 dark:bg-stone-900": active === section.name,
},
)}
onClick={() => toggle(section.name)}
key={section.name}
>
<Icon name={section.icon} size="xs" />
{section.name}
</button>
),
)}
</div>
</nav>
</div>
</>
);
}
@@ -262,7 +282,7 @@ function NavLink({
<Link
href={href}
className={clsx(
"text-stone-600 dark:text-stone-400 hover:text-black dark:hover:text-white transition-colors hover:transition-none",
"hover:text-stone-900 dark:hover:text-white transition-colors hover:transition-none",
className,
)}
onClick={onClick}
@@ -310,7 +330,7 @@ function NavLinkLogo({
}
export function Nav(props: NavProps) {
const { mainLogo, items, docNav, cta } = props;
const { mainLogo, items, cta } = props;
return (
<>
<div className="w-full border-b py-2 sticky top-0 z-50 bg-white dark:bg-stone-950 hidden md:block">

View File

@@ -42,7 +42,12 @@ const socials = [
export function SocialLinks(props: SocialLinksProps & { className?: string }) {
return (
<div className={clsx(props.className, "inline-flex gap-6 items-center")}>
<div
className={clsx(
props.className,
"inline-flex gap-3 items-center sm:gap-6",
)}
>
{socials.map(
(social) =>
props[social.key as keyof SocialLinksProps] && (

View File

@@ -9,6 +9,7 @@ import { GcmpNav } from "@/components/Nav";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Copyright } from "gcmp-design-system/src/app/components/atoms/Copyright";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
@@ -98,7 +99,7 @@ export default function RootLayout({
<GcmpNav />
<main className="flex-1 w-full">{children}</main>
<footer className="py-8 text-sm flex justify-between gap-3 w-full container mt-12 md:mt-20">
<p>©2024 Garden Computing, Inc.</p>
<Copyright />
<ThemeToggle className="hidden md:block" />
</footer>

View File

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

View File

@@ -1,13 +1,6 @@
import DocsLayout from "@/components/docs/DocsLayout";
import { DocNav } from "@/components/docs/nav";
export const metadata = {
title: {
default: "Documentation",
template: "%s | Jazz",
},
description: "Jazz guide and documentation.",
};
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function Layout({
children,
@@ -16,7 +9,7 @@ export default function Layout({
}) {
return (
<DocsLayout nav={<DocNav />}>
<div className="flex justify-center lg:gap-5">{children}</div>
<Prose className="max-w-3xl mx-auto lg:flex-1 py-8">{children}</Prose>
</DocsLayout>
);
}

View File

@@ -1,7 +1,8 @@
import MdxSource from "@/components/docs/docs-intro.mdx";
import { frameworks } from "@/lib/framework";
import { JazzLogo } from "gcmp-design-system/src/app/components/atoms/logos/JazzLogo";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import MdxSource from "../../../../../components/docs/docs-intro.mdx";
import { frameworks } from "../../../../../lib/framework";
export default function Page() {
return (

View File

@@ -0,0 +1,406 @@
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
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">
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"]}>
## 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 framework="react">
The clerk provider is not built into `jazz-react` and needs the `jazz-react-auth-clerk` package to be installed.
</ContentByFramework>
<ContentByFramework framework="react-native">
The clerk provider is not built into `jazz-react-native` and needs the `jazz-react-native-auth-clerk` package to be installed.
</ContentByFramework>
After installing the package you can use the `JazzProviderWithClerk` component to wrap your app:
<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">
<CodeGroup>
```tsx
import { JazzProviderWithClerk } from "jazz-react-native-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}>
<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">
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-react-native";
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>
</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

@@ -246,20 +246,14 @@ Let's not forget to update the `AccountSchema`.
import { JazzProvider } from "jazz-react"; // old
import { JazzAccount } from "./schema";
function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
export function MyJazzProvider({ children }: { children: React.ReactNode }) { // old
return ( // old
<> // old
<JazzProvider // old
auth={passkeyAuth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={JazzAccount}
>// old
{children} // old
</JazzProvider> // old
<PasskeyAuthBasicUI state={passKeyState} /> // old
</> // old
<JazzProvider // old
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
AccountSchema={JazzAccount}
>// old
{children} // old
</JazzProvider> // old
); // old
} // old

View File

@@ -104,8 +104,6 @@ const onAccept = async (organizationId: ID<Organization>) => {
},
});
if (!me) throw new Error("Failed to load account data");
const organization = await Organization.load(organizationId, []);
if (!organization) throw new Error("Failed to load organization data");

View File

@@ -0,0 +1,163 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Group inheritance" };
# Group Inheritance
Groups can inherit members from other groups using the `extend` method.
When a group extends another group, members of the parent group will become automatically part of the child group.
## Basic Usage
Here's how to extend a group:
<CodeGroup>
```typescript
const playlistGroup = Group.create();
const trackGroup = Group.create();
// This way track becomes visible to the members of playlist
trackGroup.extend(playlistGroup);
```
</CodeGroup>
When you extend a group:
- Members of the parent group get access to the child group
- Their roles are inherited (with some exceptions, see [below](#role-inheritance-rules))
- Removing a member from the parent group also removes their access to child groups
## Inheriting members but overriding their role
In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`:
<CodeGroup>
```typescript
const organizationGroup = Group.create();
organizationGroup.addMember(bob, "admin");
const billingGroup = Group.create();
// This way the members of the organization can only read the billing data
billingGroup.extend(organizationGroup, "reader");
```
</CodeGroup>
The "override role" works in both directions:
<CodeGroup>
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
parentGroup.addMember(alice, "admin");
const childGroup = Group.create();
childGroup.extend(parentGroup, "writer");
// Bob and Alice are now writers in the child group
```
</CodeGroup>
## Multiple Levels of Inheritance
Groups can be extended multiple levels deep:
<CodeGroup>
```typescript
const grandParentGroup = Group.create();
const parentGroup = Group.create();
const childGroup = Group.create();
childGroup.extend(parentGroup);
parentGroup.extend(grandParentGroup);
```
</CodeGroup>
Members of the grandparent group will get access to all descendant groups based on their roles.
## Permission Changes
When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security.
<CodeGroup>
```typescript
// Remove member from parent
await parentGroup.removeMember(bob);
// Bob loses access to both parent and child groups
```
</CodeGroup>
## Role Inheritance Rules
If the account is already a member of the child group, it will get the more permissive role:
<CodeGroup>
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
const childGroup = Group.create();
parentGroup.addMember(bob, "writer");
childGroup.extend(parentGroup);
// Bob stays a writer because his role is higher
// than the inherited reader role.
```
</CodeGroup>
When extending groups, only admin, writer and reader roles are inherited:
<CodeGroup>
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "writeOnly");
const childGroup = Group.create();
childGroup.extend(parentGroup);
// Bob does not become a member of the child group
```
</CodeGroup>
To extend a group:
1. The current account must be an admin in the child group
2. The current account must be a member of the parent group
<CodeGroup>
```typescript
const companyGroup = company._owner.castAs(Group)
const teamGroup = Group.create();
// Works only if I'm a member of companyGroup
teamGroup.extend(companyGroup);
```
</CodeGroup>
## Example: Team Hierarchy
Here's a practical example of using group inheritance for team permissions:
<CodeGroup>
```typescript
// Company-wide group
const companyGroup = Group.create();
companyGroup.addMember(CEO, "admin");
// Team group with elevated permissions
const teamGroup = Group.create();
teamGroup.extend(companyGroup); // Inherits company-wide access
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
// Project group with specific permissions
const projectGroup = Group.create();
projectGroup.extend(teamGroup); // Inherits team permissions
projectGroup.addMember(client, "reader"); // Client can only read project items
```
</CodeGroup>
This creates a hierarchy where:
- The CEO has admin access to everything
- Team members get writer access to team and project content
- Team leads get admin access to team and project content
- The client can only read project content

View File

@@ -50,36 +50,16 @@ Collaborative Values (CoValues), build a UI and subscribe to changes, set permis
import ReactDOM from "react-dom/client"; // old
import App from "./App.tsx"; // old
import "./index.css"; // old
import {
JazzProvider,
useDemoAuth,
DemoAuthBasicUI,
} from "jazz-react";
// old
import { JazzProvider } from "jazz-react"; // old
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, authState] = useDemoAuth();
return (
<>
<JazzProvider
auth={auth}
// replace `you@example.com` with your email as a temporary API key
peer="wss://cloud.jazz.tools/?key=you@example.com"
>
{children}
</JazzProvider>
<DemoAuthBasicUI appName="Circular" state={authState} />
</>
);
}
// old
ReactDOM.createRoot(document.getElementById("root")!).render( // old
<React.StrictMode> // old
<JazzAndAuth>
<JazzProvider
// replace `you@example.com` with your email as a temporary API key
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
>
<App />
</JazzAndAuth>
</JazzProvider>
</React.StrictMode>// old
); // old
```
@@ -349,7 +329,7 @@ This works because CoValues
1. Directly on an instance:
<CodeGroup size="sm">
<CodeGroup>
```ts
const unsub = issue.subscribe([], (updatedIssue) => console.log(updatedIssue));
```
@@ -357,7 +337,7 @@ This works because CoValues
2. If you only have an ID (this will load the issue if needed):
<CodeGroup size="sm">
<CodeGroup>
```ts
const unsub = Issue.subscribe(issueID, me, [], (updatedIssue) => {
console.log(updatedIssue);
@@ -373,7 +353,7 @@ This works because CoValues
By the way, `useCoState` is basically just an optimized version of
<CodeGroup size="sm">
<CodeGroup>
```ts
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
const [value, setValue] = useState<V>();

View File

@@ -1,46 +1,11 @@
import DocsLayout from "@/app/docs/[framework]/(others)/layout";
import DocsLayout from "@/components/docs/DocsLayout";
import { TableOfContents } from "@/components/docs/TableOfContents";
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
import { DocNav } from "@/components/docs/nav";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { Framework, frameworks } from "@/lib/framework";
import type { Toc } from "@stefanprobst/rehype-extract-toc";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import { Metadata } from "next";
async function getMdxSource(slugPath: string, framework: string) {
try {
return await import(`./${slugPath}.mdx`);
} catch (error) {
return await import(`./${slugPath}/${framework}.mdx`);
}
}
export async function generateMetadata({
params: { slug, framework },
}: {
params: { slug: string[]; framework: string };
}): Promise<Metadata> {
const slugPath = slug.join("/");
const title = "Coming soon";
try {
const mdxSource = await getMdxSource(slugPath, framework);
const title = mdxSource.metadata.title;
return {
title,
openGraph: {
title,
},
};
} catch (error) {
return {
title,
openGraph: {
title,
},
};
}
}
export default async function Page({
params: { slug, framework },
@@ -48,24 +13,31 @@ export default async function Page({
const slugPath = slug.join("/");
try {
const mdxSource = await getMdxSource(slugPath, framework);
let mdxSource;
try {
mdxSource = await import(`./${slugPath}.mdx`);
} catch (error) {
mdxSource = await import(`./${slugPath}/${framework}.mdx`);
}
const { default: Content, tableOfContents } = mdxSource;
// Exclude h1 from table of contents
const tocItems = (tableOfContents as Toc)?.[0]?.children;
return (
<>
<DocsLayout toc={tocItems} nav={<DocNav />}>
<Prose className="overflow-x-hidden lg:flex-1 py-8">
<Content />
</Prose>
{tocItems && <TableOfContents items={tocItems} />}
</>
</DocsLayout>
);
} catch (error) {
return (
<DocsLayout>
<ComingSoonPage />
<DocsLayout nav={<DocNav />}>
<Prose className="overflow-x-hidden lg:flex-1 py-8">
<ComingSoonPage className="max-w-3xl mx-auto" />
</Prose>
</DocsLayout>
);
}

View File

@@ -192,24 +192,17 @@ Wrap your app components with the `JazzProvider:
<CodeGroup>
```tsx
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
import { JazzProvider } from "jazz-react-native";
import { MyAppAccount } from "./schema";
export function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, state] = useDemoAuth();
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
return (
<>
<JazzProvider
auth={auth}
peer="wss://cloud.jazz.tools/?key=you@example.com"
AccountSchema={MyAppAccount}
>
{children}
</JazzProvider>
<DemoAuthBasicUI appName="My App" state={state} />
</>
<JazzProvider
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
AccountSchema={MyAppAccount}
>
{children}
</JazzProvider>
);
}

View File

@@ -4,62 +4,34 @@ import { CodeGroup } from "@/components/forMdx";
# <span id="react">React</span>
First you define a context providing component (typically called `JazzAndAuth`) that uses:
- the Jazz context provider with your app configs
- the hooks and default/custom UI of one of the [Auth Methods](/docs/react/authentication/auth-methods).
This is also where you specify the sync & storage server to connect to (see [Sync and storage](/docs/react/sync-and-storage)).
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider } from "jazz-react";// old
import { MyAppAccount } from "./schema";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName });
return (
<>
<JazzProvider
auth={passkeyAuth}
peer="wss://cloud.jazz.tools/?key=you@example.com"
AccountSchema={MyAppAccount}
>
{children}
</JazzProvider>
<PasskeyAuthBasicUI state={passKeyState} />
</>
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" {
interface Register {
Account: MyAppAccount;
}
}
```
</CodeGroup>
With `JazzAndAuth` defined, you can wrap your app in it and then use the hooks within your App.
Wrap your application with `<JazzProvider />`, this is where you specify the sync & storage server to connect to (see [Sync and storage](/docs/react/sync-and-storage)).
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider } from "jazz-react";
import { MyAppAccount } from "./schema";
ReactDOM.createRoot(document.getElementById("root")!).render( // old
<React.StrictMode> // old
<JazzAndAuth>
<App />
</JazzAndAuth>
</React.StrictMode>// old
<JazzProvider
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
AccountSchema={MyAppAccount}
>
<App />
</JazzProvider>
);// old
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" {
interface Register {
Account: MyAppAccount;
}
}
```
</CodeGroup>
## Next.js
### Client-side only
@@ -73,25 +45,18 @@ The easiest way to use Jazz with Next.JS is to only use it on the client side. Y
```tsx
"use client"
import { JazzProvider } from "jazz-react"; // old
import { PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";// old
import { MyAppAccount } from "./schema"; // old
function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
return ( // old
<> // old
<JazzProvider // old
auth={passkeyAuth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} // old
>
{children} // old
</JazzProvider> // old
<PasskeyAuthBasicUI state={passKeyState} /> // old
</> // old
); // old
} // old
export function MyJazzProvider(props: { children: React.ReactNode }) {
return (
<JazzProvider
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
AccountSchema={MyAppAccount}
>
{props.children}
</JazzProvider>
);
}
```
</CodeGroup>

View File

@@ -56,12 +56,11 @@ export class MyAccount extends Account {
import { JazzProvider } from 'jazz-svelte';
// Example configuration for authentication and peer connection
let auth = null; // Replace with your auth implementation
let peer = "wss://your-peer-endpoint";
let sync = { peer: "wss://cloud.jazz.tools/?key=you@example.com" };
let AccountSchema = MyAccount;
</script>
<JazzProvider {auth} {peer} {AccountSchema}>
<JazzProvider {sync} {AccountSchema}>
<App />
</JazzProvider>
```

View File

@@ -108,7 +108,7 @@ Update the `src/main.ts` file to integrate Jazz:
<CodeGroup>
```typescript
import "./assets/main.css";
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
import { JazzProvider } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import router from "./router";
@@ -123,8 +123,6 @@ declare module "jazz-vue" {
const RootComponent = defineComponent({
name: "RootComponent",
setup() {
const { authMethod, state } = useDemoAuth();
return () => [
h(
JazzProvider,
@@ -137,12 +135,6 @@ const RootComponent = defineComponent({
default: () => h(App),
},
),
state.state !== "signedIn" &&
h(DemoAuthBasicUI, {
appName: "Jazz Vue Todo",
state,
}),
];
},
});

View File

@@ -192,8 +192,6 @@ export class MyAppAccount extends Account {
root: {},
});
if (!result) throw new Error("Root missing!"); // this should never happen
const { root } = result;
// we specifically need to check for undefined,

View File

@@ -0,0 +1,377 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Jazz 0.10.0 is out!" };
# Jazz 0.10.0 is out!
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
11 February 2025
</h2>
<div>
For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.
The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.
There are also some other minor improvements that will make your Jazz experience even better!
<h3>What's new?</h3>
Here is what's changed in this release:
- [New authentication flow](/docs/upgrade/0-10-0#new-authentication-flow): Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
- [Local-only mode](/docs/upgrade/0-10-0#local-only-mode): Users can now explore your app in local-only mode before signing up.
- [Improvements on the loading APIs](/docs/upgrade/0-10-0#improved-loading-api); `ensureLoaded` now always returns a value and `useCoState` now returns `null` if the value is not found.
- [Jazz Workers on native WebSockets](/docs/upgrade/0-10-0#native-websocket-for-jazz-workers): Improves compatibility with a wider set of Javascript runtimes.
- [Group inheritance with role mapping](/docs/upgrade/0-10-0#group-inheritance): Groups can now inherit members from other groups with a fixed role.
- Support for Node 14 dropped on cojson.
- Bugfix: `Group.removeMember` now returns a promise.
</div>
<h3 id="new-authentication-flow">New authentication flow</h3>
<div>
Up until now authentication has been the first part to figure out when building a Jazz app, and this was a stumbling block for many.
Now it is no longer required and setting up a Jazz app is as easy as writing this:
<CodeGroup>
{/* prettier-ignore */}
```tsx
<JazzProvider
auth={authMethod} // removed // *bin*
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // *bin*
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // *add*
>
<App />
</JazzProvider>
```
</CodeGroup>
New users are initially authenticated as "anonymous" and you can sign them up later with the hooks/providers of your chosen auth method.
Your auth code can now blend into your component tree:
<ContentByFramework framework="react">
<CodeGroup>
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const [isSignUp, setIsSignUp] = useState(true);
const auth = usePasskeyAuth({ // Must be inside the JazzProvider!
appName: "My super-cool web app",
});
const handleViewChange = () => {
setIsSignUp(!isSignUp);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSignUp) {
await auth.signUp(username);
} else {
await auth.logIn();
}
onOpenChange(false);
};
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native">
<CodeGroup>
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const [isSignUp, setIsSignUp] = useState(true);
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({ // Must be inside the JazzProvider!
wordlist,
});
const handleViewChange = () => {
setIsSignUp(!isSignUp);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSignUp) {
await auth.signUp();
} else {
await auth.logIn(loginPassphrase);
}
onOpenChange(false);
};
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="svelte">
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { usePasskeyAuth } from 'jazz-svelte';
const auth = usePasskeyAuth({ appName: "My super-cool web app" });
let error = $state<string | undefined>(undefined);
function signUp(e: Event) {
const formData = new FormData(e.currentTarget as HTMLFormElement);
const name = formData.get('name') as string;
if (!name) {
error = 'Name is required';
return;
}
e.preventDefault();
error = undefined;
auth.current.signUp(name).catch((e) => {
error = e.message;
});
}
function logIn() {
error = undefined;
auth.current.logIn().catch((e) => {
error = e.message;
});
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="vue">
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { usePasskeyAuth } from 'jazz-vue';
import { ref } from 'vue';
const auth = usePasskeyAuth({ appName: "My super-cool web app" });
const error = ref<string | undefined>(undefined);
function signUp(e: Event) {
const formData = new FormData(e.currentTarget as HTMLFormElement);
const name = formData.get('name') as string;
if (!name) {
error.value = 'Name is required';
return;
}
e.preventDefault();
error.value = undefined;
auth.value.signUp(name).catch((e) => {
error.value = e.message;
});
}
function logIn(e: Event) {
error.value = undefined;
e.preventDefault();
auth.value.logIn().catch((e) => {
error.value = e.message;
});
}
```
</CodeGroup>
</ContentByFramework>
When a user signs up with one of the auth methods we upgrade the current anonymous account to an authenticated account by storing the account's secret keys in the auth method.
<ContentByFramework framework={["react"]}>
To check if the user has registered you can use the new `useIsAuthenticated` hook:
<CodeGroup>
{/* prettier-ignore */}
```tsx
export function AuthButton() {
const isAuthenticated = useIsAuthenticated();
const { logOut } = useAccount();
const [open, setOpen] = useState(false);
if (isAuthenticated) {
return (
<Button
variant="outline"
onClick={logOut}
>
Sign out
</Button>
);
}
return (
<>
<Button onClick={() => setOpen(true)}>
Sign up
</Button>
<AuthModal open={open} onOpenChange={setOpen} />
</>
);
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework={["react-native"]}>
To check if the user has registered you can use the new `useIsAuthenticated` hook:
<CodeGroup>
{/* prettier-ignore */}
```tsx
export function AuthButton() {
const isAuthenticated = useIsAuthenticated();
const { logOut } = useAccount();
const [open, setOpen] = useState(false);
if (isAuthenticated) {
return (
<Button
variant="outline"
onPress={logOut}
>
Sign out
</Button>
);
}
return (
<>
<Button onPress={() => setOpen(true)}>
Sign up
</Button>
<AuthModal open={open} onOpenChange={setOpen} />
</>
);
}
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework={["react"]}>
If you do want to require your users to register before using your app, you can do it by moving the auth code outside of your app and conditionally rendering your app as a child -- or apply this pattern for specific parts of the app you'd like to gate behind authentication.
Our provided "Basic UIs" for the different auth methods render their children conditionally by default, so you can either nest them in your app without children, or nest your app or gated parts of your app inside *them*, depending on the desired behavior.
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react";
ReactDOM.createRoot(document.getElementById("root")!).render(
<JazzProvider
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }}
>
<PasskeyAuthBasicUI appName="My super-cool web app">
<App />
</PasskeyAuthBasicUI>
</JazzProvider>
);
```
</CodeGroup>
</ContentByFramework>
For the changes related to the specific auth providers see the updated [authentication docs](/docs/authentication/overview).
</div>
<h3 id="local-only-mode">Local-only mode</h3>
<div>
If you are ok with data not being persisted on the sync server for anonymous users, you can now set your app to local-only depending on the user's authentication state.
With `sync.when` set to `"signedUp"` the app will work in local-only mode when the user is anonymous and unlock the multiplayer/multi-device features and cloud persistence when they sign up:
<CodeGroup>
```ts
<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>
You can control when Jazz will sync by switching the `when` config to `"always"` or `"never"`.
</div>
<h3 id="improved-loading-api">Improvements on the loading APIs</h3>
<div>
Before 0.10.0 `ensureLoaded` was returning a nullable value forcing the Typescript code to always include null checks:
<CodeGroup>
```ts
export async function updateActiveTrack(track: MusicTrack) { // No need to pass the account
const me = await MusicaAccount.getMe().ensureLoaded({
root: {},
});
if (!me) { // Technically can never happen
throw new Error("User not found");
}
me.root.activeTrack = track;
}
```
</CodeGroup>
Now `ensureLoaded` always returns a value, making it's usage leaner:
<CodeGroup>
```ts
export async function updateActiveTrack(track: MusicTrack) { // No need to pass the account
const { root } = await MusicaAccount.getMe().ensureLoaded({
root: {},
});
// Null checks are no more required
root.activeTrack = track;
}
```
</CodeGroup>
It will throw if the value is not found, which can happen if the user tries to resolve a value that is not available.
`useCoState` now returns `null` if the value is not found, making it now possible to check against not-found values:
<CodeGroup>
```ts
const value = useCoState(MusicTrack, id, {});
if (value === null) {
return <div>Track not found</div>;
}
```
</CodeGroup>
</div>
<h3 id="native-websocket-for-jazz-workers">Jazz Workers on native WebSockets</h3>
<div>
We have removed the dependency on `ws` and switched to the native WebSocket API for Jazz Workers.
This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.
If you are using a Node.js version lower than 22 you will need to install the `ws` package and provide the WebSocket constructor:
<CodeGroup>
```ts
import { WebSocket } from "ws";
import { startWorker } from "jazz-nodejs";
const { worker } = await startWorker({
WebSocket,
});
```
</CodeGroup>
</div>
<h3 id="group-inheritance">Group inheritance with role mapping</h3>
<div>
You can override the inherited role by passing a second argument to `extend`.
This can be used to give users limited access to a child group:
<CodeGroup>
```ts
const organization = Group.create();
const billing = Group.create();
billing.extend(organization, "reader");
```
</CodeGroup>
This way the members of the organization can only read the billing data, even if they are admins in the organization group.
More about the group inheritance can be found in the [dedicated docs page](/docs/groups/inheritance).
</div>

View File

@@ -17,12 +17,12 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
<CodeGroup>
{/* prettier-ignore */}
```ts
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue.tsx";
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue.tsx";
function App() {
const [issue, setIssue] = useState<Issue>();
const [issue, setIssue] = useState<Issue>();
const createIssue = () => {
setIssue(Issue.create(
{
@@ -33,13 +33,13 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
}, // The owner defaults now to a group managed by the current user!
));
};
if (issue) {
return <IssueComponent issue={issue} />;
} else {
return <IssueComponent issue={issue} />;
} else {
return <button onClick={createIssue}>Create Issue</button>;
}
}
}
}
```
</CodeGroup>
@@ -71,4 +71,4 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
Everything is backward compatible, so no upgrade steps are required.
With this Jazz API becomes way more lean and more is coming!
</div>
</div>

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