From ccd01d1f9731137b6f1d94ea4f9c8353348b8d60 Mon Sep 17 00:00:00 2001 From: Johan Venter Date: Tue, 28 Jan 2025 10:30:16 -0800 Subject: [PATCH 1/2] component-boilerplate: Add useAuthenticateWithGoogleMutation --- .../useAuthenticateWithGoogleMutation.ts | 63 +++++++++++++++++++ packages/component-boilerplate/src/index.ts | 1 + 2 files changed, 64 insertions(+) create mode 100644 packages/component-boilerplate/src/hooks/managedIdentity/useAuthenticateWithGoogleMutation.ts diff --git a/packages/component-boilerplate/src/hooks/managedIdentity/useAuthenticateWithGoogleMutation.ts b/packages/component-boilerplate/src/hooks/managedIdentity/useAuthenticateWithGoogleMutation.ts new file mode 100644 index 0000000000..1ea3ef657a --- /dev/null +++ b/packages/component-boilerplate/src/hooks/managedIdentity/useAuthenticateWithGoogleMutation.ts @@ -0,0 +1,63 @@ +import gql from "graphql-tag"; +import decode from "jwt-decode"; + +import { BaseQueryData } from "../graphql/useBaseQuery"; +import { setUserIdentity, DecodedSquatchJWT } from "../environment"; +import { useMutation } from "../graphql/useMutation"; + +const AuthenticateWithGoogleMutation = gql` + mutation AuthenticateWithGoogle($idToken: String!) { + authenticateManagedIdentityWithGoogle( + authenticateManagedIdentityWithGoogleInput: { idToken: $idToken } + ) { + token + email + emailVerified + sessionData + } + } +`; + +interface AuthenticateWithGoogleResult { + authenticateManagedIdentityWithGoogle: { + token: string; + email: string; + emailVerified: boolean; + sessionData: Record; + }; +} + +export function useAuthenticateWithGoogleMutation(): [ + (variables: { + idToken: string; + }) => Promise, + BaseQueryData +] { + const [request, { loading, data, errors }] = + useMutation(AuthenticateWithGoogleMutation); + + const requestAndSetUserIdentity = async (v: { idToken: string }) => { + const result = await request(v); + if ( + !(result instanceof Error) && + result.authenticateManagedIdentityWithGoogle + ) { + const jwt = result.authenticateManagedIdentityWithGoogle.token; + const { user } = decode(jwt); + setUserIdentity({ + jwt, + id: user.id, + accountId: user.accountId, + managedIdentity: { + email: result.authenticateManagedIdentityWithGoogle.email, + emailVerified: + result.authenticateManagedIdentityWithGoogle.emailVerified, + sessionData: result.authenticateManagedIdentityWithGoogle.sessionData, + }, + }); + } + return result; + }; + + return [requestAndSetUserIdentity, { loading, data, errors }]; +} diff --git a/packages/component-boilerplate/src/index.ts b/packages/component-boilerplate/src/index.ts index 2a0847eded..3f8c6811be 100644 --- a/packages/component-boilerplate/src/index.ts +++ b/packages/component-boilerplate/src/index.ts @@ -17,6 +17,7 @@ export { useRequestPasswordResetEmailMutation } from "./hooks/managedIdentity/us export { useRequestVerificationEmailMutation } from "./hooks/managedIdentity/useRequestVerificationEmailMutation"; export { useManagedIdentitySessionQuery } from "./hooks/managedIdentity/useManagedIdentitySessionQuery"; export { useAuthenticateManagedIdentityWithInstantAccess } from "./hooks/instantaccess/useAuthenticateManagedIdentityWithInstantAccess"; +export { useAuthenticateWithGoogleMutation } from "./hooks/managedIdentity/useAuthenticateWithGoogleMutation"; // // GraphQL API From ee5f1511afbfa04299ad805618130e2128daae18 Mon Sep 17 00:00:00 2001 From: Johan Venter Date: Tue, 28 Jan 2025 10:30:35 -0800 Subject: [PATCH 2/2] mint-components: Add sqm-google-signin --- packages/mint-components/src/components.d.ts | 17 ++++ .../components/sqm-google-signin/readme.md | 30 +++++++ .../sqm-google-signin/sqm-google-signin.tsx | 83 +++++++++++++++++++ .../src/components/sqm-portal-login/readme.md | 29 ++++--- .../sqm-portal-login-view.tsx | 2 + .../sqm-portal-login/sqm-portal-login.tsx | 5 ++ .../src/components/sqm-stencilbook/readme.md | 1 + 7 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 packages/mint-components/src/components/sqm-google-signin/readme.md create mode 100644 packages/mint-components/src/components/sqm-google-signin/sqm-google-signin.tsx diff --git a/packages/mint-components/src/components.d.ts b/packages/mint-components/src/components.d.ts index 2ace1882bd..45fed81fe7 100644 --- a/packages/mint-components/src/components.d.ts +++ b/packages/mint-components/src/components.d.ts @@ -754,6 +754,9 @@ export namespace Components { */ "type": string; } + interface SqmGoogleSignin { + "nextPage": string; + } interface SqmGraphqlClientProvider { /** * @uiName Domain @@ -2023,6 +2026,7 @@ export namespace Components { * @uiWidget pageSelect */ "registerPath": string; + "showGoogleSignIn": boolean; /** * @uiName Submit button text */ @@ -5231,6 +5235,12 @@ declare global { prototype: HTMLSqmFormMessageElement; new (): HTMLSqmFormMessageElement; }; + interface HTMLSqmGoogleSigninElement extends Components.SqmGoogleSignin, HTMLStencilElement { + } + var HTMLSqmGoogleSigninElement: { + prototype: HTMLSqmGoogleSigninElement; + new (): HTMLSqmGoogleSigninElement; + }; interface HTMLSqmGraphqlClientProviderElement extends Components.SqmGraphqlClientProvider, HTMLStencilElement { } var HTMLSqmGraphqlClientProviderElement: { @@ -5800,6 +5810,7 @@ declare global { "sqm-edit-profile": HTMLSqmEditProfileElement; "sqm-empty": HTMLSqmEmptyElement; "sqm-form-message": HTMLSqmFormMessageElement; + "sqm-google-signin": HTMLSqmGoogleSigninElement; "sqm-graphql-client-provider": HTMLSqmGraphqlClientProviderElement; "sqm-header-logo": HTMLSqmHeaderLogoElement; "sqm-hero": HTMLSqmHeroElement; @@ -6600,6 +6611,9 @@ declare namespace LocalJSX { */ "type"?: string; } + interface SqmGoogleSignin { + "nextPage"?: string; + } interface SqmGraphqlClientProvider { /** * @uiName Domain @@ -7863,6 +7877,7 @@ declare namespace LocalJSX { * @uiWidget pageSelect */ "registerPath"?: string; + "showGoogleSignIn"?: boolean; /** * @uiName Submit button text */ @@ -10965,6 +10980,7 @@ declare namespace LocalJSX { "sqm-edit-profile": SqmEditProfile; "sqm-empty": SqmEmpty; "sqm-form-message": SqmFormMessage; + "sqm-google-signin": SqmGoogleSignin; "sqm-graphql-client-provider": SqmGraphqlClientProvider; "sqm-header-logo": SqmHeaderLogo; "sqm-hero": SqmHero; @@ -11079,6 +11095,7 @@ declare module "@stencil/core" { "sqm-edit-profile": LocalJSX.SqmEditProfile & JSXBase.HTMLAttributes; "sqm-empty": LocalJSX.SqmEmpty & JSXBase.HTMLAttributes; "sqm-form-message": LocalJSX.SqmFormMessage & JSXBase.HTMLAttributes; + "sqm-google-signin": LocalJSX.SqmGoogleSignin & JSXBase.HTMLAttributes; "sqm-graphql-client-provider": LocalJSX.SqmGraphqlClientProvider & JSXBase.HTMLAttributes; "sqm-header-logo": LocalJSX.SqmHeaderLogo & JSXBase.HTMLAttributes; "sqm-hero": LocalJSX.SqmHero & JSXBase.HTMLAttributes; diff --git a/packages/mint-components/src/components/sqm-google-signin/readme.md b/packages/mint-components/src/components/sqm-google-signin/readme.md new file mode 100644 index 0000000000..e4fab4960c --- /dev/null +++ b/packages/mint-components/src/components/sqm-google-signin/readme.md @@ -0,0 +1,30 @@ +# sqm-google-signin + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------- | ----------- | ----------- | -------- | ------- | +| `nextPage` | `next-page` | | `string` | `"/"` | + + +## Dependencies + +### Used by + + - [sqm-portal-login](../sqm-portal-login) + +### Graph +```mermaid +graph TD; + sqm-portal-login --> sqm-google-signin + style sqm-google-signin fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/mint-components/src/components/sqm-google-signin/sqm-google-signin.tsx b/packages/mint-components/src/components/sqm-google-signin/sqm-google-signin.tsx new file mode 100644 index 0000000000..d36cb69b7b --- /dev/null +++ b/packages/mint-components/src/components/sqm-google-signin/sqm-google-signin.tsx @@ -0,0 +1,83 @@ +import { Component, h, Prop, getElement } from "@stencil/core"; +import { withHooks } from "@saasquatch/stencil-hooks"; +import { useEffect, useState } from "@saasquatch/universal-hooks"; +import { + navigation, + useAuthenticateWithGoogleMutation, +} from "@saasquatch/component-boilerplate"; +import { sanitizeUrlPath } from "../../utils/utils"; + +interface CredentialResponse { + credential: string; +} + +@Component({ + tag: "sqm-google-signin", + shadow: true, +}) +export class GoogleSignIn { + @Prop() + nextPage: string = "/"; + + constructor() { + withHooks(this); + } + disconnectedCallback() {} + + render() { + const [googleButtonDiv, setGoogleButtonDiv] = + useState(undefined); + + const [request, { loading, errors, data }] = + useAuthenticateWithGoogleMutation(); + + // TODO: Error handling and nextPage url parameter + + useEffect(() => { + // DOM not ready + if (!googleButtonDiv) return; + + const handleCredentialResponse = async (response: CredentialResponse) => { + const result = await request({ idToken: response.credential }); + + if (result instanceof Error) { + // TODO: Handle errors + return; + } + + if (result.authenticateManagedIdentityWithGoogle?.token) { + const url = sanitizeUrlPath(this.nextPage); + navigation.push(url.href); + } + }; + + // See https://developers.google.com/identity/gsi/web/guides/personalized-button + // for documentation on this Google button + + // @ts-ignore + google.accounts.id.initialize({ + // TODO: Get the client ID from SquatchPortal window config or similar + client_id: + "190322867385-elcm8vorp3vk2etknmv6irns3v8bo4mh.apps.googleusercontent.com", + callback: handleCredentialResponse, + }); + + // Button configuration options are here: + // https://developers.google.com/identity/gsi/web/reference/js-reference#GsiButtonConfiguration + + // @ts-ignore + google.accounts.id.renderButton( + googleButtonDiv, + { theme: "outline", size: "large" } // customization attributes + ); + + // @ts-ignore + // google.accounts.id.prompt(); // also display the One Tap dialog + + // @ts-ignore + //getElement(this).appendChild(div); + }, [googleButtonDiv]); + + return
; + } +} diff --git a/packages/mint-components/src/components/sqm-portal-login/readme.md b/packages/mint-components/src/components/sqm-portal-login/readme.md index b503321404..c6a8093d6d 100644 --- a/packages/mint-components/src/components/sqm-portal-login/readme.md +++ b/packages/mint-components/src/components/sqm-portal-login/readme.md @@ -7,19 +7,20 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| --------------------- | ----------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `demoData` | -- | | `{ states?: { error: string; loading: boolean; forgotPasswordPath: string; registerPath: string; }; content?: { forgotPasswordButton?: any; secondaryButton?: any; emailLabel?: string; passwordLabel?: string; submitLabel?: string; pageLabel?: string; }; }` | `undefined` | -| `emailLabel` | `email-label` | | `string` | `"Email"` | -| `forgotPasswordLabel` | `forgot-password-label` | | `string` | `"Forgot Password?"` | -| `forgotPasswordPath` | `forgot-password-path` | Redirect participants to this page to reset their password | `string` | `"/forgotPassword"` | -| `networkErrorMessage` | `network-error-message` | | `string` | `"An error occurred while logging you in. Please refresh the page and try again."` | -| `nextPage` | `next-page` | Redirect participants to this page after they successfully login. | `string` | `"/"` | -| `pageLabel` | `page-label` | | `string` | `"Sign in to your account"` | -| `passwordLabel` | `password-label` | | `string` | `"Password"` | -| `registerLabel` | `register-label` | | `string` | `"Register"` | -| `registerPath` | `register-path` | Redirect participants to this page to start registration. | `string` | `"/register"` | -| `submitLabel` | `submit-label` | | `string` | `"Sign In"` | +| Property | Attribute | Description | Type | Default | +| --------------------- | ----------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `demoData` | -- | | `{ states?: { error: string; loading: boolean; forgotPasswordPath: string; registerPath: string; }; content?: { forgotPasswordButton?: any; secondaryButton?: any; emailLabel?: string; passwordLabel?: string; submitLabel?: string; pageLabel?: string; googleSignInButton?: string; }; }` | `undefined` | +| `emailLabel` | `email-label` | | `string` | `"Email"` | +| `forgotPasswordLabel` | `forgot-password-label` | | `string` | `"Forgot Password?"` | +| `forgotPasswordPath` | `forgot-password-path` | Redirect participants to this page to reset their password | `string` | `"/forgotPassword"` | +| `networkErrorMessage` | `network-error-message` | | `string` | `"An error occurred while logging you in. Please refresh the page and try again."` | +| `nextPage` | `next-page` | Redirect participants to this page after they successfully login. | `string` | `"/"` | +| `pageLabel` | `page-label` | | `string` | `"Sign in to your account"` | +| `passwordLabel` | `password-label` | | `string` | `"Password"` | +| `registerLabel` | `register-label` | | `string` | `"Register"` | +| `registerPath` | `register-path` | Redirect participants to this page to start registration. | `string` | `"/register"` | +| `showGoogleSignIn` | `show-google-sign-in` | | `boolean` | `false` | +| `submitLabel` | `submit-label` | | `string` | `"Sign In"` | ## Dependencies @@ -30,11 +31,13 @@ ### Depends on +- [sqm-google-signin](../sqm-google-signin) - [sqm-form-message](../sqm-form-message) ### Graph ```mermaid graph TD; + sqm-portal-login --> sqm-google-signin sqm-portal-login --> sqm-form-message sqm-stencilbook --> sqm-portal-login style sqm-portal-login fill:#f9f,stroke:#333,stroke-width:4px diff --git a/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login-view.tsx b/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login-view.tsx index a6710d4cfb..f3dab89907 100644 --- a/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login-view.tsx +++ b/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login-view.tsx @@ -24,6 +24,7 @@ export interface PortalLoginViewProps { passwordLabel?: string; submitLabel?: string; pageLabel?: string; + googleSignInButton?: string; }; } @@ -99,6 +100,7 @@ export function PortalLoginView(props: PortalLoginViewProps) { {content.submitLabel || "Login"} {content.secondaryButton} + {content.googleSignInButton} diff --git a/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login.tsx b/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login.tsx index bc71b2063b..a8bbbafc70 100644 --- a/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login.tsx +++ b/packages/mint-components/src/components/sqm-portal-login/sqm-portal-login.tsx @@ -89,6 +89,8 @@ export class PortalLogin { */ @Prop() demoData?: DemoData; + @Prop() showGoogleSignIn: boolean = false; + constructor() { withHooks(this); } @@ -122,6 +124,9 @@ export class PortalLogin { passwordLabel: this.passwordLabel, submitLabel: this.submitLabel, pageLabel: this.pageLabel, + googleSignInButton: this.showGoogleSignIn ? ( + + ) : undefined, }; return ( sqm-empty sqm-referral-table --> sqm-table-row sqm-referral-table --> sqm-table-cell + sqm-portal-login --> sqm-google-signin sqm-portal-login --> sqm-form-message sqm-portal-change-password --> sqm-form-message sqm-portal-change-password --> sqm-password-field