Skip to content

Commit

Permalink
Support for <GoogleOneTap/> (#3409)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef committed May 21, 2024
1 parent 022223e commit fcc349c
Show file tree
Hide file tree
Showing 26 changed files with 601 additions and 207 deletions.
14 changes: 14 additions & 0 deletions .changeset/funny-berries-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@clerk/nextjs': minor
'@clerk/clerk-react': minor
---

Add support for GoogleOneTap
### React component
- `<GoogleOneTap/>`

Customize the UX of the prompt

```tsx
<GoogleOneTap cancelOnTapOutside={false} itpSupport={false} fedCmSupport={false}/>
```
37 changes: 37 additions & 0 deletions .changeset/mighty-dolphins-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'@clerk/clerk-js': minor
---

### Use the Google One Tap component from with Vanilla JS
- `Clerk.openGoogleOneTap({ cancelOnTapOutside: false, fedCmSupport: false, itpSupport: false })`
- `Clerk.closeGoogleOneTap()`
### Low level APIs for custom flows
- `await Clerk.authenticateWithGoogleOneTap({ token: 'xxxx'})`
- `await Clerk.handleGoogleOneTapCallback()`


We recommend using this two methods together in order and let Clerk perform the correct redirections.
```tsx
google.accounts.id.initialize({
callback: async response => {
const signInOrUp = await Clerk.authenticateWithGoogleOneTap({ token: response.credential})
await Clerk.handleGoogleOneTapCallback(signInOrUp, {
afterSignInUrl: window.location.href,
})
},
});
```

In case you want to handle the redirection and session management yourself you can do so like this
```tsx
google.accounts.id.initialize({
callback: async response => {
const signInOrUp = await Clerk.authenticateWithGoogleOneTap({ token: response.credential})
if(signInOrUp.status === 'complete') {
await Clerk.setActive({
session: signInOrUp.createdSessionId
})
}
},
});
```
5 changes: 5 additions & 0 deletions .changeset/shaggy-elephants-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/chrome-extension': minor
---

Update export snapshot tests to include `GoogleOneTap`.
21 changes: 21 additions & 0 deletions .changeset/silly-mugs-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@clerk/types': minor
---

Added the following types
```tsx
interface Clerk {
...
openGoogleOneTap: (props?: GoogleOneTapProps) => void;
closeGoogleOneTap: () => void;
authenticateWithGoogleOneTap: (params: AuthenticateWithGoogleOneTapParams) => Promise<SignInResource | SignUpResource>;
handleGoogleOneTapCallback: (
signInOrUp: SignInResource | SignUpResource,
params: HandleOAuthCallbackParams,
customNavigate?: (to: string) => Promise<unknown>,
) => Promise<unknown>;
...
}

type GoogleOneTapStrategy = 'google_one_tap'
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkProvider",
"CreateOrganization",
"EmailLinkErrorCode",
"GoogleOneTap",
"MagicLinkErrorCode",
"MultisessionAppSupport",
"OrganizationList",
Expand All @@ -32,7 +33,6 @@ exports[`public exports should not include a breaking change 1`] = `
"WithClerk",
"WithSession",
"WithUser",
"__experimental_GoogleOneTap",
"__internal__setErrorThrowerOptions",
"isClerkAPIResponseError",
"isEmailLinkError",
Expand Down
144 changes: 112 additions & 32 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
handleValueOrFn,
inBrowser as inClientSide,
is4xxError,
isClerkAPIResponseError,
isHttpOrHttps,
isLegacyFrontendApiKey,
isValidBrowserOnline,
Expand All @@ -17,6 +18,7 @@ import {
} from '@clerk/shared';
import type {
ActiveSessionResource,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
BeforeEmitCallback,
Clerk as ClerkInterface,
Expand All @@ -28,12 +30,12 @@ import type {
DomainOrProxyUrl,
EnvironmentJSON,
EnvironmentResource,
GoogleOneTapProps,
HandleEmailLinkVerificationParams,
HandleMagicLinkVerificationParams,
HandleOAuthCallbackParams,
InstanceType,
ListenerCallback,
OneTapProps,
OrganizationInvitationResource,
OrganizationListProps,
OrganizationMembershipResource,
Expand Down Expand Up @@ -364,6 +366,18 @@ export default class Clerk implements ClerkInterface {
}
};

public openGoogleOneTap = (props?: GoogleOneTapProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
.ensureMounted({ preloadHint: 'GoogleOneTap' })
.then(controls => controls.openModal('googleOneTap', props || {}));
};

public closeGoogleOneTap = (): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('googleOneTap'));
};

