Skip to content

Commit

Permalink
Update invitations (user and organizations); update custom flow for u…
Browse files Browse the repository at this point in the history
…sing tokens; add guide on accepting organization invitations (#1509)

Co-authored-by: panteliselef <[email protected]>
Co-authored-by: victoria <[email protected]>
  • Loading branch information
3 people authored Sep 17, 2024
1 parent 83b515a commit 1955a9e
Show file tree
Hide file tree
Showing 7 changed files with 479 additions and 130 deletions.
119 changes: 73 additions & 46 deletions docs/custom-flows/embedded-email-links.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Embeddable email links with sign-in tokens
description: Learn how to build custom embeddable email link sign-in flows to increase user engagement and reduce drop off in transactional emails, SMS's, and more.
---

An "email link" is a link that, when visited, will automatically authenticate your user so that they can perform some action on your site with less friction than if they had to sign in manually. You can create email links with Clerk by generating a sign-in token, which you can detect and take action on in your frontend.
An "email link" is a link that, when visited, will automatically authenticate your user so that they can perform some action on your site with less friction than if they had to sign in manually. You can create email links with Clerk by generating a sign-in token.

Common use cases include:

Expand All @@ -12,17 +12,17 @@ Common use cases include:
- Recovering abandoned carts
- Surveys or questionnaires

This guide will teach you how to set this flow up.
This guide will demonstrate how to generate a sign-in token and use it to sign in a user.

<Steps>
### Generate a sign-in token

Embeddable email links leverage [sign-in tokens](/docs/reference/backend-api/tag/Sign-in-Tokens#operation/CreateSignInToken){{ target: '_blank' }}, which are JWTs that can be used to sign in to an application without specifying any credentials. A sign-in token can be used at most once, and can be consumed from the Frontend API using the [`ticket`](/docs/references/javascript/sign-in/sign-in#sign-in-create-params) strategy.
[Sign-in tokens](/docs/reference/backend-api/tag/Sign-in-Tokens#operation/CreateSignInToken){{ target: '_blank' }} are JWTs that can be used to sign in to an application without specifying any credentials. A sign-in token can be used **once**, and can be consumed from the Frontend API using the [`ticket`](/docs/references/javascript/sign-in/sign-in#sign-in-create-params) strategy, which is demonstrated in the following example.

> [!NOTE]
> By default, sign-in tokens expire in 30 days. You can optionally supply a different duration in seconds using the `expires_in_seconds` property.
> By default, sign-in tokens expire in 30 days. You can optionally specify a different duration in seconds using the `expires_in_seconds` property.
The following example demonstrates a cURL request that fetches a valid sign-in token:
The following example demonstrates a cURL request that creates a valid sign-in token:

```bash
curl 'https://api.clerk.com/v1/sign_in_tokens' \
Expand All @@ -32,17 +32,19 @@ This guide will teach you how to set this flow up.
-d '{ "user_id": "user_123" }'
```

This will return a token, which can then be embedded as a query param in any link, such as the following example:
This will return a `url` property, which can be used as your email link. Keep in mind that this link will use Clerk's [Account Portal sign-in page](/docs/customization/account-portal/overview#sign-in) to sign in the user.

`https://your-site.com/welcome?token=THE_TOKEN`
If you would rather use your own sign-in page, you can use the `token` property that is returned. Add the `token` as a query param in any link, such as the following example:

You can embedded this link anywhere, such as an email.
`https://your-site.com/accept-token?token=<INSERT_TOKEN_HERE>`

Then, you can embed this link anywhere, such as an email.

### Build a custom flow for signing in with a sign-in token

To do something with the email links you generate, you must setup a page in your frontend that detects the sign-in token, signs the user in, then performs whatever actions you want.
To handle email links with sign-in tokens, you must set up a page in your frontend that detects the token, signs the user in, and performs any additional actions you need.

The following example demonstrates basic code that detects a token and uses it to initiate a sign-in with Clerk:
The following example demonstrates basic code that detects a token in the URL query params and uses it to initiate a sign-in with Clerk:

<Tabs items={["Next.js"]}>
<Tab>
Expand All @@ -53,47 +55,60 @@ This guide will teach you how to set this flow up.
import { useEffect, useState } from 'react'
import { useSearchParams } from 'next/navigation'

export default function AcceptToken() {
export default function Page() {
const [loading, setLoading] = useState<boolean>(false)
const { signIn, setActive } = useSignIn()
const { user } = useUser()
const [signInProcessed, setSignInProcessed] = useState<boolean>(false)

// Get the token from the query params
const signInToken = useSearchParams().get('token')

useEffect(() => {
if (!signIn || !setActive || !signInToken) {
if (!signIn || !setActive || !signInToken || user || loading) {
return
}

const createSignIn = async () => {
setLoading(true)
try {
// Create a signIn with the token.
// Note that you need to use the "ticket" strategy.
const res = await signIn.create({
// Create the `SignIn` with the token
const signInAttempt = await signIn.create({
strategy: 'ticket',
ticket: signInToken as string,
})
setActive({
session: res.createdSessionId,
beforeEmit: () => setSignInProcessed(true),
})

// If the sign-in was successful, set the session to active
if (signInAttempt.status === 'complete') {
setActive({
session: signInAttempt.createdSessionId,
})
} else {
// If the sign-in attempt is not complete, check why.
// User may need to complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err) {
setSignInProcessed(true)
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
} finally {
setLoading(false)
}
}

createSignIn()
}, [signIn, setActive])
}, [signIn, setActive, signInToken, user, loading])

if (!signInToken) {
return <div>no token provided</div>
return <div>No token provided.</div>
}

if (!signInProcessed) {
return <div>loading</div>
if (!user) {
return null
}

if (!user) {
return <div>error invalid token</div>
if (loading) {
return <div>Signing you in...</div>
}

return <div>Signed in as {user.id}</div>
Expand All @@ -105,59 +120,71 @@ This guide will teach you how to set this flow up.
import { useUser, useSignIn } from '@clerk/nextjs'
import { useEffect, useState } from 'react'

// Grab the query param server side, and pass through props
// Get the token from the query param server side, and pass through props
export const getServerSideProps: GetServerSideProps = async (context) => {
return {
props: { signInToken: context.query.token ? context.query.token : null },
}
}

const AcceptToken = ({ signInToken }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
export default function AcceptTokenPage({
signInToken,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const [loading, setLoading] = useState<boolean>(false)
const { signIn, setActive } = useSignIn()
const { user } = useUser()
const [signInProcessed, setSignInProcessed] = useState<boolean>(false)

useEffect(() => {
if (!signIn || !setActive || !signInToken) {
if (!signIn || !setActive || !signInToken || user || loading) {
return
}

const createSignIn = async () => {
setLoading(true)
try {
// Create a signIn with the token.
// Note that you need to use the "ticket" strategy.
const res = await signIn.create({
// Create the `SignIn` with the token
const signInAttempt = await signIn.create({
strategy: 'ticket',
ticket: signInToken as string,
})
setActive({
session: res.createdSessionId,
beforeEmit: () => setSignInProcessed(true),
})

// If the sign-in was successful, set the session to active
if (signInAttempt.status === 'complete') {
setActive({
session: signInAttempt.createdSessionId,
})
} else {
// If the sign-in attempt is not complete, check why.
// User may need to complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err) {
setSignInProcessed(true)
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
setLoading(true)
} finally {
setLoading(false)
}
}

createSignIn()
}, [signIn, setActive])
}, [signIn, setActive, signInToken, user, loading])

if (!signInToken) {
return <div>no token provided</div>
return <div>No token provided.</div>
}

if (!signInProcessed) {
return <div>loading</div>
if (loading) {
return <div>Loading...</div>
}

if (!user) {
return <div>error invalid token</div>
return null
}

return <div>Signed in as {user.id}</div>
}

export default AcceptToken
```
</CodeBlockTabs>
</Tab>
Expand Down
Loading

0 comments on commit 1955a9e

Please sign in to comment.