create-aron-app 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/templates/_base/.cursor/agents/skills/clerk/SKILL.md +89 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/SKILL.md +142 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +30 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +88 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +165 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +208 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +14 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/SKILL.md +157 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +224 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +190 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +314 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +259 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +125 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +94 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +50 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +56 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +68 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +56 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +104 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-webhooks/SKILL.md +131 -0
- package/templates/_base/.cursor/agents/skills/shadcn/SKILL.md +241 -0
- package/templates/_base/.cursor/agents/skills/shadcn/agents/openai.yml +5 -0
- package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn.png +0 -0
- package/templates/_base/.cursor/agents/skills/shadcn/cli.md +257 -0
- package/templates/_base/.cursor/agents/skills/shadcn/customization.md +202 -0
- package/templates/_base/.cursor/agents/skills/shadcn/evals/evals.json +47 -0
- package/templates/_base/.cursor/agents/skills/shadcn/mcp.md +94 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/composition.md +195 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/forms.md +192 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/icons.md +101 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/styling.md +162 -0
- package/templates/_base/.cursor/commands/builder.md +0 -0
- package/templates/_base/.cursor/commands/pr.md +7 -0
- package/templates/_base/.cursor/rules/api_architecture.mdc +268 -0
- package/templates/_base/.cursor/rules/coding_standards.mdc +64 -0
- package/templates/_base/.cursor/rules/convex_rules.mdc +675 -0
- package/templates/_base/.cursor/rules/frontend_rules.mdc +268 -0
- package/templates/_base/.env.convex.example +3 -0
- package/templates/_base/.github/workflows/ci.yml +29 -0
- package/templates/_base/.nvmrc +1 -0
- package/templates/_base/.vscode/settings.json +9 -0
- package/templates/_base/apps/api/auth.config.ts +18 -0
- package/templates/_base/apps/api/functions.ts +99 -0
- package/templates/_base/apps/api/project.json +22 -0
- package/templates/_base/apps/api/schema.ts +11 -0
- package/templates/_base/apps/api/todos/crud.ts +81 -0
- package/templates/_base/apps/api/todos/schema.ts +11 -0
- package/templates/_base/apps/api/todos/types.ts +22 -0
- package/templates/_base/apps/api/tsconfig.json +23 -0
- package/templates/_base/apps/api/types.ts +16 -0
- package/templates/_base/biome.json +114 -0
- package/templates/_base/convex.json +4 -0
- package/templates/_base/emails/project.json +16 -0
- package/templates/_base/emails/tsconfig.json +5 -0
- package/templates/_base/emails/welcome_email.tsx +53 -0
- package/templates/_base/nx.json +29 -0
- package/templates/_base/package.json +73 -0
- package/templates/_base/scripts/sync_convex_env.ts +63 -0
- package/templates/_base/shared/assets/image.d.ts +4 -0
- package/templates/_base/shared/assets/src/styles/global.css +73 -0
- package/templates/_base/shared/assets/tsconfig.json +5 -0
- package/templates/_base/shared/ui/src/base/alert_dialog.tsx +139 -0
- package/templates/_base/shared/ui/src/base/badge.tsx +33 -0
- package/templates/_base/shared/ui/src/base/basic_data_table.tsx +61 -0
- package/templates/_base/shared/ui/src/base/button.tsx +69 -0
- package/templates/_base/shared/ui/src/base/button_group.tsx +82 -0
- package/templates/_base/shared/ui/src/base/card.tsx +79 -0
- package/templates/_base/shared/ui/src/base/checkbox.tsx +26 -0
- package/templates/_base/shared/ui/src/base/command.tsx +165 -0
- package/templates/_base/shared/ui/src/base/dialog.tsx +129 -0
- package/templates/_base/shared/ui/src/base/dropdown_menu.tsx +232 -0
- package/templates/_base/shared/ui/src/base/form.tsx +161 -0
- package/templates/_base/shared/ui/src/base/input.tsx +129 -0
- package/templates/_base/shared/ui/src/base/label.tsx +19 -0
- package/templates/_base/shared/ui/src/base/popover.tsx +46 -0
- package/templates/_base/shared/ui/src/base/radio_group.tsx +49 -0
- package/templates/_base/shared/ui/src/base/resizable.tsx +55 -0
- package/templates/_base/shared/ui/src/base/scroll_area.tsx +44 -0
- package/templates/_base/shared/ui/src/base/select.tsx +151 -0
- package/templates/_base/shared/ui/src/base/separator.tsx +32 -0
- package/templates/_base/shared/ui/src/base/sheet.tsx +130 -0
- package/templates/_base/shared/ui/src/base/side_bar.tsx +688 -0
- package/templates/_base/shared/ui/src/base/skeleton.tsx +7 -0
- package/templates/_base/shared/ui/src/base/spinner.tsx +20 -0
- package/templates/_base/shared/ui/src/base/switch.tsx +27 -0
- package/templates/_base/shared/ui/src/base/table.tsx +91 -0
- package/templates/_base/shared/ui/src/base/text_area.tsx +21 -0
- package/templates/_base/shared/ui/src/base/tooltip.tsx +31 -0
- package/templates/_base/shared/ui/src/base/utils.ts +17 -0
- package/templates/_base/shared/ui/src/hooks/use_keyboard_press.tsx +48 -0
- package/templates/_base/shared/ui/src/hooks/use_keyboard_release.tsx +48 -0
- package/templates/_base/shared/ui/src/hooks/use_mobile.tsx +25 -0
- package/templates/_base/shared/ui/src/hooks/use_mouse_click.tsx +44 -0
- package/templates/_base/shared/ui/src/hooks/use_mouse_location.tsx +55 -0
- package/templates/_base/shared/ui/src/hooks/use_outside_click.tsx +29 -0
- package/templates/_base/shared/ui/src/hooks/use_query_params.tsx +33 -0
- package/templates/_base/shared/ui/tsconfig.json +8 -0
- package/templates/_base/shared/utils/src/convex.ts +3 -0
- package/templates/_base/shared/utils/src/time.ts +12 -0
- package/templates/_base/shared/utils/tsconfig.json +5 -0
- package/templates/_base/skills-lock.json +35 -0
- package/templates/_base/tsconfig.base.json +34 -0
- package/templates/nextjs/.env.example +8 -0
- package/templates/nextjs/index.d.ts +6 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.js +22 -0
- package/templates/nextjs/postcss.config.js +17 -0
- package/templates/nextjs/project.json +22 -0
- package/templates/nextjs/src/app/(auth)/layout.tsx +21 -0
- package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +22 -0
- package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +15 -0
- package/templates/nextjs/src/app/(dashboard)/layout.tsx +27 -0
- package/templates/nextjs/src/app/(dashboard)/page.tsx +5 -0
- package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +23 -0
- package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +16 -0
- package/templates/nextjs/src/app/app.css +3 -0
- package/templates/nextjs/src/app/layout.tsx +26 -0
- package/templates/nextjs/src/convex.ts +11 -0
- package/templates/nextjs/src/middleware.ts +18 -0
- package/templates/nextjs/src/providers/convex_provider.tsx +44 -0
- package/templates/nextjs/src/surfaces/home_surface.tsx +22 -0
- package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +97 -0
- package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +107 -0
- package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +90 -0
- package/templates/nextjs/src/ui/sidebar/nav_link.tsx +36 -0
- package/templates/nextjs/src/ui/sidebar/sidebar.tsx +125 -0
- package/templates/nextjs/src/utils/font.ts +9 -0
- package/templates/nextjs/tsconfig.json +42 -0
- package/templates/react-router/.env.example +8 -0
- package/templates/react-router/postcss.config.js +15 -0
- package/templates/react-router/project.json +23 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/react-router.config.ts +9 -0
- package/templates/react-router/src/app.css +3 -0
- package/templates/react-router/src/components/error_boundary.tsx +33 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +76 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/user_menu.tsx +36 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +22 -0
- package/templates/react-router/src/providers/api_auth_provider.tsx +38 -0
- package/templates/react-router/src/root.tsx +37 -0
- package/templates/react-router/src/routes/auth/layout.tsx +13 -0
- package/templates/react-router/src/routes/auth/sign-in.tsx +13 -0
- package/templates/react-router/src/routes/index.tsx +9 -0
- package/templates/react-router/src/routes/layout.tsx +26 -0
- package/templates/react-router/src/routes/todos/[id].tsx +22 -0
- package/templates/react-router/src/routes/todos/index.tsx +13 -0
- package/templates/react-router/src/routes.ts +12 -0
- package/templates/react-router/src/surfaces/home_surface.tsx +20 -0
- package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +87 -0
- package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +102 -0
- package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +81 -0
- package/templates/react-router/tsconfig.json +20 -0
- package/templates/react-router/vite.config.ts +40 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Custom Sign-Up Flow
|
|
2
|
+
|
|
3
|
+
Build a custom sign-up experience using the `useSignUp()` hook.
|
|
4
|
+
|
|
5
|
+
## Hook API
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { useSignUp } from '@clerk/nextjs' // or @clerk/react, @clerk/expo
|
|
9
|
+
|
|
10
|
+
const { signUp, errors, fetchStatus } = useSignUp()
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Property | Type | Description |
|
|
14
|
+
|----------|------|-------------|
|
|
15
|
+
| `signUp` | `SignUpFuture` | Sign-up object with namespaced methods |
|
|
16
|
+
| `errors` | `Errors<SignUpFields>` | Structured error object |
|
|
17
|
+
| `fetchStatus` | `'idle' \| 'fetching'` | Network request status |
|
|
18
|
+
|
|
19
|
+
## Sign-Up Methods
|
|
20
|
+
|
|
21
|
+
### Password (Email/Password)
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const { error } = await signUp.password({
|
|
25
|
+
emailAddress: 'user@example.com',
|
|
26
|
+
password: 'securePassword123',
|
|
27
|
+
firstName: 'Jane', // optional
|
|
28
|
+
lastName: 'Doe', // optional
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### SSO (OAuth)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const { error } = await signUp.sso({
|
|
36
|
+
strategy: 'oauth_google', // or 'oauth_github', etc.
|
|
37
|
+
redirectUrl: '/dashboard', // where to go after SSO completes
|
|
38
|
+
redirectCallbackUrl: '/sso-callback', // intermediate callback route
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Web3
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const { error } = await signUp.web3({ strategy: 'web3_solana_signature' })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Update (add fields to existing sign-up)
|
|
49
|
+
|
|
50
|
+
Use `update()` to add optional fields (name, metadata, legal acceptance, locale) to an existing sign-up before finalization.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const { error } = await signUp.update({
|
|
54
|
+
firstName: 'Jane',
|
|
55
|
+
lastName: 'Doe',
|
|
56
|
+
unsafeMetadata: { referralSource: 'twitter' },
|
|
57
|
+
legalAccepted: true,
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Email / Phone Verification
|
|
62
|
+
|
|
63
|
+
After creating a sign-up, verify the user's email or phone:
|
|
64
|
+
|
|
65
|
+
### Email Code
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Send verification code
|
|
69
|
+
const { error } = await signUp.verifications.sendEmailCode()
|
|
70
|
+
|
|
71
|
+
// Verify the code
|
|
72
|
+
const { error } = await signUp.verifications.verifyEmailCode({ code: '123456' })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Phone Code
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Send verification code
|
|
79
|
+
const { error } = await signUp.verifications.sendPhoneCode()
|
|
80
|
+
|
|
81
|
+
// Verify the code
|
|
82
|
+
const { error } = await signUp.verifications.verifyPhoneCode({ code: '123456' })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Email Link
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// verificationUrl: where the user lands after clicking the email link (relative or absolute)
|
|
89
|
+
const { error } = await signUp.verifications.sendEmailLink({ verificationUrl: '/verify' })
|
|
90
|
+
// User clicks the link in their email to verify
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Finalizing Sign-Up
|
|
94
|
+
|
|
95
|
+
After successful sign-up and verification, call `finalize()` to activate the session:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
await signUp.finalize({
|
|
99
|
+
navigate: async ({ session, decorateUrl }) => {
|
|
100
|
+
const destination = session.currentTask
|
|
101
|
+
? `/sign-up/tasks/${session.currentTask.key}`
|
|
102
|
+
: '/'
|
|
103
|
+
const url = decorateUrl(destination)
|
|
104
|
+
// decorateUrl may return an absolute URL for Safari ITP
|
|
105
|
+
if (url.startsWith('http')) {
|
|
106
|
+
window.location.href = url
|
|
107
|
+
} else {
|
|
108
|
+
router.push(url)
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Transferable Sign-Ups
|
|
115
|
+
|
|
116
|
+
If `signUp.isTransferable` is `true`, the identifier matches an existing user and the sign-up should be transferred to a sign-in flow. This involves coordinating between sign-up and sign-in resources. See the [transferable sign-up docs](https://clerk.com/docs/custom-flows/overview) for the full implementation.
|
|
117
|
+
|
|
118
|
+
### Reset State
|
|
119
|
+
|
|
120
|
+
Clear local sign-up state and start over:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
signUp.reset()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Error Handling
|
|
127
|
+
|
|
128
|
+
All methods return `Promise<{ error: ClerkError | null }>`. Errors are also available reactively on the hook:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const { signUp, errors } = useSignUp()
|
|
132
|
+
|
|
133
|
+
// Field-level errors
|
|
134
|
+
errors?.fields?.emailAddress // { code, message, longMessage? }
|
|
135
|
+
errors?.fields?.password // { code, message, longMessage? }
|
|
136
|
+
errors?.fields?.firstName // { code, message, longMessage? }
|
|
137
|
+
errors?.fields?.lastName // { code, message, longMessage? }
|
|
138
|
+
errors?.fields?.phoneNumber // { code, message, longMessage? }
|
|
139
|
+
errors?.fields?.username // { code, message, longMessage? }
|
|
140
|
+
errors?.fields?.code // { code, message, longMessage? }
|
|
141
|
+
|
|
142
|
+
// Global errors
|
|
143
|
+
errors?.global // ClerkGlobalHookError[] | null
|
|
144
|
+
|
|
145
|
+
// Raw error array
|
|
146
|
+
errors?.raw // ClerkError[] | null
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Complete Example: Phone OTP Sign-Up
|
|
150
|
+
|
|
151
|
+
From [the docs](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp). Uses phone OTP with inline comments for adapting to email OTP.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
'use client'
|
|
155
|
+
|
|
156
|
+
import * as React from 'react'
|
|
157
|
+
import { useAuth, useSignUp } from '@clerk/nextjs'
|
|
158
|
+
import { useRouter } from 'next/navigation'
|
|
159
|
+
|
|
160
|
+
export default function SignUpPage() {
|
|
161
|
+
const { signUp, errors, fetchStatus } = useSignUp()
|
|
162
|
+
const { isSignedIn } = useAuth()
|
|
163
|
+
const router = useRouter()
|
|
164
|
+
|
|
165
|
+
const handleSubmit = async (formData: FormData) => {
|
|
166
|
+
// For email OTP: collect the email address instead of the phone number
|
|
167
|
+
const phoneNumber = formData.get('phoneNumber') as string
|
|
168
|
+
|
|
169
|
+
// For email OTP: change create({ phoneNumber }) to create({ emailAddress })
|
|
170
|
+
const error = await signUp.create({ phoneNumber })
|
|
171
|
+
|
|
172
|
+
// For email OTP: change sendPhoneCode() to sendEmailCode()
|
|
173
|
+
if (!error) await signUp.verifications.sendPhoneCode()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const handleVerify = async (formData: FormData) => {
|
|
177
|
+
const code = formData.get('code') as string
|
|
178
|
+
|
|
179
|
+
// For email OTP: change verifyPhoneCode() to verifyEmailCode()
|
|
180
|
+
await signUp.verifications.verifyPhoneCode({ code })
|
|
181
|
+
|
|
182
|
+
if (signUp.status === 'complete') {
|
|
183
|
+
await signUp.finalize({
|
|
184
|
+
navigate: ({ session, decorateUrl }) => {
|
|
185
|
+
if (session?.currentTask) {
|
|
186
|
+
// Handle pending session tasks
|
|
187
|
+
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
|
|
188
|
+
console.log(session?.currentTask)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const url = decorateUrl('/')
|
|
193
|
+
if (url.startsWith('http')) {
|
|
194
|
+
window.location.href = url
|
|
195
|
+
} else {
|
|
196
|
+
router.push(url)
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (signUp.status === 'complete' || isSignedIn) {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
signUp.status === 'missing_requirements' &&
|
|
209
|
+
// For email OTP: check for phone_number instead of email_address
|
|
210
|
+
signUp.unverifiedFields.includes('phone_number') &&
|
|
211
|
+
signUp.missingFields.length === 0
|
|
212
|
+
) {
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
<h1>Verify your account</h1>
|
|
216
|
+
<form action={handleVerify}>
|
|
217
|
+
<div>
|
|
218
|
+
<label htmlFor="code">Code</label>
|
|
219
|
+
<input id="code" name="code" type="text" />
|
|
220
|
+
</div>
|
|
221
|
+
{errors.fields.code && <p>{errors.fields.code.message}</p>}
|
|
222
|
+
<button type="submit" disabled={fetchStatus === 'fetching'}>
|
|
223
|
+
Verify
|
|
224
|
+
</button>
|
|
225
|
+
</form>
|
|
226
|
+
{/* For email OTP: change sendPhoneCode() to sendEmailCode() */}
|
|
227
|
+
<button onClick={() => signUp.verifications.sendPhoneCode()}>I need a new code</button>
|
|
228
|
+
</>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
<h1>Sign up</h1>
|
|
235
|
+
<form action={handleSubmit}>
|
|
236
|
+
{/* For email OTP: collect the emailAddress instead */}
|
|
237
|
+
<div>
|
|
238
|
+
<label htmlFor="phoneNumber">Phone number</label>
|
|
239
|
+
<input id="phoneNumber" name="phoneNumber" type="tel" />
|
|
240
|
+
{errors.fields.phoneNumber && <p>{errors.fields.phoneNumber.message}</p>}
|
|
241
|
+
</div>
|
|
242
|
+
<button type="submit" disabled={fetchStatus === 'fetching'}>
|
|
243
|
+
Continue
|
|
244
|
+
</button>
|
|
245
|
+
</form>
|
|
246
|
+
{errors && <p>{JSON.stringify(errors, null, 2)}</p>}
|
|
247
|
+
|
|
248
|
+
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
|
|
249
|
+
<div id="clerk-captcha" />
|
|
250
|
+
</>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Docs
|
|
256
|
+
|
|
257
|
+
- [Custom sign-up flow](https://clerk.com/docs/custom-flows/overview)
|
|
258
|
+
- [Email/phone OTP custom flow](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp)
|
|
259
|
+
- [useSignUp() reference](https://clerk.com/docs/references/react/use-sign-up)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# `<Show>` Component
|
|
2
|
+
|
|
3
|
+
The `<Show>` component conditionally renders content based on authentication state, roles, permissions, billing plans, and features.
|
|
4
|
+
|
|
5
|
+
> **Core 2 ONLY (skip if current SDK):** The `<Show>` component does not exist in Core 2. Use `<SignedIn>`, `<SignedOut>`, and `<Protect>` instead. See migration table below.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Show } from '@clerk/nextjs' // Next.js
|
|
11
|
+
import { Show } from '@clerk/react' // React
|
|
12
|
+
import { Show } from '@clerk/react-router' // React Router
|
|
13
|
+
import { Show } from '@clerk/expo' // Expo
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Description |
|
|
19
|
+
|------|------|-------------|
|
|
20
|
+
| `when` | `string \| object \| function` | Condition for rendering children |
|
|
21
|
+
| `fallback?` | `ReactNode` | Content shown when condition fails |
|
|
22
|
+
| `treatPendingAsSignedOut?` | `boolean` | Treat pending sessions as signed-out (default: `true`) |
|
|
23
|
+
|
|
24
|
+
## `when` Prop Variants
|
|
25
|
+
|
|
26
|
+
### Authentication State
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// Show content only when signed in
|
|
30
|
+
<Show when="signed-in">
|
|
31
|
+
<p>Welcome back!</p>
|
|
32
|
+
</Show>
|
|
33
|
+
|
|
34
|
+
// Show content only when signed out
|
|
35
|
+
<Show when="signed-out">
|
|
36
|
+
<p>Please sign in.</p>
|
|
37
|
+
</Show>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Role Check
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<Show when={{ role: 'org:admin' }}>
|
|
44
|
+
<AdminPanel />
|
|
45
|
+
</Show>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Permission Check
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<Show when={{ permission: 'org:billing:manage' }}>
|
|
52
|
+
<BillingSettings />
|
|
53
|
+
</Show>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Billing Feature Check
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<Show when={{ feature: 'widgets' }}>
|
|
60
|
+
<WidgetBuilder />
|
|
61
|
+
</Show>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Billing Plan Check
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Show when={{ plan: 'gold' }}>
|
|
68
|
+
<PremiumContent />
|
|
69
|
+
</Show>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Custom Condition (Function)
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Show when={(has) => has({ role: 'org:admin' }) || has({ permission: 'org:billing:manage' })}>
|
|
76
|
+
<SettingsPanel />
|
|
77
|
+
</Show>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Fallback Content
|
|
81
|
+
|
|
82
|
+
Show alternative content when the condition fails:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<Show when="signed-in" fallback={<p>Please sign in to continue.</p>}>
|
|
86
|
+
<Dashboard />
|
|
87
|
+
</Show>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Session Tasks and Pending State
|
|
91
|
+
|
|
92
|
+
The `treatPendingAsSignedOut` prop controls how pending sessions (sessions with incomplete tasks) are handled:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Default: pending sessions are treated as signed-out
|
|
96
|
+
<Show when="signed-in" treatPendingAsSignedOut>
|
|
97
|
+
<Dashboard />
|
|
98
|
+
</Show>
|
|
99
|
+
|
|
100
|
+
// Treat pending sessions as signed-in (e.g., to show task completion UI)
|
|
101
|
+
<Show when="signed-in" treatPendingAsSignedOut={false}>
|
|
102
|
+
<TaskCompletionFlow />
|
|
103
|
+
</Show>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Security Caveat
|
|
107
|
+
|
|
108
|
+
**`<Show>` only visually hides content** — it remains in browser source. It is not a security boundary. For protecting sensitive data, always verify authentication server-side with `auth()` or use `auth.protect()` in middleware.
|
|
109
|
+
|
|
110
|
+
## Migration from Core 2
|
|
111
|
+
|
|
112
|
+
| Core 2 | Current |
|
|
113
|
+
|--------|---------|
|
|
114
|
+
| `<SignedIn>` | `<Show when="signed-in">` |
|
|
115
|
+
| `<SignedOut>` | `<Show when="signed-out">` |
|
|
116
|
+
| `<Protect role="org:admin">` | `<Show when={{ role: 'org:admin' }}>` |
|
|
117
|
+
| `<Protect permission="org:billing:manage">` | `<Show when={{ permission: 'org:billing:manage' }}>` |
|
|
118
|
+
| `<Protect condition={(has) => expr}>` | `<Show when={(has) => expr}>` |
|
|
119
|
+
| `<Protect fallback={...}>` | `<Show when={...} fallback={...}>` |
|
|
120
|
+
| *(no equivalent)* | `<Show when={{ feature: 'widgets' }}>` |
|
|
121
|
+
| *(no equivalent)* | `<Show when={{ plan: 'gold' }}>` |
|
|
122
|
+
|
|
123
|
+
## Docs
|
|
124
|
+
|
|
125
|
+
- [Show component reference](https://clerk.com/docs/components/control/show)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clerk-nextjs-patterns
|
|
3
|
+
description: Advanced Next.js patterns - middleware, Server Actions, caching with Clerk.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: WebFetch
|
|
6
|
+
metadata:
|
|
7
|
+
author: clerk
|
|
8
|
+
version: "2.1.0"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Next.js Patterns
|
|
12
|
+
|
|
13
|
+
> **Version**: Check `package.json` for the SDK version — see `clerk` skill for the version table. Core 2 differences are noted inline with `> **Core 2 ONLY (skip if current SDK):**` callouts.
|
|
14
|
+
|
|
15
|
+
For basic setup, see `setup/`.
|
|
16
|
+
|
|
17
|
+
## Impact Levels
|
|
18
|
+
|
|
19
|
+
- **CRITICAL** - Breaking bugs, security holes
|
|
20
|
+
- **HIGH** - Common mistakes
|
|
21
|
+
- **MEDIUM** - Optimization
|
|
22
|
+
|
|
23
|
+
## References
|
|
24
|
+
|
|
25
|
+
| Reference | Impact |
|
|
26
|
+
|-----------|--------|
|
|
27
|
+
| `references/server-vs-client.md` | CRITICAL - `await auth()` vs hooks |
|
|
28
|
+
| `references/middleware-strategies.md` | HIGH - Public-first vs protected-first, `proxy.ts` (Next.js <=15: `middleware.ts`) |
|
|
29
|
+
| `references/server-actions.md` | HIGH - Protect mutations |
|
|
30
|
+
| `references/api-routes.md` | HIGH - 401 vs 403 |
|
|
31
|
+
| `references/caching-auth.md` | MEDIUM - User-scoped caching |
|
|
32
|
+
|
|
33
|
+
## Mental Model
|
|
34
|
+
|
|
35
|
+
Server vs Client = different auth APIs:
|
|
36
|
+
- **Server**: `await auth()` from `@clerk/nextjs/server` (async!)
|
|
37
|
+
- **Client**: `useAuth()` hook from `@clerk/nextjs` (sync)
|
|
38
|
+
|
|
39
|
+
Never mix them. Server Components use server imports, Client Components use hooks.
|
|
40
|
+
|
|
41
|
+
Key properties from `auth()`:
|
|
42
|
+
- `isAuthenticated` — boolean, replaces the `!!userId` pattern
|
|
43
|
+
- `sessionStatus` — `'active'` | `'pending'`, for detecting incomplete session tasks
|
|
44
|
+
- `userId`, `orgId`, `orgSlug`, `has()`, `protect()` — unchanged
|
|
45
|
+
|
|
46
|
+
> **Core 2 ONLY (skip if current SDK):** `isAuthenticated` and `sessionStatus` are not available. Check `!!userId` instead.
|
|
47
|
+
|
|
48
|
+
## Minimal Pattern
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Server Component
|
|
52
|
+
import { auth } from '@clerk/nextjs/server'
|
|
53
|
+
|
|
54
|
+
export default async function Page() {
|
|
55
|
+
const { isAuthenticated, userId } = await auth() // MUST await!
|
|
56
|
+
if (!isAuthenticated) return <p>Not signed in</p>
|
|
57
|
+
return <p>Hello {userId}</p>
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> **Core 2 ONLY (skip if current SDK):** `isAuthenticated` is not available. Use `if (!userId)` instead.
|
|
62
|
+
|
|
63
|
+
### Conditional Rendering with `<Show>`
|
|
64
|
+
|
|
65
|
+
For client-side conditional rendering based on auth state:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Show } from '@clerk/nextjs'
|
|
69
|
+
|
|
70
|
+
<Show when="signed-in" fallback={<p>Please sign in</p>}>
|
|
71
|
+
<Dashboard />
|
|
72
|
+
</Show>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> **Core 2 ONLY (skip if current SDK):** Use `<SignedIn>` and `<SignedOut>` components instead of `<Show>`. See `custom-ui/core-3/show-component.md` for the full migration table.
|
|
76
|
+
|
|
77
|
+
## Common Pitfalls
|
|
78
|
+
|
|
79
|
+
| Symptom | Cause | Fix |
|
|
80
|
+
|---------|-------|-----|
|
|
81
|
+
| `undefined` userId in Server Component | Missing `await` | `await auth()` not `auth()` |
|
|
82
|
+
| Auth not working on API routes | Missing matcher | Add `'/(api|trpc)(.*)'` to `proxy.ts` (Next.js <=15: `middleware.ts`) |
|
|
83
|
+
| Cache returns wrong user's data | Missing userId in key | Include `userId` in `unstable_cache` key |
|
|
84
|
+
| Mutations bypass auth | Unprotected Server Action | Check `auth()` at start of action |
|
|
85
|
+
| Wrong HTTP error code | Confused 401/403 | 401 = not signed in, 403 = no permission |
|
|
86
|
+
|
|
87
|
+
## See Also
|
|
88
|
+
|
|
89
|
+
- `setup/`
|
|
90
|
+
- `orgs/`
|
|
91
|
+
|
|
92
|
+
## Docs
|
|
93
|
+
|
|
94
|
+
[Next.js SDK](https://clerk.com/docs/reference/nextjs/overview)
|
package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# API Routes (HIGH)
|
|
2
|
+
|
|
3
|
+
## Auth Check Pattern
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { auth } from '@clerk/nextjs/server';
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
const { isAuthenticated, userId } = await auth();
|
|
10
|
+
if (!isAuthenticated) {
|
|
11
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
12
|
+
}
|
|
13
|
+
const data = await db.data.findMany({ where: { userId } });
|
|
14
|
+
return Response.json(data);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> **Core 2 ONLY (skip if current SDK):** `isAuthenticated` is not available. Use `if (!userId)` instead.
|
|
19
|
+
|
|
20
|
+
## 401 vs 403
|
|
21
|
+
|
|
22
|
+
- **401** - Not authenticated
|
|
23
|
+
- **403** - Authenticated but lacks permission
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export async function DELETE(req: Request) {
|
|
27
|
+
const { isAuthenticated, has } = await auth();
|
|
28
|
+
if (!isAuthenticated) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
29
|
+
|
|
30
|
+
const isAdmin = await has({ role: 'org:admin' });
|
|
31
|
+
if (!isAdmin) return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
32
|
+
|
|
33
|
+
return Response.json({ success: true });
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Org Route Protection
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
export async function GET(req: Request, { params }: { params: { orgId: string } }) {
|
|
41
|
+
const { userId, orgId } = await auth();
|
|
42
|
+
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
43
|
+
if (orgId !== params.orgId) return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
44
|
+
|
|
45
|
+
const orgData = await db.orgs.findUnique({ where: { id: orgId } });
|
|
46
|
+
return Response.json(orgData);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
[Docs](https://clerk.com/docs/reference/nextjs/auth)
|
package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Caching with Auth (CRITICAL)
|
|
2
|
+
|
|
3
|
+
**CRITICAL**: Cache keys MUST include userId/orgId to prevent data leaking between users.
|
|
4
|
+
|
|
5
|
+
## User-Scoped Cache
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { auth } from '@clerk/nextjs/server';
|
|
9
|
+
import { unstable_cache } from 'next/cache';
|
|
10
|
+
|
|
11
|
+
export default async function ProfilePage() {
|
|
12
|
+
const { userId } = await auth();
|
|
13
|
+
if (!userId) return <div>Not signed in</div>;
|
|
14
|
+
|
|
15
|
+
const cachedGetUserData = unstable_cache(
|
|
16
|
+
() => getUserData(userId),
|
|
17
|
+
[`user-${userId}`],
|
|
18
|
+
{ revalidate: 60, tags: [`user-${userId}`] }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const userData = await cachedGetUserData();
|
|
22
|
+
return <div>{userData.name}</div>;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Revalidate After Updates
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
'use server';
|
|
30
|
+
import { revalidateTag } from 'next/cache';
|
|
31
|
+
import { auth } from '@clerk/nextjs/server';
|
|
32
|
+
|
|
33
|
+
export async function updateProfile(formData: FormData) {
|
|
34
|
+
const { userId } = await auth();
|
|
35
|
+
if (!userId) throw new Error('Unauthorized');
|
|
36
|
+
|
|
37
|
+
await db.users.update({
|
|
38
|
+
where: { id: userId },
|
|
39
|
+
data: { name: formData.get('name') as string },
|
|
40
|
+
});
|
|
41
|
+
revalidateTag(`user-${userId}`);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Org-Scoped Cache
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const { orgId } = await auth();
|
|
49
|
+
const getOrgData = unstable_cache(
|
|
50
|
+
() => db.orgData.findMany({ where: { organizationId: orgId } }),
|
|
51
|
+
[`org-${orgId}-data`],
|
|
52
|
+
{ revalidate: 300, tags: [`org-${orgId}`] }
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
[Docs](https://nextjs.org/docs/app/building-your-application/caching)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Middleware Strategies (HIGH)
|
|
2
|
+
|
|
3
|
+
> **Filename:** `proxy.ts` (Next.js <=15: `middleware.ts`). The code is identical; only the filename changes.
|
|
4
|
+
|
|
5
|
+
## Public-First (marketing sites, blogs)
|
|
6
|
+
|
|
7
|
+
Protect specific routes, allow everything else:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
11
|
+
|
|
12
|
+
const isProtectedRoute = createRouteMatcher([
|
|
13
|
+
'/dashboard(.*)',
|
|
14
|
+
'/settings(.*)',
|
|
15
|
+
'/api/private(.*)',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
19
|
+
if (isProtectedRoute(req)) await auth.protect();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const config = {
|
|
23
|
+
matcher: [
|
|
24
|
+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
25
|
+
'/(api|trpc)(.*)',
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Protected-First (internal tools, dashboards)
|
|
31
|
+
|
|
32
|
+
Block everything, allow specific public routes:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
36
|
+
|
|
37
|
+
const isPublicRoute = createRouteMatcher([
|
|
38
|
+
'/',
|
|
39
|
+
'/sign-in(.*)',
|
|
40
|
+
'/sign-up(.*)',
|
|
41
|
+
'/api/public(.*)',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
45
|
+
if (!isPublicRoute(req)) await auth.protect();
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Session Tasks
|
|
50
|
+
|
|
51
|
+
When session tasks are enabled (e.g., forced password reset, MFA setup), users may have a `pending` session status. You can handle this in middleware:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
55
|
+
const { sessionStatus } = await auth();
|
|
56
|
+
|
|
57
|
+
// Redirect pending sessions to task completion page
|
|
58
|
+
if (sessionStatus === 'pending') {
|
|
59
|
+
return NextResponse.redirect(new URL('/sign-in/tasks', req.url));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isProtectedRoute(req)) await auth.protect();
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Core 2 ONLY (skip if current SDK):** `sessionStatus` is not available. Session tasks do not exist in Core 2.
|
|
67
|
+
|
|
68
|
+
[Docs](https://clerk.com/docs/reference/nextjs/clerk-middleware)
|