public openSignIn = (props?: SignInProps): void => {
this.assertComponentsReady(this.#componentControls);
if (sessionExistsAndSingleSessionModeEnabled(this, this.#environment) && this.#instanceType === 'development') {
Expand Down Expand Up @@ -457,30 +471,6 @@ export default class Clerk implements ClerkInterface {
);
};

public __experimental_mountGoogleOneTap = (node: HTMLDivElement, props?: OneTapProps): void => {
this.assertComponentsReady(this.#componentControls);

void this.#componentControls.ensureMounted({ preloadHint: 'OneTap' }).then(controls =>
controls.mountComponent({
name: 'OneTap',
appearanceKey: 'oneTap',
node,
props,
}),
);
// TODO-ONETAP: Enable telemetry one feature is ready for public beta
// this.telemetry?.record(eventComponentMounted('GoogleOneTap', props));
};

public __experimental_unmountGoogleOneTap = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
Expand Down Expand Up @@ -966,14 +956,47 @@ export default class Clerk implements ClerkInterface {
return null;
};

public handleRedirectCallback = async (
params: HandleOAuthCallbackParams = {},
public handleGoogleOneTapCallback = async (
signInOrUp: SignInResource | SignUpResource,
params: HandleOAuthCallbackParams,
customNavigate?: (to: string) => Promise<unknown>,
): Promise<unknown> => {
if (!this.#isReady || !this.#environment || !this.client) {
return;
}
const { signIn, signUp } = this.client;
const { signIn: _signIn, signUp: _signUp } = this.client;

const signIn = 'identifier' in (signInOrUp || {}) ? (signInOrUp as SignInResource) : _signIn;
const signUp = 'missingFields' in (signInOrUp || {}) ? (signInOrUp as SignUpResource) : _signUp;

const navigate = (to: string) =>
customNavigate && typeof customNavigate === 'function'
? customNavigate(this.buildUrlWithAuth(to))
: this.navigate(this.buildUrlWithAuth(to));

return this._handleRedirectCallback(params, {
signUp,
signIn,
navigate,
});
};

private _handleRedirectCallback = async (
params: HandleOAuthCallbackParams,
{
signIn,
signUp,
navigate,
}: {
signIn: SignInResource;
signUp: SignUpResource;
navigate: (to: string) => Promise<unknown>;
},
): Promise<unknown> => {
if (!this.loaded || !this.#environment || !this.client) {
return;
}

const { displayConfig } = this.#environment;
const { firstFactorVerification } = signIn;
const { externalAccount } = signUp.verifications;
Expand All @@ -983,18 +1006,17 @@ export default class Clerk implements ClerkInterface {
externalAccountStatus: externalAccount.status,
externalAccountErrorCode: externalAccount.error?.code,
externalAccountSessionId: externalAccount.error?.meta?.sessionId,
sessionId: signUp.createdSessionId,
};

const si = {
status: signIn.status,
firstFactorVerificationStatus: firstFactorVerification.status,
firstFactorVerificationErrorCode: firstFactorVerification.error?.code,
firstFactorVerificationSessionId: firstFactorVerification.error?.meta?.sessionId,
sessionId: signIn.createdSessionId,
};

const navigate = (to: string) =>
customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);

const makeNavigate = (to: string) => () => navigate(to);

const navigateToSignIn = makeNavigate(displayConfig.signInUrl);
Expand Down Expand Up @@ -1026,7 +1048,13 @@ export default class Clerk implements ClerkInterface {

const navigateToContinueSignUp = makeNavigate(
params.continueSignUpUrl ||
buildURL({ base: displayConfig.signUpUrl, hashPath: '/continue' }, { stringify: true }),
buildURL(
{
base: displayConfig.signUpUrl,
hashPath: '/continue',
},
{ stringify: true },
),
);

const navigateToNextStepSignUp = ({ missingFields }: { missingFields: SignUpField[] }) => {
Expand All @@ -1046,6 +1074,13 @@ export default class Clerk implements ClerkInterface {
});
};

if (si.status === 'complete') {
return this.setActive({
session: si.sessionId,
beforeEmit: navigateAfterSignIn,
});
}

const userExistsButNeedsToSignIn =
su.externalAccountStatus === 'transferable' && su.externalAccountErrorCode === 'external_account_exists';

Expand Down Expand Up @@ -1108,6 +1143,13 @@ export default class Clerk implements ClerkInterface {
}
}

if (su.status === 'complete') {
return this.setActive({
session: su.sessionId,
beforeEmit: navigateAfterSignUp,
});
}

if (si.status === 'needs_second_factor') {
return navigateToFactorTwo();
}
Expand Down Expand Up @@ -1144,6 +1186,25 @@ export default class Clerk implements ClerkInterface {
return navigateToSignIn();
};

public handleRedirectCallback = async (
params: HandleOAuthCallbackParams = {},
customNavigate?: (to: string) => Promise<unknown>,
): Promise<unknown> => {
if (!this.loaded || !this.#environment || !this.client) {
return;
}
const { signIn, signUp } = this.client;

const navigate = (to: string) =>
customNavigate && typeof customNavigate === 'function' ? customNavigate(to) : this.navigate(to);

return this._handleRedirectCallback(params, {
signUp,
signIn,
navigate,
});
};

public handleUnauthenticated = async (opts = { broadcast: true }): Promise<unknown> => {
if (!this.client || !this.session) {
return;
Expand All @@ -1159,6 +1220,25 @@ export default class Clerk implements ClerkInterface {
return this.setActive({ session: null });
};

public authenticateWithGoogleOneTap = async (
params: AuthenticateWithGoogleOneTapParams,
): Promise<SignInResource | SignUpResource> => {
return this.client?.signIn
.create({
strategy: 'google_one_tap',
token: params.token,
})
.catch(err => {
if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') {
return this.client?.signUp.create({
strategy: 'google_one_tap',
token: params.token,
});
}
throw err;
}) as Promise<SignInResource | SignUpResource>;
};

public authenticateWithMetamask = async ({
redirectUrl,
signUpContinueUrl,
Expand Down
25 changes: 1 addition & 24 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { deepSnakeToCamel, deprecated, isClerkAPIResponseError, Poller } from '@clerk/shared';
import { deepSnakeToCamel, deprecated, Poller } from '@clerk/shared';
import type {
__experimental_AuthenticateWithGoogleOneTapParams,
AttemptFirstFactorParams,
AttemptSecondFactorParams,
AuthenticateWithRedirectParams,
Expand All @@ -25,7 +24,6 @@ import type {
SignInStartEmailLinkFlowParams,
SignInStartMagicLinkFlowParams,
SignInStatus,
SignUpResource,
VerificationResource,
Web3SignatureConfig,
Web3SignatureFactor,
Expand Down Expand Up @@ -240,27 +238,6 @@ export class SignIn extends BaseResource implements SignInResource {
}
};

public __experimental_authenticateWithGoogleOneTap = async (
params: __experimental_AuthenticateWithGoogleOneTapParams,
): Promise<SignInResource | SignUpResource> => {
return this.create({
// TODO-ONETAP: Add new types when feature is ready for public beta
// @ts-expect-error
strategy: 'google_one_tap',
googleOneTapToken: params.token,
}).catch(err => {
if (isClerkAPIResponseError(err) && err.errors[0].code === 'external_account_not_found') {
return SignIn.clerk.client?.signUp.create({
// TODO-ONETAP: Add new types when feature is ready for public beta
// @ts-expect-error
strategy: 'google_one_tap',
googleOneTapToken: params.token,
});
}
throw err;
}) as Promise<SignInResource | SignUpResource>;
};

public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise<SignInResource> => {
const { identifier, generateSignature } = params || {};
if (!(typeof generateSignature === 'function')) {
Expand Down

0 comments on commit fcc349c

Please sign in to comment.