Compare commits

...

136 Commits

Author SHA1 Message Date
Anselm Eickhoff
c3c723776e Merge pull request #2268 from garden-co/changeset-release/main
Version Packages
2025-05-19 12:57:06 +01:00
github-actions[bot]
33d7789530 Version Packages 2025-05-19 11:53:38 +00:00
Anselm Eickhoff
dcd7398b06 Merge pull request #2150 from garden-co/feature/zod-horribly
Zod support (using a schema translation layer)
2025-05-19 12:51:00 +01:00
Anselm
c3dd8d0271 Upgrade guide skeleton 2025-05-19 12:50:15 +01:00
Anselm
afe0accad5 Merge branch 'main' into feature/zod-horribly 2025-05-19 12:37:47 +01:00
Anselm
5835ed1274 Add changeset 2025-05-19 12:37:15 +01:00
Anselm
c5a6b87885 Fix betterauth example 2025-05-19 12:35:11 +01:00
Trisha Lim
294678e18e update thumbnail and desc for hend app (#2259) 2025-05-19 12:34:46 +01:00
Trisha Lim
9bb90489f8 feat(docs): link to Previous and Next pages (#2258) 2025-05-19 12:34:34 +01:00
Anselm
67efefde3f Merge branch 'main' into feature/zod-horribly 2025-05-19 12:32:37 +01:00
Anselm
5b28545714 More docs and allow broken homepage samples 2025-05-19 12:26:57 +01:00
Guido D'Orsi
b4352e38aa test: add await on group.removeMember 2025-05-19 13:16:21 +02:00
Guido D'Orsi
8015b8099b test: disable RetryUnavailable flaky test 2025-05-19 13:12:55 +02:00
Anselm
ee451ebd67 Port FileStreams docs (some schema methods missing) 2025-05-19 12:02:00 +01:00
Anselm
9dcd70c896 Port CoFeed docs 2025-05-19 11:56:07 +01:00
Anselm
bcacd04773 Post CoList docs 2025-05-19 11:53:32 +01:00
Anselm
65f20184e5 Port CoMap docs 2025-05-19 11:50:51 +01:00
Anselm
0c1817e5a4 Port accounts-and-migrations 2025-05-19 11:38:23 +01:00
Guido D'Orsi
addd31d43d test: disable RetryUnavailable flaky test 2025-05-19 12:25:54 +02:00
Giordano Ricci
53a02511e1 Merge pull request #2254 from garden-co/gio/betterauth-shadcn
Rework betterauth example
2025-05-19 11:24:18 +01:00
Anselm
c66aa631b3 Remove references to guide for now 2025-05-19 11:14:03 +01:00
Anselm
49a84bd8c5 Make .by and .members props again 2025-05-19 10:56:39 +01:00
Giordano Ricci
218ec7eb8d Merge branch 'main' into gio/betterauth-shadcn 2025-05-19 10:53:33 +01:00
Giordano Ricci
54f313e41e chore: readme & .env 2025-05-19 09:46:03 +01:00
Anselm
f14d971a75 Fix useAcceptInvite in jazz-react-native-core 2025-05-19 09:15:31 +01:00
Anselm
5290452e52 Fix CoList schema bug 2025-05-19 08:48:47 +01:00
Anselm
f49c24807a Upgrade to latest zod 2025-05-18 20:50:06 +01:00
Anselm Eickhoff
938f6767e4 add zod sponsor message 2025-05-18 20:30:23 +01:00
Anselm
b053369589 Make primitive objects & arrays actually work 2025-05-18 20:11:32 +01:00
Anselm
42c7f48c52 Fix CoFeed Proxy stuff and typecheck org example 2025-05-18 18:07:02 +01:00
Anselm
cd3c2cbe81 Move CoFeed entries into perAccount subobject 2025-05-18 17:39:21 +01:00
Anselm
e55bcafedb Make remaining tests pass 2025-05-18 17:03:41 +01:00
Anselm
dd3f7b5685 Make remaining examples build 2025-05-18 16:57:42 +01:00
Anselm
bde9d7637d Fix more examples 2025-05-18 16:36:49 +01:00
Anselm
b3f1bba17f Port all example apps except richtext and vue 2025-05-18 16:22:36 +01:00
Anselm
eb33fe6cf0 Handle non-collaborative objects & arrays 2025-05-18 15:25:04 +01:00
Anselm
54cf1b5906 Fix file name typo 2025-05-18 15:15:10 +01:00
Anselm
c7f48a0750 Split zod schema stuff into files 2025-05-18 15:13:01 +01:00
Anselm
f0419c5cde Port a bunch more apps, fix more stuff 2025-05-18 14:19:00 +01:00
Anselm
500ceb593c Add FileStreamSchema.createFromBlob, start working through examples 2025-05-18 11:09:04 +01:00
Anselm
51251fd87c Fix react package types 2025-05-18 10:28:26 +01:00
Anselm
ce27b1d6f9 Properly implement co.profile and inbox (all jazz-tools tests pass) 2025-05-18 10:25:53 +01:00
Anselm
dc46e36281 Merge branch 'emil/comap-doc-typecheck' into feature/zod-horribly 2025-05-17 22:58:10 +01:00
Anselm
b2bef6ffc2 Update intro & schema docs 2025-05-17 22:54:23 +01:00
Anselm
2766009ce6 Get rid of registered accounts 2025-05-17 22:04:22 +01:00
Anselm
8f88b212c1 Attempt to make form example work 2025-05-17 21:25:20 +01:00
Anselm
32e0133468 Extra changes after merge 2025-05-17 19:32:24 +01:00
Anselm
bf88604943 Merge branch 'main' into feature/zod-horribly 2025-05-17 19:32:11 +01:00
Anselm
9b66fde52d Add co.richText() 2025-05-17 17:43:00 +01:00
Anselm
1caa2761a4 Port a bunch of higher level packages 2025-05-17 17:28:47 +01:00
Anselm
4d54f1ec50 Port jazz-react-core 2025-05-17 16:28:25 +01:00
Anselm
135ca17438 Fix overly strict type assertions 2025-05-17 16:21:19 +01:00
Anselm
a5f9223215 Port requestToJoin (types only) 2025-05-17 16:12:06 +01:00
Anselm
b14a045cdb Port inbox tests (only types) 2025-05-17 15:44:52 +01:00
Anselm
0dfbda0487 Mostly make deep loading work 2025-05-17 15:26:20 +01:00
Anselm
19cb9c3ad3 port ContextManager test 2025-05-17 14:38:54 +01:00
Anselm
646c12f093 Make arbitrarily nested schema unions work 2025-05-17 14:22:57 +01:00
Anselm
fbbaad3e80 Port testing test 2025-05-17 11:59:50 +01:00
Anselm
425aa476c8 Port subscribe tests 2025-05-17 11:54:24 +01:00
Anselm
6922431197 Prefer accepting/returning undefined for optional refs in CoLists 2025-05-17 11:43:54 +01:00
Guido D'Orsi
31614c0a4f Merge pull request #2263 from garden-co/changeset-release/main
Version Packages
2025-05-17 08:58:51 +02:00
Emil Sayahi
07222cd6b0 fix: remove extra line 2025-05-16 15:58:18 -04:00
Emil Sayahi
4a88732f89 feat(docs): typechecking for CoMap docs 2025-05-16 15:57:55 -04:00
github-actions[bot]
57b69eb8da Version Packages 2025-05-16 19:49:36 +00:00
Guido D'Orsi
066676c243 Merge pull request #2264 from garden-co/feat/storage-streaming
feat(storage): implement content streaming
2025-05-16 21:46:46 +02:00
Giordano Ricci
2aac54b91d remove callback param from sso button 2025-05-16 20:29:55 +01:00
Giordano Ricci
4e4b53c420 remove leftover routes handlers 2025-05-16 20:24:33 +01:00
Guido D'Orsi
e141024656 feat(storage): implement content streaming 2025-05-16 20:36:09 +02:00
Guido D'Orsi
2c48ae0434 Merge pull request #2262 from garden-co/feat/storage-streaming
feat(storage): implement chunking for large content files
2025-05-16 19:46:36 +02:00
Guido D'Orsi
2bf974390d feat(storage): implement chunking for large content files 2025-05-16 18:41:14 +02:00
Anselm
a5116b9d63 Most CoList tests passing 2025-05-16 17:24:36 +01:00
Anselm
91ffdcc82f Fix _refs types 2025-05-16 17:19:15 +01:00
Anselm
810a7e6db2 Add getMe() 2025-05-16 16:58:23 +01:00
Joe Innes
e123715819 Fix missing $ in template literal (#2261)
Svelte interpolates curly braces in normal strings, but not template literals.
2025-05-16 16:47:11 +01:00
Anselm
1b1c0a98af Add co.account().createAs 2025-05-16 16:42:53 +01:00
Anselm
9fdf5b4831 Make first custom account test pass 2025-05-16 16:40:02 +01:00
Trisha Lim
0d087f3d4c docs: all roles can remove themselves from a Group (#2255) 2025-05-16 13:44:27 +01:00
Giordano Ricci
e2c222c8a6 password recovery flow 2025-05-16 12:08:12 +01:00
Guido D'Orsi
e410823087 Merge pull request #2251 from garden-co/changeset-release/main
Version Packages
2025-05-16 12:26:58 +02:00
Giordano Ricci
5262580222 cleanup layout 2025-05-16 11:26:25 +01:00
github-actions[bot]
dcc11e5b60 Version Packages 2025-05-16 10:22:41 +00:00
Guido D'Orsi
1b05fe5b55 Merge pull request #2252 from garden-co/fix/remove-members-no-parent-access
fix(group): removing members when the admin doesn't have access to the parent group read keys
2025-05-16 12:18:14 +02:00
Guido D'Orsi
de8f896bf9 Merge pull request #2247 from garden-co/feat/skipInvalid
feat: add $onError to catch errors on resolve
2025-05-16 12:17:10 +02:00
Guido D'Orsi
d63716a827 fix(group): removing members when the admin doesn't have access to the parent group read keys 2025-05-16 12:11:03 +02:00
Guido D'Orsi
a1e7fce3b9 Merge pull request #2250 from garden-co/fix/invites-upgrades
fix(invite): restore role upgrades an inviting revoked members
2025-05-16 12:02:50 +02:00
Guido D'Orsi
d5edad7ba5 fix(invite): restore role upgrades an inviting revoked members 2025-05-16 12:02:32 +02:00
Guido D'Orsi
559a4a223b test: cover more logic for $onError 2025-05-16 11:18:27 +02:00
Guido D'Orsi
16d5553ccd docs: replace expect in the example with comments 2025-05-16 09:52:01 +02:00
Guido D'Orsi
ef76c586cc docs: improve the Requesting Invites section 2025-05-15 23:25:54 +02:00
Guido D'Orsi
8ea4b9761c docs: improve the Requesting Invites section 2025-05-15 23:24:38 +02:00
Guido D'Orsi
a81f281079 feat: move to 2025-05-15 23:11:18 +02:00
Guido D'Orsi
3ecb602459 fix: remove Jazz paper scissors from the examples list because the worker isn't deployed 2025-05-15 23:09:16 +02:00
Meg Culotta
ea69ea1f67 Merge pull request #2004 from garden-co/feat/1936-add-request-invites-to-docs
Add request invites to docs
2025-05-15 15:25:28 -05:00
Margaret Culotta
e430bd061e remove unnecessary break 2025-05-15 15:18:35 -05:00
Guido D'Orsi
a957485172 chore: docs improvements and more tests 2025-05-15 19:52:00 +02:00
Margaret Culotta
9060975dfb Merge main and handle conflicts 2025-05-15 12:41:32 -05:00
Margaret Culotta
753ec83fdd remove unnecessary comments, clean up 2025-05-15 12:34:05 -05:00
Margaret Culotta
44a9785e93 clean up, clarify and simplfy examples 2025-05-15 12:28:55 -05:00
Trisha Lim
d9b390e538 remove support for demos in examples page (#2230) 2025-05-15 17:57:12 +01:00
Guido D'Orsi
b194f7831b fix: fix return type of $skipInvalid when used on nested lists 2025-05-15 18:17:53 +02:00
Giordano Ricci
9892dd708f improvements, restore SSO button 2025-05-15 17:07:14 +01:00
Guido D'Orsi
b60be9405d docs: add $skipInvalid usage 2025-05-15 17:48:32 +02:00
Guido D'Orsi
84588e0798 feat(organization): add remove member flow and add tests 2025-05-15 17:11:56 +02:00
Guido D'Orsi
e5b170f25e feat: add $skipInvalid resolve keyword 2025-05-15 17:11:09 +02:00
Giordano Ricci
b6244582c7 wrap up 2025-05-15 15:34:19 +01:00
Giordano Ricci
37ab25ff62 wip: replace home components 2025-05-15 11:39:53 +01:00
Giordano Ricci
4750e23800 wip: login & signup forms 2025-05-15 11:18:00 +01:00
Margaret Culotta
63d0b0673e run linter 2025-05-14 11:49:21 -05:00
Margaret Culotta
2b456b5e07 add approval code snippet 2025-05-14 11:45:25 -05:00
Margaret Culotta
e20809d314 fix errors in code snippets 2025-05-14 11:12:26 -05:00
Margaret Culotta
b6de11e125 Merge branch 'main' into feat/1936-add-request-invites-to-docs 2025-05-13 12:56:38 -05:00
Anselm
09b815b87e WIP Account schemas 2025-05-13 16:27:52 +01:00
Anselm
db3c737e9f Make co.feed() work 2025-05-09 16:31:24 +01:00
Anselm
28d7fe9b90 Start updating the homepage 2025-05-09 15:29:55 +01:00
Anselm
4c70ca198b Port coList tests to zod 2025-05-09 14:40:54 +01:00
Anselm
742283263f Get rid of coField<...>, stop using instance getters on CoMaps 2025-05-09 14:26:55 +01:00
Anselm
5f4e9a9936 Trying to make records work 2025-05-09 10:35:18 +01:00
Anselm
2b40b3052c Implement the rest of CoMap loading & subscribing 2025-05-08 15:05:55 +01:00
Anselm
9dd926a59a Add todo for discriminated union narrowing 2025-05-08 12:08:35 +01:00
Anselm
9aa6ab6fea Make discriminated unions of CoMaps work 2025-05-08 12:03:31 +01:00
Anselm
4207fb5914 Use zod schemas for simple CoMap tests 2025-05-08 10:58:02 +01:00
Anselm
cb3e5cbe81 Fix build, update RN (to support zod4's wildcard exports) and make tests pass 2025-05-07 20:07:55 +01:00
Anselm
d199fb06b3 Make ImageDefs work, fix circular imports 2025-05-07 17:29:52 +01:00
Anselm
0eaa500aef Make basic chat app work 2025-05-07 11:17:39 +01:00
Anselm
774c05cef0 Merge branch 'main' into feature/zod-horribly 2025-05-07 09:28:45 +01:00
Anselm
96a1f303fe Merge branch 'refactor/combine-covalue-core-and-state' into feature/zod-horribly 2025-05-06 15:14:47 +01:00
Anselm
6a55a548a0 Replacing ID<CoValue> in favor of string, initial "soft" step 2025-05-06 11:17:22 +01:00
Anselm
0e87c1c971 Fix more occurences of coField 2025-05-05 15:28:48 +01:00
Anselm
89fac342bf Rename co -> coField in preparation of giving co a new meaning 2025-05-05 11:38:35 +01:00
Margaret Culotta
d23c71d511 working through twoslash errors 2025-04-30 08:11:32 -05:00
Benjamin S. Leveritt
b24071cf33 Fixes example for TwoSlash 2025-04-28 16:36:25 +01:00
Benjamin S. Leveritt
a5c88c08de Merge main into feat/1936-add-request-invites-to-docs 2025-04-28 16:14:17 +01:00
Margaret Culotta
906932db1e update imports and mocks for twoslash 2025-04-24 12:22:17 -05:00
Margaret Culotta
2033e35a41 Merge branch 'main' into feat/1936-add-request-invites-to-docs 2025-04-24 11:26:09 -05:00
Margaret Culotta
5a2a12ccbb update twoslash imports 2025-04-18 08:10:50 -05:00
Margaret Culotta
d288cb6954 update docs for request invite 2025-04-17 10:18:17 -05:00
515 changed files with 15465 additions and 11629 deletions

View File

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

2
.gitignore vendored
View File

@@ -29,3 +29,5 @@ test-results
.cursorrules
.windsurfrules
playwright-report

View File

@@ -1 +1 @@
BETTER_AUTH_SECRET="TEST_SECRET"
BETTER_AUTH_SECRET="TEST_SECRET"

View File

@@ -1,5 +1,38 @@
# betterauth
## 0.1.5
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-betterauth-server-plugin@0.14.0
- jazz-inspector@0.14.0
- jazz-react@0.14.0
- jazz-react-auth-betterauth@0.14.0
- jazz-betterauth-client-plugin@0.14.0
## 0.1.4
### Patch Changes
- jazz-betterauth-server-plugin@0.13.32
- jazz-react@0.13.32
- jazz-react-auth-betterauth@0.13.32
- jazz-betterauth-client-plugin@0.13.32
## 0.1.3
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-betterauth-server-plugin@0.13.31
- jazz-inspector@0.13.31
- jazz-react@0.13.31
- jazz-react-auth-betterauth@0.13.31
- jazz-betterauth-client-plugin@0.13.31
## 0.1.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
# Better Auth Integration Example
This example demonstrates using Jazz with Better Auth and Next.js.
This example demonstrates how to integrate [Better Auth](https://www.better-auth.com/) with Jazz.
## Getting started
@@ -29,21 +29,26 @@ pnpm dev
### Using the monorepo
This requires `pnpm` to be installed; see [https://pnpm.io/installation](https://pnpm.io/installation).
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
1. Clone the `jazz` repository.
```sh
Clone the jazz repository.
```bash
git clone https://github.com/garden-co/jazz.git
```
2. Install dependencies.
```sh
cd jazz
pnpm install
Install and build dependencies.
```bash
pnpm i && npx turbo build
```
3. Navigate to the example and start the development server.
```sh
cd examples/betterauth
Go to the example directory.
```bash
cd jazz/examples/betterauth/
```
Start the dev server.
```bash
pnpm dev
```
The example should be running at [http://localhost:3000](http://localhost:3000) by default.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -1,6 +1,6 @@
{
"name": "betterauth",
"version": "0.1.2",
"version": "0.1.5",
"private": true,
"type": "module",
"scripts": {
@@ -15,18 +15,26 @@
"email": "email dev --dir src/components/emails"
},
"dependencies": {
"@react-email/components": "^0.0.38",
"@icons-pack/react-simple-icons": "^12.8.0",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-slot": "^1.2.2",
"better-auth": "^1.2.4",
"better-sqlite3": "^11.9.1",
"jazz-betterauth-server-plugin": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-betterauth-client-plugin": "workspace:*",
"jazz-betterauth-server-plugin": "workspace:*",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-react-auth-betterauth": "workspace:*",
"jazz-tools": "workspace:*",
"next": "15.3.1",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link-icon lucide-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>

Before

Width:  |  Height:  |  Size: 344 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-icon lucide-mail"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"/><rect x="2" y="4" width="20" height="16" rx="2"/></svg>

Before

Width:  |  Height:  |  Size: 301 B

View File

@@ -1 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

Before

Width:  |  Height:  |  Size: 823 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wrench-icon lucide-wrench"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,22 @@
import { Navbar } from "@/components/navbar";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Jazz Example: Better Auth",
description: "Jazz example application demonstrating Better Auth integration",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
<div className="container mx-auto pt-16 min-h-screen flex flex-col items-center justify-center">
{children}
</div>
</>
);
}

View File

@@ -0,0 +1,113 @@
"use client";
import { Button } from "@/components/ui/button";
import { useAccount } from "jazz-react";
import { Account } from "jazz-tools";
import {
AppWindowMacIcon,
FileTextIcon,
GlobeIcon,
WrenchIcon,
} from "lucide-react";
import Image from "next/image";
export default function Home() {
const { me } = useAccount(Account, { resolve: { profile: {} } });
if (!me) {
return null;
}
return (
<div className="grow flex flex-col items-center justify-center">
<main className="flex flex-col gap-8 row-start-2 grow justify-center">
<Image
src="/jazz-logo.svg"
alt="Jazz logo"
width={180}
height={38}
priority
/>
<p className="text-sm/6 text-center sm:text-left">
Signed in as{" "}
<span className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-mono font-semibold">
{me.profile.name}
</span>{" "}
with id{" "}
<span className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-mono font-semibold">
{me.id}
</span>
</p>
<div className="flex gap-4 items-center flex-row">
<Button asChild size="lg">
<a
href="https://jazz.tools/docs"
target="_blank"
rel="noopener noreferrer"
>
<Image src="/jazz.svg" alt="" width={20} height={20} />
Start building
</a>
</Button>
<Button asChild variant="secondary" size="lg">
<a
href="https://jazz.tools/docs"
target="_blank"
rel="noopener noreferrer"
>
<FileTextIcon className="size-4" />
Read the docs
</a>
</Button>
</div>
</main>
<footer className="flex gap-4 py-8">
<Button asChild variant="ghost">
<a
href="https://jazz.tools/api-reference"
target="_blank"
rel="noopener noreferrer"
>
<FileTextIcon className="size-4" />
API reference
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/examples"
target="_blank"
rel="noopener noreferrer"
>
<AppWindowMacIcon className="size-4" />
Examples
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/status"
target="_blank"
rel="noopener noreferrer"
>
<GlobeIcon className="size-4" />
Status
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/showcase"
target="_blank"
rel="noopener noreferrer"
>
<WrenchIcon className="size-4" />
Built with Jazz
</a>
</Button>
</footer>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { UserSettings } from "@/components/user-settings";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Settings | Jazz Example: Better Auth",
};
export default function SettingsPage() {
return (
<div className="max-w-screen-md w-full mx-auto px-4">
<UserSettings />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { ForgotPasswordForm } from "@/components/forgot-password-form";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Forgot password | Jazz Example: Better Auth",
};
export default function ForgotPasswordPage() {
return <ForgotPasswordForm />;
}

View File

@@ -0,0 +1,23 @@
import Image from "next/image";
import Link from "next/link";
interface Props {
children: React.ReactNode;
}
export default function AuthLayout({ children }: Props) {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<Link
href="/"
className="flex items-center gap-2 self-center font-medium"
>
<Image src="/jazz.svg" alt="Jazz Logo" width={24} height={24} />
Jazz BetterAuth Demo
</Link>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { ResetPasswordForm } from "@/components/reset-password-form";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Reset password | Jazz Example: Better Auth",
};
export default function ResetPasswordPage() {
return <ResetPasswordForm />;
}

View File

@@ -0,0 +1,11 @@
import { SigninForm } from "@/components/signin-form";
import type { Metadata } from "next";
import { ssoProviders } from "../sso-providers";
export const metadata: Metadata = {
title: "Sign in | Jazz Example: Better Auth",
};
export default function LoginPage() {
return <SigninForm providers={ssoProviders} />;
}

View File

@@ -0,0 +1,11 @@
import { SignupForm } from "@/components/signup-form";
import type { Metadata } from "next";
import { ssoProviders } from "../sso-providers";
export const metadata: Metadata = {
title: "Sign up | Jazz Example: Better Auth",
};
export default function LoginPage() {
return <SignupForm providers={ssoProviders} />;
}

View File

@@ -0,0 +1,4 @@
import type { SSOProviderType } from "jazz-react-auth-betterauth";
// Fill in the providers you want to use
export const ssoProviders: SSOProviderType[] = ["github"];

View File

@@ -1,10 +0,0 @@
"use client";
import { useAccount } from "jazz-react";
import { redirect } from "next/navigation";
export default function Page() {
const { logOut } = useAccount({ resolve: { profile: true } });
logOut();
redirect("/");
}

View File

@@ -1,7 +0,0 @@
"use client";
import ForgotForm from "@/components/forms/Forgot";
export default function ForgotPage() {
return <ForgotForm />;
}

View File

@@ -1,27 +1,123 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: #ffffff;
--foreground: #171717;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,17 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { JazzAndAuth } from "@/components/JazzAndAuth";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import { Toaster } from "@/components/ui/sonner";
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Jazz Example: Better Auth",
@@ -20,15 +10,16 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<JazzAndAuth>{children}</JazzAndAuth>
<body className="antialiased">
<JazzAndAuth>
{children}
<Toaster richColors />
</JazzAndAuth>
</body>
</html>
);

View File

@@ -1,5 +0,0 @@
"use client";
import MagicLinkSignIn from "@/components/routes/magic-link/signIn/page";
export default MagicLinkSignIn;

View File

@@ -1,5 +0,0 @@
"use client";
import MagicLinkSignUp from "@/components/routes/magic-link/logIn/page";
export default MagicLinkSignUp;

View File

@@ -1,11 +0,0 @@
"use client";
import { Verify } from "@/components/emails";
export default function Page() {
const searchParams = new URLSearchParams(window.location.search);
const name = searchParams.get("name") ?? undefined;
const url = searchParams.get("url") ?? undefined;
const otp = searchParams.get("otp") ?? undefined;
return <Verify name={name} url={url} otp={otp}></Verify>;
}

View File

@@ -1,11 +0,0 @@
"use client";
import { Reset } from "@/components/emails";
export default function Page() {
const searchParams = new URLSearchParams(window.location.search);
const name = searchParams.get("name") ?? undefined;
const url = searchParams.get("url") ?? undefined;
const otp = searchParams.get("otp") ?? undefined;
return <Reset name={name} url={url} otp={otp}></Reset>;
}

View File

@@ -1,11 +0,0 @@
"use client";
import { Login } from "@/components/emails";
export default function Page() {
const searchParams = new URLSearchParams(window.location.search);
const name = searchParams.get("name") ?? undefined;
const url = searchParams.get("url") ?? undefined;
const otp = searchParams.get("otp") ?? undefined;
return <Login name={name} url={url} otp={otp}></Login>;
}

View File

@@ -1,7 +0,0 @@
"use client";
import { Welcome } from "@/components/emails";
export default function Page() {
return <Welcome></Welcome>;
}

View File

@@ -1,165 +0,0 @@
"use client";
import { Button } from "@/components/Button";
import { useAccount, useIsAuthenticated } from "jazz-react";
import { useAuth } from "jazz-react-auth-betterauth";
import Image from "next/image";
import { useCallback } from "react";
export default function Home() {
const { authClient, account, state } = useAuth();
const hasCredentials = state !== "anonymous";
const { me, logOut } = useAccount({ resolve: { profile: {} } });
const isAuthenticated = useIsAuthenticated();
const signOut = useCallback(() => {
authClient.signOut().catch(console.error).finally(logOut);
}, [logOut, authClient]);
console.log("me", me);
console.log("account", account);
console.log("state", state);
console.log("hasCredentials", hasCredentials);
console.log("isAuthenticated", isAuthenticated);
return (
<>
<header className="absolute p-4 top-0 left-0 w-full z-10 flex items-center justify-between gap-4">
<div className="float-start flex gap-4">
{me && hasCredentials && isAuthenticated && (
<>
<Button onClick={signOut}>Sign out</Button>
<Button href="/settings">Settings</Button>
</>
)}
</div>
<div className="float-end flex gap-4">
{!hasCredentials && !isAuthenticated && (
<>
<Button href="/sign-in" variant="secondary">
Sign in
</Button>
<Button href="/sign-up">Sign up</Button>
</>
)}
</div>
</header>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
src="/jazz-logo.svg"
alt="Jazz logo"
width={180}
height={38}
priority
/>
<p className="text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
{me && hasCredentials && isAuthenticated && (
<>
{"Signed in as "}
<span className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
{me.profile.name}
</span>
.
</>
)}
{!hasCredentials && !isAuthenticated && <>Not signed in.</>}
{!hasCredentials && isAuthenticated && (
<>Not connected to the authentication server.</>
)}
{hasCredentials && !isAuthenticated && (
<>Authenticated, but not logged in. Try refreshing.</>
)}
</p>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://jazz.tools/"
target="_blank"
rel="noopener noreferrer"
>
<Image src="/jazz.svg" alt="Jazz logo" width={20} height={20} />
Start building
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center gap-2 hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto"
href="https://jazz.tools/docs"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Read the docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://jazz.tools/api-reference"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
API reference
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://jazz.tools/examples"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://jazz.tools/status"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Status
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://jazz.tools/showcase"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/wrench.svg"
alt="Wrench icon"
width={16}
height={16}
/>
Built with Jazz
</a>
</footer>
</div>
</>
);
}

View File

@@ -1,7 +0,0 @@
import { auth } from "@/lib/auth";
import { toNodeHandler } from "better-auth/node";
// Disallow body parsing, we will parse it manually
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth.handler);

View File

@@ -1,7 +0,0 @@
"use client";
import ResetForm from "@/components/forms/Reset";
export default function ResetPage() {
return <ResetForm />;
}

View File

@@ -1,7 +0,0 @@
"use client";
import SettingsForm from "@/components/forms/Settings";
export default function SettingsPage() {
return <SettingsForm providers={["github"]} />;
}

View File

@@ -1,7 +0,0 @@
"use client";
import SignInForm from "@/components/forms/SignIn";
export default function SignInPage() {
return <SignInForm providers={["github"]} />;
}

View File

@@ -1,7 +0,0 @@
"use client";
import SignUpForm from "@/components/forms/SignUp";
export default function SignUpPage() {
return <SignUpForm providers={["github"]} />;
}

View File

@@ -1,10 +0,0 @@
"use client";
import { useAuth } from "jazz-react-auth-betterauth";
import { redirect } from "next/navigation";
export default function Page() {
const { logIn } = useAuth();
logIn().then(redirect("/"));
return null;
}

View File

@@ -1,10 +0,0 @@
"use client";
import { useAuth } from "jazz-react-auth-betterauth";
import { redirect } from "next/navigation";
export default function Page() {
const { signIn } = useAuth();
signIn().then(redirect("/"));
return null;
}

View File

@@ -1,85 +0,0 @@
import { Button } from "@/components/Button";
import { AccountsType, useAuth } from "jazz-react-auth-betterauth";
export const AccountProviders = ({
setLoading,
setError,
accounts,
}: {
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
accounts: AccountsType | undefined;
}) => {
const auth = useAuth();
return (
<table className="w-full text-sm border-full border-collapse">
<thead className="text-xs">
<tr>
<th scope="col" className="px-6 py-3">
Provider
</th>
<th scope="col" className="px-6 py-3">
Created
</th>
<th scope="col" className="px-6 py-3">
Updated
</th>
<th scope="col" className="px-6 py-3">
Scopes
</th>
</tr>
</thead>
<tbody>
{!accounts?.data?.length && "No authentication providers found"}
{accounts?.data &&
accounts.data.map((account) => (
<tr key={account.id} className="border-b">
<th
scope="row"
className="px-6 py-4 font-medium whitespace-nowrap"
>
{account.provider}
</th>
<td className="px-6 py-4">
{account.createdAt.toLocaleString()}
</td>
<td className="px-6 py-4">
{account.updatedAt.toLocaleString()}
</td>
<td className="px-6 py-4">{account.scopes.join(", ")}</td>
<td className="px-6 py-4">
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await auth.authClient.unlinkAccount({
providerId: account.provider,
accountId: account.id,
});
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
Unlink
</Button>
</td>
</tr>
))}
</tbody>
</table>
);
};

View File

@@ -1,92 +0,0 @@
import { clsx } from "clsx";
import Image from "next/image";
import Link from "next/link";
import { forwardRef } from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger";
children?: React.ReactNode;
className?: string;
src?: string;
alt?: string;
imageClassName?: string;
href?: string;
newTab?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
children,
variant = "primary",
src,
alt,
imageClassName,
href,
newTab = false,
...buttonProps
},
ref,
) => {
const primary =
"rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto";
const secondary =
"rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center gap-2 hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto";
const danger =
"rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center bg-red-400 dark:bg-red-600 gap-2 hover:bg-red-300 dark:hover:bg-red-700 hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto";
const classes = clsx(
variant === "primary"
? primary
: variant === "secondary"
? secondary
: danger,
className,
);
const [width, height] = [
variant === "primary" ? 20 : 16,
variant === "primary" ? 20 : 16,
];
if (href) {
return (
<Link
href={href}
target={newTab ? "_blank" : undefined}
className={classes}
rel="noopener noreferrer"
>
{src && alt && (
<Image
className={imageClassName}
src={src}
alt={alt}
width={width}
height={height}
/>
)}
{children}
</Link>
);
} else {
return (
<button
ref={ref}
{...buttonProps}
className={classes}
rel="noopener noreferrer"
>
{src && alt && (
<Image
className={imageClassName}
src={src}
alt={alt}
width={width}
height={height}
/>
)}
{children}
</button>
);
}
},
);

View File

@@ -1,56 +0,0 @@
import { Button } from "@/components/Button";
import { useAuth } from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
import { forwardRef } from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children?: React.ReactNode;
src?: InstanceType<typeof Image>["src"];
alt?: InstanceType<typeof Image>["alt"];
callbackURL: string;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
}
export const DeleteAccountButton = forwardRef<HTMLButtonElement, ButtonProps>(
({ callbackURL, setLoading, setError }) => {
const router = useRouter();
const auth = useAuth();
return (
<Button
variant="danger"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await auth.authClient.deleteUser(
{
callbackURL: callbackURL,
},
{
onSuccess: () => {
router.replace(callbackURL);
},
},
);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
Delete account
</Button>
);
},
);

View File

@@ -1,6 +1,5 @@
"use client";
import { emailOTPClient, magicLinkClient } from "better-auth/client/plugins";
import { JazzProvider } from "jazz-react";
import { AuthProvider } from "jazz-react-auth-betterauth";
import { type ReactNode, lazy } from "react";
@@ -21,17 +20,14 @@ export function JazzAndAuth({ children }: { children: ReactNode }) {
peer: "wss://cloud.jazz.tools/?key=betterauth-example@garden.co",
}}
>
<>
<AuthProvider
options={{
baseURL: process.env.NEXT_PUBLIC_AUTH_BASE_URL,
plugins: [magicLinkClient(), emailOTPClient()],
}}
>
{children}
</AuthProvider>
<JazzDevTools />
</>
<AuthProvider
options={{
baseURL: process.env.NEXT_PUBLIC_AUTH_BASE_URL,
}}
>
{children}
</AuthProvider>
<JazzDevTools />
</JazzProvider>
);
}

View File

@@ -1,7 +0,0 @@
export function Loading() {
return (
<div className="flex h-full items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
);
}

View File

@@ -1,73 +1,137 @@
import { Button } from "@/components/Button";
import { SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import { socialProviderNames } from "jazz-react-auth-betterauth";
import { forwardRef } from "react";
import { Button } from "@/components/ui/button";
import {
SiApple,
SiDiscord,
SiDropbox,
SiFacebook,
SiGithub,
SiGitlab,
SiGoogle,
SiKick,
SiReddit,
SiRoblox,
SiSpotify,
SiTiktok,
SiTwitch,
SiVk,
SiX,
SiZoom,
} from "@icons-pack/react-simple-icons";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { toast } from "sonner";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children?: React.ReactNode;
src?: InstanceType<typeof Image>["src"];
alt?: InstanceType<typeof Image>["alt"];
provider: SSOProviderType;
link?: boolean;
callbackURL?: string;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
interface SocialProvider {
name: string;
icon?: ReactNode;
}
export const SSOButton = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
provider,
link = false,
callbackURL,
setLoading,
setError,
...buttonProps
},
ref,
) => {
const auth = useAuth();
const providerName = socialProviderNames[provider];
return (
<Button
src={`/social/${provider}.svg`}
alt={`${providerName} logo`}
imageClassName="absolute left-3 dark:invert"
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await (async () => {
if (link) {
return await auth.authClient.linkSocial({
provider: provider,
});
} else {
return await auth.authClient.signIn.social({
provider: provider,
callbackURL: callbackURL,
});
}
})();
if (error) {
setError({
...error,
name: error.message ?? error.statusText,
message: error.message ?? error.statusText,
});
}
setLoading(false);
}}
{...buttonProps}
ref={ref}
>
{link
? `Link ${providerName} account`
: `Continue with ${providerName}`}
{children}
</Button>
);
const socialProviderMap: Record<SSOProviderType, SocialProvider> = {
github: {
name: "GitHub",
icon: <SiGithub />,
},
);
google: {
name: "Google",
icon: <SiGoogle />,
},
apple: {
name: "Apple",
icon: <SiApple />,
},
discord: {
name: "Discord",
icon: <SiDiscord />,
},
facebook: {
name: "Facebook",
icon: <SiFacebook />,
},
microsoft: {
name: "Microsoft",
},
twitter: {
name: "X",
icon: <SiX />,
},
dropbox: {
name: "Dropbox",
icon: <SiDropbox />,
},
linkedin: {
name: "LinkedIn",
},
gitlab: {
name: "GitLab",
icon: <SiGitlab />,
},
kick: {
name: "Kick",
icon: <SiKick />,
},
tiktok: {
name: "TikTok",
icon: <SiTiktok />,
},
twitch: {
name: "Twitch",
icon: <SiTwitch />,
},
vk: {
name: "VK",
icon: <SiVk />,
},
zoom: {
name: "Zoom",
icon: <SiZoom />,
},
roblox: {
name: "Roblox",
icon: <SiRoblox />,
},
reddit: {
name: "Reddit",
icon: <SiReddit />,
},
spotify: {
name: "Spotify",
icon: <SiSpotify />,
},
};
interface Props {
provider: SSOProviderType;
}
export function SSOButton({ provider }: Props) {
const auth = useAuth();
const router = useRouter();
return (
<Button
type="button"
variant="outline"
onClick={() => {
auth.authClient.signIn.social(
{
provider,
},
{
onSuccess: () => {
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
}}
>
{socialProviderMap[provider].icon}
Continue with {socialProviderMap[provider].name}
</Button>
);
}

View File

@@ -1,66 +0,0 @@
export const appName = "Jazz Example App";
export const main = {
backgroundColor: "#f6f9fc",
padding: "10px 0",
};
export const container = {
backgroundColor: "#ffffff",
border: "1px solid #f0f0f0",
padding: "45px",
};
export const text = {
fontSize: "16px",
fontFamily:
"'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
fontWeight: "300",
color: "#404040",
lineHeight: "26px",
};
export const h1 = {
...text,
fontSize: "24px",
fontWeight: "bold",
};
export const button = {
backgroundColor: "#146AFF",
borderRadius: "4px",
color: "#fff",
fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
fontSize: "15px",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
width: "210px",
padding: "14px 7px",
};
export const anchor = {
textDecoration: "underline",
};
export const codeContainer = {
background: "rgba(0,0,0,.05)",
borderRadius: "4px",
margin: "16px auto 14px",
verticalAlign: "middle",
width: "280px",
};
export const code = {
color: "#000",
fontFamily: "HelveticaNeue-Bold",
fontSize: "32px",
fontWeight: 700,
letterSpacing: "6px",
lineHeight: "40px",
paddingBottom: "8px",
paddingTop: "8px",
margin: "0 auto",
display: "block",
textAlign: "center" as const,
};

View File

@@ -1,91 +0,0 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import Image from "next/image";
import * as React from "react";
import {
appName,
button,
code,
codeContainer,
container,
h1,
main,
text,
} from "./Common";
export const Login = ({
name = undefined,
url = undefined,
otp = undefined,
}: {
name?: string;
url?: string;
otp?: string;
}) => {
return (
<Html>
<Head>
<title>
{url && !otp
? `Your login link for ${appName}`
: `Your login code for ${appName}`}
</title>
</Head>
<Preview>
{url && !otp
? `Your login link for ${appName}`
: `Your login code for ${appName}`}
</Preview>
<Body style={main}>
<Container style={container}>
<Image
src="/jazz.svg"
alt="Jazz logo"
width={100}
height={100}
priority
/>
<Heading style={h1}>
{url && !otp
? `Your login link for ${appName}`
: `Your login code for ${appName}`}
</Heading>
<Section>
<Text style={text}>Hello{name ? ` ${name}` : ""},</Text>
<Text style={text}>
Someone recently attempted to login to {appName} using your email
address.
{url && !otp ? " If this was you, you can login here:" : ""}
</Text>
{url && !otp && (
<Button style={button} href={url}>
Login
</Button>
)}
{otp && !url && (
<Section style={codeContainer}>
<Text style={code}>{otp}</Text>
</Section>
)}
<Text style={text}>
To keep your account secure, please do not forward this email to
anyone. If you did not try to login, just ignore and delete this
message.
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
export default Login;

View File

@@ -1,84 +0,0 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import Image from "next/image";
import * as React from "react";
import {
appName,
button,
code,
codeContainer,
container,
h1,
main,
text,
} from "./Common";
export const Reset = ({
name = undefined,
url = undefined,
otp = undefined,
}: {
name?: string;
url?: string;
otp?: string;
}) => {
return (
<Html>
<Head>
<title>{`Reset your ${appName} password`}</title>
</Head>
<Preview>Reset your {appName} password</Preview>
<Body style={main}>
<Container style={container}>
<Image
src="/jazz.svg"
alt="Jazz logo"
width={100}
height={100}
priority
/>
<Heading style={h1}>Reset your {appName} password</Heading>
<Section>
<Text style={text}>Hello{name ? ` ${name}` : ""},</Text>
<Text style={text}>
Someone recently requested a password change for your {appName}{" "}
account.
{url && !otp
? " If this was you, you can set a new password here:"
: ""}
</Text>
{url && !otp && (
<Button style={button} href={url}>
Reset password
</Button>
)}
{otp && !url && (
<Section style={codeContainer}>
<Text style={code}>{otp}</Text>
</Section>
)}
<Text style={text}>
If you do not want to change your password or did not request
this, just ignore and delete this message.
</Text>
<Text style={text}>
To keep your account secure, please do not forward this email to
anyone.
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
export default Reset;

View File

@@ -1,80 +0,0 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import Image from "next/image";
import * as React from "react";
import {
appName,
button,
code,
codeContainer,
container,
h1,
main,
text,
} from "./Common";
export const Verify = ({
name = undefined,
url = undefined,
otp = undefined,
}: {
name?: string;
url?: string;
otp?: string;
}) => {
return (
<Html>
<Head>
<title>{`Verify your ${appName} account`}</title>
</Head>
<Preview>Verify your {appName} account</Preview>
<Body style={main}>
<Container style={container}>
<Image
src="/jazz.svg"
alt="Jazz logo"
width={100}
height={100}
priority
/>
<Heading style={h1}>Verify your {appName} account</Heading>
<Section>
<Text style={text}>Hello{name ? ` ${name}` : ""},</Text>
<Text style={text}>
Someone recently signed up for a {appName} account using your
email address.
{url && !otp
? " If this was you, you can verify your account here:"
: ""}
</Text>
{url && !otp && (
<Button style={button} href={url}>
Verify account
</Button>
)}
{otp && !url && (
<Section style={codeContainer}>
<Text style={code}>{otp}</Text>
</Section>
)}
<Text style={text}>
To keep your account secure, please do not forward this email to
anyone.
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
export default Verify;

View File

@@ -1,53 +0,0 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Row,
Section,
Text,
} from "@react-email/components";
import Image from "next/image";
import * as React from "react";
import { appName, button, container, h1, main, text } from "./Common";
export const Welcome = () => {
return (
<Html>
<Head>
<title>{`Welcome to ${appName}`}</title>
</Head>
<Preview>Welcome to {appName}</Preview>
<Body style={main}>
<Container style={container}>
<Image
src="/jazz.svg"
alt="Jazz logo"
width={100}
height={100}
priority
/>
<Heading style={h1}>Welcome to {appName}</Heading>
<Section>
<Row>
<Text style={text}>
Congratulations! You have successfully registered for {appName}.
We are excited to have you on board!
</Text>
</Row>
</Section>
<Section>
<Button style={button} href="/">
Go home
</Button>
</Section>
</Container>
</Body>
</Html>
);
};
export default Welcome;

View File

@@ -1,5 +0,0 @@
export * from "./Common";
export * from "./Welcome";
export * from "./Verify";
export * from "./Reset";
export * from "./Login";

View File

@@ -0,0 +1,85 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
export function ForgotPasswordForm() {
const router = useRouter();
const [email, setEmail] = useState("");
const auth = useAuth();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
auth.authClient.forgetPassword(
{ email, redirectTo: `${window.location.origin}/auth/reset-password` },
{
onSuccess: () => {
toast.success("Email sent");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Recover password</CardTitle>
<CardDescription>
Enter your email to receive a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Recover password
</Button>
</div>
<div className="text-center text-sm">
Back to{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,199 +0,0 @@
import { Button } from "@/components/Button";
import { Loading } from "@/components/Loading";
import { useAuth } from "jazz-react-auth-betterauth";
import type { FullAuthClient } from "jazz-react-auth-betterauth";
import { useState } from "react";
const title = "Forgot Password";
export default function ForgotForm() {
const auth = useAuth();
const [email, setEmail] = useState<string>("");
const [otp, setOtp] = useState<string>("");
const [otpSentStatus, setOtpSentStatus] = useState<boolean>(false);
const [otpStatus, setOtpStatus] = useState<boolean>(false);
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const [status, setStatus] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
return (
<div className="min-h-screen flex flex-col justify-center">
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
<div>
<h1 className="text-stone-950 dark:text-white font-display text-5xl lg:text-6xl font-medium tracking-tighter mb-2">
{title}
</h1>
<p>
Enter your email address, and we'll send you a link to reset your
password.
</p>
</div>
{status && !otpStatus && (
<div>
Instructions to reset your password have been sent to {email}, if an
account with that email address exists.
</div>
)}
{otpStatus && (
<div>
Your password has been successfully reset. You may now log in.
</div>
)}
{error && <div>{error.message}</div>}
{loading && <Loading />}
<form className="flex flex-col gap-6">
<div>
<label htmlFor="email-address">Email address</label>
<input
id="email-address"
value={email}
disabled={loading}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<Button
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await auth.authClient.forgetPassword({
email: email,
redirectTo: `${window.location.origin}/reset`,
});
setStatus(data?.status ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
disabled={loading}
>
Send recovery link
</Button>
<Button
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await (
auth.authClient as FullAuthClient
).emailOtp.sendVerificationOtp({
email: email,
type: "forget-password",
});
setStatus(data?.success ?? false);
setOtpSentStatus(data?.success ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
disabled={loading}
>
Send recovery one-time password
</Button>
</form>
{otpSentStatus && (
<form
className="flex flex-col gap-6"
onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
if (password !== confirmPassword) {
setError(new Error("Passwords do not match"));
setLoading(false);
return;
}
const { data, error } = await (
auth.authClient as FullAuthClient
).emailOtp.resetPassword({
email: email,
otp: otp,
password: password,
});
setStatus(data?.success ?? false);
setOtpStatus(data?.success ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<div>
<label htmlFor="otp">One-time password</label>
<input
id="otp"
value={otp}
disabled={loading}
onChange={(e) => setOtp(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
disabled={loading}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label htmlFor="confirm-password">Confirm password</label>
<input
id="confirm-password"
type="password"
value={confirmPassword}
disabled={loading}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type={"submit"} disabled={loading}>
Submit
</Button>
</form>
)}
<Button href="/sign-in" disabled={loading}>
Sign in
</Button>
</div>
</div>
);
}

View File

@@ -1,115 +0,0 @@
import { Button } from "@/components/Button";
import { Loading } from "@/components/Loading";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useState } from "react";
const title = "Reset Password";
export default function ResetForm() {
const auth = useAuth();
const searchParams = new URLSearchParams(window.location.search);
const token = searchParams.get("token");
const initialError = searchParams.get("error");
const [error, setError] = useState<Error | undefined>(
initialError
? {
name: initialError,
message: initialError,
}
: undefined,
);
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const [status, setStatus] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
return (
<div className="min-h-screen flex flex-col justify-center">
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
<div>
<h1 className="text-stone-950 dark:text-white font-display text-5xl lg:text-6xl font-medium tracking-tighter mb-2">
{title}
</h1>
<p>Enter your new password.</p>
</div>
{status && (
<div>
Your password has been reset. You may now{" "}
<Link href="/sign-in">sign in</Link>.
</div>
)}
{error && <div>{error.message}</div>}
{loading && <Loading />}
<form
className="flex flex-col gap-6"
onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
if (password !== confirmPassword) {
setError(new Error("Passwords do not match"));
setLoading(false);
return;
}
if (!token) {
setError(new Error("No password reset token provided"));
setLoading(false);
return;
}
const { data, error } = await auth.authClient.resetPassword({
newPassword: password,
token,
});
setStatus(data?.status ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
disabled={loading}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label htmlFor="confirm-password">Confirm password</label>
<input
id="confirm-password"
type="password"
value={confirmPassword}
disabled={loading}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type="submit" disabled={loading}>
Reset password
</Button>
</form>
<Button href="/sign-in" disabled={loading}>
Sign in
</Button>
</div>
</div>
);
}

View File

@@ -1,222 +0,0 @@
import { AccountProviders } from "@/components/AccountProviders";
import { Button } from "@/components/Button";
import { DeleteAccountButton } from "@/components/DeleteAccountButton";
import { Loading } from "@/components/Loading";
import { SSOButton } from "@/components/SSOButton";
import { useAccount, useIsAuthenticated } from "jazz-react";
import { useAuth } from "jazz-react-auth-betterauth";
import type {
AccountsType,
FullAuthClient,
SSOProviderType,
} from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
const title = "Settings";
export default function SettingsForm({
providers,
}: {
providers?: SSOProviderType[];
}) {
const router = useRouter();
const { authClient, account, state } = useAuth();
const hasCredentials = state !== "anonymous";
const [accounts, setAccounts] = useState<AccountsType | undefined>(undefined);
useEffect(() => {
return authClient.useSession.subscribe(() => {
authClient.listAccounts().then((x) => setAccounts(x));
});
}, [authClient]);
const [status, setStatus] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [otpSentStatus, setOtpSentStatus] = useState<boolean>(false);
const [otpStatus, setOtpStatus] = useState<boolean>(false);
const [otp, setOtp] = useState<string>("");
const { me, logOut } = useAccount({ resolve: { profile: true } });
const isAuthenticated = useIsAuthenticated();
const signOut = useCallback(() => {
authClient
.signOut()
.catch(console.error)
.finally(() => {
logOut();
router.push("/");
});
}, [logOut, authClient]);
return (
<>
<header className="absolute p-4 top-0 left-0 w-full z-10 flex items-center justify-between gap-4">
<div className="float-start">
{me && hasCredentials && account && isAuthenticated && (
<Button className="float-start" onClick={signOut}>
Sign out
</Button>
)}
</div>
</header>
<div className="min-h-screen flex flex-col justify-center font-[family-name:var(--font-geist-sans)]">
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
<h1 className="text-stone-950 dark:text-white font-display text-5xl lg:text-6xl font-medium tracking-tighter mb-2">
{title}
</h1>
{status && account && !account?.emailVerified && (
<div>
Instructions to verify your account have been sent to{" "}
{account.email}, if an account with that email address exists.
</div>
)}
{(status || otpStatus) && account && account.emailVerified && (
<div>Your account has been successfully verified.</div>
)}
{error && <div>{error.message}</div>}
{loading && <Loading />}
<AccountProviders
accounts={accounts}
setLoading={setLoading}
setError={setError}
/>
{accounts?.data &&
providers?.map((x) => {
return (
accounts.data.find((y) => y.provider === x) === undefined && (
<SSOButton
link={true}
provider={x}
setLoading={setLoading}
setError={setError}
/>
)
);
})}
{account && account.emailVerified && <p>Account verified.</p>}
{account && !account.emailVerified && (
<>
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } =
await authClient.sendVerificationEmail({
email: account.email,
callbackURL: `${window.location.origin}`,
});
setStatus(data?.status ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
Send verification link
</Button>
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await (
authClient as FullAuthClient
).emailOtp.sendVerificationOtp({
email: account.email,
type: "email-verification",
});
setStatus(data?.success ?? false);
setOtpSentStatus(data?.success ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
Send verification one-time password
</Button>
</>
)}
{otpSentStatus && account && !account.emailVerified && (
<form
className="flex flex-col gap-6"
onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await (
authClient as FullAuthClient
).emailOtp.verifyEmail({
email: account.email,
otp: otp,
});
setOtpStatus(data?.status ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<div>
<label htmlFor="otp">One-time password</label>
<input
id="otp"
value={otp}
disabled={loading}
onChange={(e) => setOtp(e.target.value)}
/>
</div>
<Button type={"submit"} disabled={loading}>
Submit
</Button>
</form>
)}
<DeleteAccountButton
setLoading={setLoading}
setError={setError}
callbackURL={`${window.location.origin}/delete-account`}
/>
</div>
</div>
</>
);
}

View File

@@ -1,245 +0,0 @@
import { Button } from "@/components/Button";
import { Loading } from "@/components/Loading";
import { SSOButton } from "@/components/SSOButton";
import { useAuth } from "jazz-react-auth-betterauth";
import type {
FullAuthClient,
SSOProviderType,
} from "jazz-react-auth-betterauth";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
const title = "Sign In";
export default function SignInForm({
providers,
}: {
providers?: SSOProviderType[];
}) {
const router = useRouter();
const auth = useAuth();
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [rememberMe, setRememberMe] = useState(true);
const [otp, setOtp] = useState<string>("");
const [otpStatus, setOtpStatus] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
return (
<div className="min-h-screen flex flex-col justify-center">
<h1 className="sr-only">{title}</h1>
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
{otpStatus && (
<div>A one-time password has been sent to your email.</div>
)}
{error && <div>{error.message}</div>}
{loading && <Loading />}
<form
className="flex flex-col gap-6"
onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
if (!otpStatus) {
await auth.authClient.signIn.email(
{
email,
password,
rememberMe,
},
{
onSuccess: async () => {
await auth.logIn();
router.push("/");
},
onError: (error) => {
setError(error.error);
},
},
);
} else {
const { data, error } = await (
auth.authClient as FullAuthClient
).signIn.emailOtp({
email: email,
otp: otp,
});
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
if (data) {
await auth.logIn();
router.push("/");
}
}
setLoading(false);
}}
>
<div>
<label htmlFor="email-address">Email address</label>
<input
id="email-address"
value={email}
disabled={loading}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{!otpStatus && (
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
disabled={loading}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
)}
{otpStatus && (
<div>
<label htmlFor="otp">One-time password</label>
<input
id="otp"
value={otp}
disabled={loading}
onChange={(e) => setOtp(e.target.value)}
/>
</div>
)}
<div className="items-center">
<div>
<label htmlFor="remember-me">Remember me</label>
<input
id="remember-me"
type="checkbox"
checked={rememberMe}
disabled={loading}
onChange={(e) => setRememberMe(e.target.checked)}
/>
</div>
<Link href="/forgot" className="text-sm float-right">
Forgot password?
</Link>
</div>
<Button type="submit" disabled={loading}>
Sign in
</Button>
</form>
<div className="flex items-center gap-4">
<hr className="flex-1" />
<p className="text-center">or</p>
<hr className="flex-1" />
</div>
<div className="flex flex-col gap-4">
{providers?.map((x) => {
return (
<SSOButton
callbackURL={`${window.location.origin}/social/logIn`}
provider={x}
setLoading={setLoading}
setError={setError}
/>
);
})}
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await (
auth.authClient as FullAuthClient
).signIn.magicLink({
email: email,
callbackURL: `${window.location.origin}/magic-link/logIn`,
});
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<Image
src="/link.svg"
alt="Link icon"
className="absolute left-3"
width={16}
height={16}
/>
Sign in with magic link
</Button>
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await (
auth.authClient as FullAuthClient
).emailOtp.sendVerificationOtp({
email: email,
type: "sign-in",
});
setOtpStatus(data?.success ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<Image
src="/mail.svg"
alt="Mail icon"
className="absolute left-3"
width={16}
height={16}
/>
Sign in with one-time password
</Button>
</div>
<p className="text-sm">
Don't have an account? <Link href="/sign-up">Sign up</Link>
</p>
</div>
</div>
);
}

View File

@@ -1,257 +0,0 @@
import { Button } from "@/components/Button";
import { Loading } from "@/components/Loading";
import { SSOButton } from "@/components/SSOButton";
import { useAuth } from "jazz-react-auth-betterauth";
import type {
FullAuthClient,
SSOProviderType,
} from "jazz-react-auth-betterauth";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
const title = "Sign Up";
export default function SignUpForm({
providers,
}: {
providers?: SSOProviderType[];
}) {
const router = useRouter();
const auth = useAuth();
const [name, setName] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const [otp, setOtp] = useState<string>("");
const [otpStatus, setOtpStatus] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
return (
<div className="min-h-screen flex flex-col justify-center">
<h1 className="sr-only">{title}</h1>
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
{otpStatus && (
<div>A one-time password has been sent to your email.</div>
)}
{error && <div>{error.message}</div>}
{loading && <Loading />}
<form
className="flex flex-col gap-6"
onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
if (password !== confirmPassword) {
setError(new Error("Passwords do not match"));
setLoading(false);
return;
}
if (!otpStatus) {
await auth.authClient.signUp.email(
{
email,
password,
name,
},
{
onSuccess: async () => {
await auth.signIn();
router.push("/");
},
onError: (error) => {
setError(error.error);
},
},
);
} else {
const { data, error } = await (
auth.authClient as FullAuthClient
).signIn.emailOtp({
email: email,
otp: otp,
});
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
if (data) {
await auth.signIn();
router.push("/");
}
}
setLoading(false);
}}
>
<div>
<label htmlFor="full-name">Full name</label>
<input
id="full-name"
value={name}
disabled={loading}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email-address">Email address</label>
<input
id="email-address"
value={email}
disabled={loading}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{!otpStatus && (
<>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
disabled={loading}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label htmlFor="confirm-password">Confirm password</label>
<input
id="confirm-password"
type="password"
value={confirmPassword}
disabled={loading}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
</>
)}
{otpStatus && (
<div>
<label htmlFor="otp">One-time password</label>
<input
id="otp"
value={otp}
disabled={loading}
onChange={(e) => setOtp(e.target.value)}
/>
</div>
)}
<Button type="submit" disabled={loading}>
Sign up
</Button>
</form>
<div className="flex items-center gap-4">
<hr className="flex-1" />
<p className="text-center">or</p>
<hr className="flex-1" />
</div>
<div className="flex flex-col gap-4">
{providers?.map((x) => {
return (
<SSOButton
callbackURL={`${window.location.origin}/social/signIn`}
provider={x}
setLoading={setLoading}
setError={setError}
/>
);
})}
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { error } = await (
auth.authClient as FullAuthClient
).signIn.magicLink({
email: email,
callbackURL: `${window.location.origin}/magic-link/signIn`,
});
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<Image
src="/link.svg"
alt="Link icon"
className="absolute left-3"
width={16}
height={16}
/>
Sign up with magic link
</Button>
<Button
variant="secondary"
className="relative"
onClick={async (e) => {
e.preventDefault();
setLoading(true);
const { data, error } = await (
auth.authClient as FullAuthClient
).emailOtp.sendVerificationOtp({
email: email,
type: "sign-in",
});
setOtpStatus(data?.success ?? false);
const errorMessage = error?.message ?? error?.statusText;
setError(
error
? {
...error,
name: error.statusText,
message:
errorMessage && errorMessage.length > 0
? errorMessage
: "An error occurred",
}
: undefined,
);
setLoading(false);
}}
>
<Image
src="/mail.svg"
alt="Mail icon"
className="absolute left-3"
width={16}
height={16}
/>
Sign up with one-time password
</Button>
</div>
<p className="text-sm">
Already have an account? <Link href="/sign-in">Sign in</Link>
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
"use client";
import { Button } from "@/components/ui/button";
import { useAccount, useIsAuthenticated } from "jazz-react";
import { useAuth } from "jazz-react-auth-betterauth";
import { Account } from "jazz-tools";
import Image from "next/image";
import Link from "next/link";
import { useCallback } from "react";
export function Navbar() {
const { authClient } = useAuth();
const { logOut } = useAccount(Account, { resolve: { profile: {} } });
const isAuthenticated = useIsAuthenticated();
const signOut = useCallback(() => {
authClient.signOut().catch(console.error).finally(logOut);
}, [logOut, authClient]);
return (
<header className="absolute p-4 top-0 left-0 w-full z-10 flex justify-between">
<nav className="flex gap-4">
<Link href="/">
<Image
src="/jazz-logo.svg"
alt="Jazz logo"
width={96}
height={96}
priority
/>
</Link>
{isAuthenticated && (
<Button asChild variant="link">
<Link href="/settings">Settings</Link>
</Button>
)}
</nav>
<nav className="flex gap-4">
{isAuthenticated ? (
<Button onClick={signOut}>Sign out</Button>
) : (
<>
<Button asChild variant="secondary">
<Link href="/auth/sign-in">Sign in</Link>
</Button>
<Button asChild>
<Link href="/auth/sign-up">Sign up</Link>
</Button>
</>
)}
</nav>
</header>
);
}

View File

@@ -0,0 +1,108 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
export function ResetPasswordForm() {
const router = useRouter();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const auth = useAuth();
const searchParams = useSearchParams();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const token = searchParams.get("token");
if (password !== confirmPassword) {
toast.error("Passwords do not match");
return;
}
if (!token) {
toast.error("Invalid token");
return;
}
auth.authClient.resetPassword(
{ newPassword: password, token },
{
onSuccess: () => {
toast.success("Password successfully updated");
router.push("/auth/sign-in");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Reset password</CardTitle>
<CardDescription>Enter your new password</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Set password
</Button>
</div>
<div className="text-center text-sm">
Back to{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,21 +0,0 @@
import { useAuth } from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const auth = useAuth();
const searchParams = new URLSearchParams(window.location.search);
const error = searchParams.get("error");
if (!error) {
auth.logIn().then(() => router.push("/"));
return null;
} else {
return (
<div className="min-h-screen flex flex-col justify-center">
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
<div>{error}</div>
</div>
</div>
);
}
}

View File

@@ -1,21 +0,0 @@
import { useAuth } from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const auth = useAuth();
const searchParams = new URLSearchParams(window.location.search);
const error = searchParams.get("error");
if (!error) {
auth.signIn().then(() => router.push("/"));
return null;
} else {
return (
<div className="min-h-screen flex flex-col justify-center">
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
<div>{error}</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,124 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { SSOButton } from "./SSOButton";
interface Props {
providers: SSOProviderType[];
}
export function SigninForm({ providers }: Props) {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const auth = useAuth();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await auth.authClient.signIn.email(
{ email, password },
{
onSuccess: async () => {
await auth.logIn();
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>
Sign in with one of the following providers
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
{providers.length > 0 && (
<>
<div className="flex flex-col gap-4">
{providers?.map((provider) => (
<SSOButton key={provider} provider={provider} />
))}
</div>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
</>
)}
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-3">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Sign in
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<Link
href="/auth/sign-up"
className="underline underline-offset-4"
>
Sign up
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,151 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { SSOButton } from "./SSOButton";
interface Props {
providers: SSOProviderType[];
}
export function SignupForm({ providers }: Props) {
const router = useRouter();
const auth = useAuth();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (password !== confirmPassword) {
toast.error("Error", {
description: "Passwords do not match",
});
return;
}
await auth.authClient.signUp.email(
{
email,
password,
name,
},
{
onSuccess: async () => {
await auth.signIn();
router.push("/");
},
onError: (error) => {
toast.error("Sign up error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome</CardTitle>
<CardDescription>
Sign up with one of the following providers
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
{providers.length > 0 && (
<>
<div className="flex flex-col gap-4">
{providers?.map((provider) => (
<SSOButton key={provider} provider={provider} />
))}
</div>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
</>
)}
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="********"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Sign up
</Button>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,22 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,22 @@
"use client";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme="light"
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -0,0 +1,60 @@
"use client";
import { Button } from "@/components/ui/button";
import { useAccount, useIsAuthenticated } from "jazz-react";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
export function UserSettings() {
const router = useRouter();
const { authClient } = useAuth();
const { logOut } = useAccount();
const isAuthenticated = useIsAuthenticated();
if (!isAuthenticated) {
return (
<div className="flex flex-col gap-8">
<h1 className="text-center text-2xl">Forbidden</h1>
<p className="text-center text-sm text-muted-foreground">
Please{" "}
<Link href="/auth/sign-in" className="underline text-primary">
sign in
</Link>{" "}
to access this page.
</p>
</div>
);
}
return (
<>
<div className="flex flex-col gap-8">
<h1 className="text-4xl font-semibold">Settings</h1>
<Button
variant="destructive"
className="relative"
type="button"
onClick={async (e) => {
e.preventDefault();
authClient.deleteUser(undefined, {
onSuccess: () => {
logOut();
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
});
}}
>
Delete account
</Button>
</div>
</>
);
}

View File

@@ -1,63 +0,0 @@
import type { betterAuth } from "better-auth";
import type { emailOTP } from "better-auth/plugins";
import type { magicLink } from "better-auth/plugins";
export const sendEmailOtpCb: Parameters<
typeof emailOTP
>[0]["sendVerificationOTP"] = async ({ email, otp, type }) => {
const searchParams = new URLSearchParams();
searchParams.set("email", email);
searchParams.set("otp", otp);
let emailLink = "";
if (type === "sign-in") {
// Send the OTP for sign-in
emailLink = `/mockEmail/sign-in?${searchParams.toString()}`;
} else if (type === "email-verification") {
// Send the OTP for email verification
emailLink = `/mockEmail/email-verification?${searchParams.toString()}`;
} else {
// Send the OTP for password reset
emailLink = `/mockEmail/forget-password?${searchParams.toString()}`;
}
console.log("Mock email sent: " + emailLink);
};
export const sendMagicLinkCb: Parameters<typeof magicLink>[0]["sendMagicLink"] =
async ({ email, url }) => {
const searchParams = new URLSearchParams();
searchParams.set("url", url);
const emailLink = `/mockEmail/sign-in?${searchParams.toString()}`;
console.log("Mock email sent: " + emailLink);
};
export const sendWelcomeEmailCb: NonNullable<
NonNullable<
NonNullable<Parameters<typeof betterAuth>[0]["databaseHooks"]>["user"]
>["create"]
>["after"] = async (user) => {
const searchParams = new URLSearchParams();
searchParams.set("name", user.name);
const emailLink = `/mockEmail/welcome?${searchParams.toString()}`;
console.log("Mock email sent: " + emailLink);
};
export const sendVerificationEmailCb: NonNullable<
Parameters<typeof betterAuth>[0]["emailVerification"]
>["sendVerificationEmail"] = async ({ user, url }) => {
const searchParams = new URLSearchParams();
searchParams.set("name", user.name);
searchParams.set("url", url);
const emailLink = `/mockEmail/email-verification?${searchParams.toString()}`;
console.log("Mock email sent: " + emailLink);
};
export const sendResetPasswordCb: NonNullable<
Parameters<typeof betterAuth>[0]["emailAndPassword"]
>["sendResetPassword"] = async ({ user, url }) => {
const searchParams = new URLSearchParams();
searchParams.set("name", user.name);
searchParams.set("url", url);
const emailLink = `/mockEmail/forgot-password?${searchParams.toString()}`;
console.log("Mock email sent: " + emailLink);
};

View File

@@ -1,54 +1,42 @@
import { appName } from "@/components/emails";
import { betterAuth } from "better-auth";
import { getMigrations } from "better-auth/db";
import { emailOTP, haveIBeenPwned, magicLink } from "better-auth/plugins";
import Database from "better-sqlite3";
import { jazzPlugin } from "jazz-betterauth-server-plugin";
import {
sendEmailOtpCb,
sendMagicLinkCb,
sendResetPasswordCb,
sendVerificationEmailCb,
sendWelcomeEmailCb,
} from "./auth-email";
import { socialProviders } from "./socialProviders";
export const auth = await (async () => {
// Configure Better Auth server
const auth = betterAuth({
appName: appName,
appName: "Jazz Example: Better Auth",
database: new Database("sqlite.db"),
emailAndPassword: {
enabled: true,
sendResetPassword: sendResetPasswordCb,
},
emailVerification: {
sendVerificationEmail: sendVerificationEmailCb,
},
socialProviders: socialProviders,
account: {
accountLinking: {
allowDifferentEmails: true,
allowUnlinkingAll: true,
async sendResetPassword({ url }) {
// Here we can send an email to the user with the reset password link
console.log("****** RESET PASSWORD ******");
console.log("navigate to", url, "to reset your password");
console.log("******");
},
},
emailVerification: {
async sendVerificationEmail(data) {
console.error("Not implemented");
},
},
socialProviders,
user: {
deleteUser: {
enabled: true,
},
},
plugins: [
haveIBeenPwned(),
jazzPlugin(),
magicLink({
sendMagicLink: sendMagicLinkCb,
}),
emailOTP({ sendVerificationOTP: sendEmailOtpCb }),
],
plugins: [jazzPlugin()],
databaseHooks: {
user: {
create: {
after: sendWelcomeEmailCb,
async after(user) {
// Here we can send a welcome email to the user
console.error("Not implemented");
},
},
},
},

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -11,14 +11,14 @@ test("should sign up, sign in, and logout", async ({ page }) => {
await page.goto("/");
const homePage = new HomePage(page);
await homePage.expectLoggedOut();
await homePage.signUpLinkButton.click();
await homePage.signUpLink.click();
await homePage.signUpEmail(username, email, password);
await homePage.expectLoggedIn(username);
// Log out & sign in
await homePage.logout();
await homePage.expectLoggedOut();
await homePage.signInLinkButton.click();
await homePage.signInLink.click();
await homePage.signInEmail(email, password);
await homePage.expectLoggedIn(username);

View File

@@ -4,11 +4,11 @@ export class HomePage {
constructor(public page: Page) {}
usernameInput = this.page.getByRole("textbox", {
name: "Full name",
name: "Name",
exact: true,
});
emailInput = this.page.getByRole("textbox", {
name: "Email address",
name: "Email",
exact: true,
});
passwordInput = this.page.getByRole("textbox", {
@@ -27,11 +27,11 @@ export class HomePage {
name: "Sign in",
exact: true,
});
signUpLinkButton = this.page.getByRole("link", {
signUpLink = this.page.getByRole("link", {
name: "Sign up",
exact: true,
});
signInLinkButton = this.page.getByRole("link", {
signInLink = this.page.getByRole("link", {
name: "Sign in",
exact: true,
});
@@ -60,17 +60,17 @@ export class HomePage {
async expectLoggedIn(name?: string) {
await expect(this.logoutButton).toBeVisible();
await expect(this.signInLinkButton).not.toBeVisible();
await expect(this.signUpLinkButton).not.toBeVisible();
await expect(this.signInLink).not.toBeVisible();
await expect(this.signUpLink).not.toBeVisible();
if (name) {
await expect(this.page.getByText(`Signed in as ${name}.`)).toBeVisible();
await expect(this.page.getByText(`Signed in as ${name}`)).toBeVisible();
}
}
async expectLoggedOut() {
await expect(this.logoutButton).not.toBeVisible();
await expect(this.signInLinkButton).toBeVisible();
await expect(this.signUpLinkButton).toBeVisible();
await expect(this.page.getByText(`Not signed in.`)).toBeVisible();
await expect(this.signInLink).toBeVisible();
await expect(this.signUpLink).toBeVisible();
await expect(this.page.getByText(`Anonymous user`)).toBeVisible();
}
}

View File

@@ -1,5 +1,29 @@
# chat-rn-expo-clerk
## 1.0.124
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-expo@0.14.0
- jazz-react-native-media-images@0.14.0
## 1.0.123
### Patch Changes
- jazz-expo@0.13.32
## 1.0.122
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-expo@0.13.31
- jazz-react-native-media-images@0.13.31
## 1.0.121
### Patch Changes

View File

@@ -7,7 +7,7 @@ import { useLocalSearchParams } from "expo-router";
import { useAccount, useCoState } from "jazz-expo";
import { ProgressiveImg } from "jazz-expo";
import { createImage } from "jazz-react-native-media-images";
import { CoPlainText, Group, ID } from "jazz-tools";
import { CoPlainText, Group, Loaded } from "jazz-tools";
import { useEffect, useLayoutEffect, useState } from "react";
import React, {
SafeAreaView,
@@ -26,7 +26,7 @@ import React, {
export default function Conversation() {
const { chatId } = useLocalSearchParams();
const { me } = useAccount();
const [chat, setChat] = useState<Chat>();
const [chat, setChat] = useState<Loaded<typeof Chat>>();
const [message, setMessage] = useState("");
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
const navigation = useNavigation();
@@ -37,7 +37,7 @@ export default function Conversation() {
if (chatId === "new") {
createChat();
} else {
loadChat(chatId as ID<Chat>);
loadChat(chatId as string);
}
}, [chat]);
@@ -69,7 +69,7 @@ export default function Conversation() {
setChat(chat);
};
const loadChat = async (chatId: ID<Chat>) => {
const loadChat = async (chatId: string) => {
try {
const chat = await Chat.load(chatId);
if (chat) setChat(chat);
@@ -123,8 +123,8 @@ export default function Conversation() {
}
};
const renderMessageItem = ({ item }: { item: Message }) => {
const isMe = item._edits.text.by?.isMe;
const renderMessageItem = ({ item }: { item: Loaded<typeof Message> }) => {
const isMe = item._edits.text?.by?.isMe;
return (
<View
className={clsx(
@@ -139,7 +139,7 @@ export default function Conversation() {
isMe ? "text-right" : "text-left",
)}
>
{item._edits.text.by?.profile?.name}
{item._edits.text?.by?.profile?.name}
</Text>
) : null}
<View
@@ -175,8 +175,8 @@ export default function Conversation() {
!isMe ? "mt-2 text-gray-500" : "mt-1 text-gray-200",
)}
>
{item._edits.text.madeAt?.getHours().toString().padStart(2, "0")}:
{item._edits.text.madeAt?.getMinutes().toString().padStart(2, "0")}
{item._edits.text?.madeAt?.getHours().toString().padStart(2, "0")}:
{item._edits.text?.madeAt?.getMinutes().toString().padStart(2, "0")}
</Text>
</View>
</View>

View File

@@ -32,7 +32,7 @@ export default function ChatScreen() {
});
}, [navigation]);
const loadChat = async (chatId: ID<Chat> | "new") => {
const loadChat = async (chatId: string | "new") => {
router.navigate(`/chat/${chatId}`);
};
@@ -49,7 +49,7 @@ export default function ChatScreen() {
text: "Join",
onPress: (chatId) => {
if (chatId) {
loadChat(chatId as ID<Chat>);
loadChat(chatId);
} else {
Alert.alert("Error", "Chat ID cannot be empty.");
}

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-expo-clerk",
"main": "index.js",
"version": "1.0.121",
"version": "1.0.124",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",
@@ -18,50 +18,50 @@
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@clerk/clerk-expo": "^2.2.21",
"@craftzdog/react-native-buffer": "6.0.5",
"@expo/vector-icons": "^14.0.2",
"@expo/vector-icons": "^14.1.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/native": "7.0.19",
"@react-navigation/native-stack": "7.2.1",
"clsx": "^2.0.0",
"expo": "^52.0.42",
"expo-build-properties": "~0.13.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.8",
"expo-crypto": "~14.0.2",
"expo-dev-client": "~5.0.16",
"expo-file-system": "^18.0.4",
"expo-font": "~13.0.1",
"expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.19",
"expo-secure-store": "~14.0.0",
"expo-splash-screen": "~0.29.22",
"expo-sqlite": "15.1.3",
"expo-status-bar": "~2.0.1",
"expo-web-browser": "~14.0.1",
"expo": "^53.0.8",
"expo-build-properties": "~0.14.6",
"expo-clipboard": "~7.1.4",
"expo-constants": "~17.1.6",
"expo-crypto": "~14.1.4",
"expo-dev-client": "~5.1.8",
"expo-file-system": "^18.1.9",
"expo-font": "~13.3.1",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.6",
"expo-secure-store": "~14.2.3",
"expo-splash-screen": "~0.30.8",
"expo-sqlite": "15.2.9",
"expo-status-bar": "~2.2.3",
"expo-web-browser": "~14.1.6",
"jazz-expo": "workspace:*",
"jazz-react-native-media-images": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^4.1.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native-gesture-handler": "~2.20.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
"react-native-gesture-handler": "~2.24.0",
"react-native-get-random-values": "^1.11.0",
"react-native-reanimated": "~3.16.3",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.4.0",
"react-native-reanimated": "~3.17.5",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.10.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.19.13",
"react-native-web": "~0.20.0",
"readable-stream": "4.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~18.3.12",
"@types/react": "~19.0.14",
"@types/react-test-renderer": "^19.0.0",
"react-test-renderer": "18.3.1",
"tailwindcss": "^3.4.17",
"typescript": "5.6.2"
"typescript": "5.8.3"
},
"private": true
}

View File

@@ -1,8 +1,8 @@
import { CoList, CoMap, CoPlainText, ImageDefinition, co } from "jazz-tools";
import { CoList, co, coField, z } from "jazz-tools";
export class Message extends CoMap {
text = co.ref(CoPlainText);
image = co.optional.ref(ImageDefinition);
}
export const Message = co.map({
text: co.plainText(),
image: z.optional(co.image()),
});
export class Chat extends CoList.Of(co.ref(Message)) {}
export const Chat = co.list(Message);

View File

@@ -1,5 +1,27 @@
# chat-rn-expo
## 1.0.111
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-expo@0.14.0
## 1.0.110
### Patch Changes
- jazz-expo@0.13.32
## 1.0.109
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-expo@0.13.31
## 1.0.108
### Patch Changes

View File

@@ -23,7 +23,12 @@
},
"package": "com.jazz.chatrn"
},
"plugins": ["expo-secure-store", "expo-sqlite", "expo-build-properties"],
"plugins": [
"expo-secure-store",
"expo-sqlite",
"expo-build-properties",
"expo-web-browser"
],
"extra": {
"eas": {
"projectId": "e0e61872-1906-4c84-b9d8-9be77355cad0"

View File

@@ -1,9 +1,9 @@
{
"name": "chat-rn-expo",
"version": "1.0.108",
"version": "1.0.111",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
"build": "tsc --noEmit && expo export -p ios",
"start": "expo start",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
@@ -19,35 +19,35 @@
"@react-navigation/native": "7.0.19",
"@react-navigation/native-stack": "7.2.1",
"clsx": "^2.0.0",
"expo": "52.0.42",
"expo-build-properties": "~0.13.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.8",
"expo-dev-client": "~5.0.16",
"expo-linking": "~7.0.5",
"expo-secure-store": "~14.0.0",
"expo-sqlite": "15.1.3",
"expo-status-bar": "~2.0.1",
"expo-web-browser": "~14.0.1",
"expo": "53.0.8",
"expo-build-properties": "~0.14.6",
"expo-clipboard": "~7.1.4",
"expo-constants": "~17.1.6",
"expo-dev-client": "~5.1.8",
"expo-linking": "~7.1.4",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "15.2.9",
"expo-status-bar": "~2.2.3",
"expo-web-browser": "~14.1.6",
"jazz-expo": "workspace:*",
"jazz-tools": "workspace:*",
"nativewind": "^4.1.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
"react-native-get-random-values": "^1.11.0",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.10.0",
"react-native-nitro-modules": "0.25.2",
"react-native-quick-crypto": "1.0.0-beta.15",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.4.0",
"react-native-url-polyfill": "^2.0.0",
"readable-stream": "4.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~18.3.12",
"@types/react": "~19.0.14",
"tailwindcss": "^3.4.17",
"typescript": "5.6.2"
"typescript": "5.8.3"
},
"private": true
}

View File

@@ -1,6 +1,6 @@
import clsx from "clsx";
import * as Clipboard from "expo-clipboard";
import { Group, ID, Profile } from "jazz-tools";
import { Group, ID, Loaded, Profile } from "jazz-tools";
import { useEffect, useState } from "react";
import React, {
Button,
@@ -19,7 +19,7 @@ import { Chat, Message } from "./schema";
export default function ChatScreen({ navigation }: { navigation: any }) {
const { me, logOut } = useAccount();
const [chatId, setChatId] = useState<ID<Chat>>();
const [chatId, setChatId] = useState<string>();
const [chatIdInput, setChatIdInput] = useState<string>();
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
const [message, setMessage] = useState("");
@@ -59,7 +59,7 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
const joinChat = () => {
if (chatIdInput) {
setChatId(chatIdInput as ID<Chat>);
setChatId(chatIdInput);
} else {
Alert.alert("Error", "Chat ID cannot be empty.");
}
@@ -75,7 +75,7 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
}
};
const renderMessageItem = ({ item }: { item: Message }) => {
const renderMessageItem = ({ item }: { item: Loaded<typeof Message> }) => {
const isMe = item._edits?.text?.by?.isMe;
return (
<View
@@ -156,7 +156,7 @@ export default function ChatScreen({ navigation }: { navigation: any }) {
textAlignVertical="center"
onSubmitEditing={() => {
if (chatIdInput) {
setChatId(chatIdInput as ID<Chat>);
setChatId(chatIdInput);
}
}}
testID="chat-id-input"

View File

@@ -1,7 +1,7 @@
import { CoList, CoMap, co } from "jazz-tools";
import { co, z } from "jazz-tools";
export class Message extends CoMap {
text = co.string;
}
export const Message = co.map({
text: z.string(),
});
export class Chat extends CoList.Of(co.ref(Message)) {}
export const Chat = co.list(Message);

View File

@@ -1,5 +1,34 @@
# chat-rn
## 1.0.119
### Patch Changes
- Updated dependencies [5835ed1]
- Updated dependencies [5835ed1]
- cojson@0.14.0
- jazz-tools@0.14.0
- cojson-transport-ws@0.14.0
- jazz-react-native@0.14.0
## 1.0.118
### Patch Changes
- jazz-react-native@0.13.32
## 1.0.117
### Patch Changes
- Updated dependencies [e5b170f]
- Updated dependencies [d63716a]
- Updated dependencies [d5edad7]
- jazz-tools@0.13.31
- cojson@0.13.31
- jazz-react-native@0.13.31
- cojson-transport-ws@0.13.31
## 1.0.116
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.116",
"version": "1.0.119",
"main": "index.js",
"scripts": {
"android": "react-native run-android",
@@ -24,7 +24,7 @@
"jazz-react-native": "workspace:*",
"jazz-tools": "workspace:*",
"react": "18.3.1",
"react-native": "0.76.7",
"react-native": "0.79.2",
"react-native-get-random-values": "^1.11.0",
"react-native-mmkv": "^3.2.0",
"react-native-polyfill-globals": "^3.1.0",

View File

@@ -1,6 +1,6 @@
import Clipboard from "@react-native-clipboard/clipboard";
import { useAccount, useCoState } from "jazz-react-native";
import { CoPlainText, Group, ID, Profile } from "jazz-tools";
import { CoPlainText, Group, ID, Loaded, Profile } from "jazz-tools";
import { useEffect, useState } from "react";
import {
Alert,
@@ -18,7 +18,7 @@ import { Chat, Message } from "./schema";
export function ChatScreen({ navigation }: { navigation: any }) {
const { me, logOut } = useAccount();
const [chatId, setChatId] = useState<ID<Chat>>();
const [chatId, setChatId] = useState<string>();
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
const [message, setMessage] = useState("");
const profile = useCoState(Profile, me._refs.profile?.id, {});
@@ -68,7 +68,7 @@ export function ChatScreen({ navigation }: { navigation: any }) {
text: "Join",
onPress: (chatId) => {
if (chatId) {
setChatId(chatId as ID<Chat>);
setChatId(chatId);
} else {
Alert.alert("Error", "Chat ID cannot be empty.");
}
@@ -92,7 +92,9 @@ export function ChatScreen({ navigation }: { navigation: any }) {
}
};
const renderMessageItem = ({ item }: { item: Message }) => {
const renderMessageItem = ({
item,
}: { item: Loaded<typeof Message, { text: true }> }) => {
const isMe = item._edits?.text?.by?.isMe;
return (
<View

View File

@@ -1,7 +1,7 @@
import { CoList, CoMap, CoPlainText, co } from "jazz-tools";
import { co } from "jazz-tools";
export class Message extends CoMap {
text = co.ref(CoPlainText);
}
export const Message = co.map({
text: co.plainText(),
});
export class Chat extends CoList.Of(co.ref(Message)) {}
export const Chat = co.list(Message);

View File

@@ -1,5 +1,30 @@
# chat-vue
## 0.0.102
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-browser@0.14.0
- jazz-vue@0.14.0
## 0.0.101
### Patch Changes
- jazz-browser@0.13.32
- jazz-vue@0.13.32
## 0.0.100
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-browser@0.13.31
- jazz-vue@0.13.31
## 0.0.99
### Patch Changes

View File

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

View File

@@ -1,7 +1,7 @@
import { CoList, CoMap, CoPlainText, co } from "jazz-tools";
import { CoList, CoMap, CoPlainText, coField } from "jazz-tools";
export class Message extends CoMap {
text = co.ref(CoPlainText);
text = coField.ref(CoPlainText);
}
export class Chat extends CoList.Of(co.ref(Message)) {}
export class Chat extends CoList.Of(coField.ref(Message)) {}

View File

@@ -1,5 +1,29 @@
# jazz-example-chat
## 0.0.200
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-inspector@0.14.0
- jazz-react@0.14.0
## 0.0.199
### Patch Changes
- jazz-react@0.13.32
## 0.0.198
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-inspector@0.13.31
- jazz-react@0.13.31
## 0.0.197
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.197",
"version": "0.0.200",
"type": "module",
"scripts": {
"dev": "vite",
@@ -20,7 +20,8 @@
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"zod": "4.0.0-beta.20250505T012514"
},
"devDependencies": {
"@playwright/test": "^1.50.1",

View File

@@ -3,7 +3,7 @@ import { getRandomUsername, inIframe, onChatLoad } from "@/util.ts";
import { useIframeHashRouter } from "hash-slash";
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, useAccount } from "jazz-react";
import { Group, ID } from "jazz-tools";
import { Group } from "jazz-tools";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ChatScreen } from "./chatScreen.tsx";
@@ -43,7 +43,7 @@ export function App() {
</TopBar>
{router.route({
"/": () => createChat() as never,
"/chat/:id": (id) => <ChatScreen chatID={id as ID<Chat>} />,
"/chat/:id": (id) => <ChatScreen chatID={id} />,
})}
</AppContainer>
);

View File

@@ -1,5 +1,5 @@
import { createImage, useAccount, useCoState } from "jazz-react";
import { Account, CoPlainText, ID } from "jazz-tools";
import { Account, Loaded, co } from "jazz-tools";
import { useState } from "react";
import { Chat, Message } from "./schema.ts";
import {
@@ -15,8 +15,10 @@ import {
TextInput,
} from "./ui.tsx";
export function ChatScreen(props: { chatID: ID<Chat> }) {
const chat = useCoState(Chat, props.chatID, { resolve: { $each: true } });
export function ChatScreen(props: { chatID: string }) {
const chat = useCoState(Chat, props.chatID, {
resolve: { $each: { text: true } },
});
const account = useAccount();
const [showNLastMessages, setShowNLastMessages] = useState(30);
@@ -39,7 +41,7 @@ export function ChatScreen(props: { chatID: ID<Chat> }) {
chat.push(
Message.create(
{
text: CoPlainText.create(file.name, chat._owner),
text: co.plainText().create(file.name, chat._owner),
image: image,
},
chat._owner,
@@ -76,7 +78,7 @@ export function ChatScreen(props: { chatID: ID<Chat> }) {
onSubmit={(text) => {
chat.push(
Message.create(
{ text: CoPlainText.create(text, chat._owner) },
{ text: co.plainText().create(text, chat._owner) },
chat._owner,
),
);
@@ -87,7 +89,10 @@ export function ChatScreen(props: { chatID: ID<Chat> }) {
);
}
function ChatBubble(props: { me: Account; msg: Message }) {
function ChatBubble(props: {
me: Account;
msg: Loaded<typeof Message, { text: true }>;
}) {
if (!props.me.canRead(props.msg) || !props.msg.text?.toString()) {
return (
<BubbleContainer fromMe={false}>
@@ -102,7 +107,7 @@ function ChatBubble(props: { me: Account; msg: Message }) {
}
const lastEdit = props.msg._edits.text;
const fromMe = lastEdit.by?.isMe;
const fromMe = lastEdit?.by?.isMe;
const { text, image } = props.msg;
return (
@@ -111,7 +116,9 @@ function ChatBubble(props: { me: Account; msg: Message }) {
{image && <BubbleImage image={image} />}
<BubbleText text={text} />
</BubbleBody>
<BubbleInfo by={lastEdit.by?.profile?.name} madeAt={lastEdit.madeAt} />
{lastEdit && (
<BubbleInfo by={lastEdit.by?.profile?.name} madeAt={lastEdit.madeAt} />
)}
</BubbleContainer>
);
}

View File

@@ -1,8 +1,8 @@
import { CoList, CoMap, CoPlainText, ImageDefinition, co } from "jazz-tools";
import { co, z } from "jazz-tools";
export class Message extends CoMap {
text = co.ref(CoPlainText);
image = co.optional.ref(ImageDefinition);
}
export const Message = co.map({
text: co.plainText(),
image: z.optional(co.image()),
});
export class Chat extends CoList.Of(co.ref(Message)) {}
export const Chat = co.list(Message);

View File

@@ -1,6 +1,6 @@
import clsx from "clsx";
import { ProgressiveImg } from "jazz-react";
import { CoPlainText, ImageDefinition } from "jazz-tools";
import { CoPlainText, ImageDefinition, Loaded } from "jazz-tools";
import { ImageIcon } from "lucide-react";
import { useId, useRef } from "react";
@@ -81,7 +81,7 @@ export function BubbleText(props: {
);
}
export function BubbleImage(props: { image: ImageDefinition }) {
export function BubbleImage(props: { image: Loaded<typeof ImageDefinition> }) {
return (
<ProgressiveImg image={props.image}>
{({ src }) => (

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