diff --git a/package-lock.json b/package-lock.json index a28ef3b1..01a473d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@capacitor/network": "^8.0.1", "@capacitor/preferences": "^8.0.1", "@capacitor/status-bar": "^8.0.2", + "@capawesome/capacitor-google-sign-in": "^0.1.4", "@capawesome/capacitor-live-update": "^8.3.0", "@eslint/js": "^9.39.4", "@ionic/angular-toolkit": "^13.0.0", @@ -83,7 +84,7 @@ }, "dist/photo-editor": { "name": "@rdlabo/ionic-angular-photo-editor", - "version": "22.0.0-0", + "version": "22.0.0-8", "dependencies": { "tslib": "^2.3.0" }, @@ -114,7 +115,7 @@ }, "dist/scroll-header": { "name": "@rdlabo/ionic-angular-scroll-header", - "version": "22.0.0-0", + "version": "22.0.0-8", "dependencies": { "tslib": "^2.3.0" }, @@ -127,7 +128,7 @@ }, "dist/scroll-strategies": { "name": "@rdlabo/ngx-cdk-scroll-strategies", - "version": "22.0.0-0", + "version": "22.0.0-8", "dependencies": { "tslib": "^2.3.0" }, @@ -2773,6 +2774,26 @@ "@capacitor/core": ">=8.0.0" } }, + "node_modules/@capawesome/capacitor-google-sign-in": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@capawesome/capacitor-google-sign-in/-/capacitor-google-sign-in-0.1.4.tgz", + "integrity": "sha512-+8+3mfWN1B0+qB5r1xUZpy0hrYkpEDD5XHxa5LQ5wwDIVKoFLj/orYGRf/RrsB1AbkSpRfZZ1N2U7C3gPcjAdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/capawesome-team/" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/capawesome" + } + ], + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capawesome/capacitor-live-update": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/@capawesome/capacitor-live-update/-/capacitor-live-update-8.3.0.tgz", diff --git a/package.json b/package.json index b59b6652..e5fcae1b 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@capacitor/network": "^8.0.1", "@capacitor/preferences": "^8.0.1", "@capacitor/status-bar": "^8.0.2", + "@capawesome/capacitor-google-sign-in": "^0.1.4", "@capawesome/capacitor-live-update": "^8.3.0", "@eslint/js": "^9.39.4", "@ionic/angular-toolkit": "^13.0.0", @@ -90,8 +91,8 @@ "angular-eslint": "^22.1.0", "child_process": "^1.0.2", "dom-to-image-more": "^3.10.0", - "eslint": "^9.39.4", "esbuild": "^0.28.2", + "eslint": "^9.39.4", "firebase": "^11.10.0", "husky": "^8.0.3", "ionicons": "^8.1.0", diff --git a/projects/kit/README.md b/projects/kit/README.md index 30158726..51929aac 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -29,7 +29,8 @@ import { disableHandler, KitStorageService } from '@rdlabo/ionic-angular-kit'; @Component({ selector: 'app-preferences-demo', imports: [IonButton], - template: `Save preference

{{ result() }}

`, + template: `Save preference +

{{ result() }}

`, }) export class PreferencesDemo { private readonly storage = inject(KitStorageService); @@ -59,19 +60,23 @@ The core package declares `@ionic/storage-angular` and Capacitor Core, App, Hapt Firebase, social login, Live Update, Preferences, Status Bar, in-app review, and printer/PDF dependencies are optional feature peers. Install only the dependencies used by the selected secondary entry points and follow each plugin's own compatibility range; some optional plugins support only Capacitor 8. +The `/auth-firebase/google` entry point uses `@capawesome/capacitor-google-sign-in` 0.1.x and therefore requires Capacitor 8. Applications remaining on Capacitor 7 can continue to use the core Kit and other compatible entry points, but cannot use this Google entry point. + ## Entry points -| Import | Responsibility | -| ----------------------------------------- | ---------------------------------------------------------------------------------- | -| `@rdlabo/ionic-angular-kit` | Storage, overlays, guards, HTTP, realtime, directives, keyboard, and utilities | -| `@rdlabo/ionic-angular-kit/offline` | **Experimental.** Scoped local replica, outbox, pull, replay, and request policies | -| `@rdlabo/ionic-angular-kit/theme` | Persisted light/dark theme and native status bar sync | -| `@rdlabo/ionic-angular-kit/forms` | Ionic error text and state classes for Angular Signal Forms | -| `@rdlabo/ionic-angular-kit/review` | Throttled native in-app review requests | -| `@rdlabo/ionic-angular-kit/printer` | DOM-to-PNG, Brother label, and PDF helpers | -| `@rdlabo/ionic-angular-kit/auth-firebase` | Firebase dependency wiring and authentication flows | -| `@rdlabo/ionic-angular-kit/app-update` | Atomic Angular service-worker update transitions | -| `@rdlabo/ionic-angular-kit/live-update` | Capawesome Live Update readiness provider | +| Import | Responsibility | +| ------------------------------------------------ | ---------------------------------------------------------------------------------- | +| `@rdlabo/ionic-angular-kit` | Storage, overlays, guards, HTTP, realtime, directives, keyboard, and utilities | +| `@rdlabo/ionic-angular-kit/offline` | **Experimental.** Scoped local replica, outbox, pull, replay, and request policies | +| `@rdlabo/ionic-angular-kit/theme` | Persisted light/dark theme and native status bar sync | +| `@rdlabo/ionic-angular-kit/forms` | Ionic error text and state classes for Angular Signal Forms | +| `@rdlabo/ionic-angular-kit/review` | Throttled native in-app review requests | +| `@rdlabo/ionic-angular-kit/printer` | DOM-to-PNG, Brother label, and PDF helpers | +| `@rdlabo/ionic-angular-kit/auth-firebase` | Firebase dependency wiring and authentication flows | +| `@rdlabo/ionic-angular-kit/auth-firebase/google` | Google popup/native sign-in, Firebase session linking, and logout | +| `@rdlabo/ionic-angular-kit/auth-firebase/social` | Apple and Facebook Firebase social-auth helpers | +| `@rdlabo/ionic-angular-kit/app-update` | Atomic Angular service-worker update transitions | +| `@rdlabo/ionic-angular-kit/live-update` | Capawesome Live Update readiness provider | Secondary entry points isolate optional native and SDK dependencies from the core bundle. diff --git a/projects/kit/auth-firebase/google/ng-package.json b/projects/kit/auth-firebase/google/ng-package.json new file mode 100644 index 00000000..d32ba70b --- /dev/null +++ b/projects/kit/auth-firebase/google/ng-package.json @@ -0,0 +1,3 @@ +{ + "lib": { "entryFile": "src/public-api.ts" } +} diff --git a/projects/kit/auth-firebase/google/src/kit-google.spec.ts b/projects/kit/auth-firebase/google/src/kit-google.spec.ts new file mode 100644 index 00000000..f1ce9987 --- /dev/null +++ b/projects/kit/auth-firebase/google/src/kit-google.spec.ts @@ -0,0 +1,239 @@ +import type { Auth } from 'firebase/auth'; +import { kitGoogleLogin, kitGoogleLogout } from './kit-google'; + +const initialize = vi.fn(); +const nativeSignIn = vi.fn(); +const signInWithCredential = vi.fn(); +const signInWithPopup = vi.fn(); +const linkWithCredential = vi.fn(); +const linkWithPopup = vi.fn(); +const reauthenticateWithCredential = vi.fn(); +const reauthenticateWithPopup = vi.fn(); +const signOut = vi.fn(); +const getPlatform = vi.fn(); + +vi.mock('@capawesome/capacitor-google-sign-in', () => ({ + ErrorCode: { SignInCanceled: 'SIGN_IN_CANCELED' }, + GoogleSignIn: { initialize: (...a: unknown[]) => initialize(...a), signIn: () => nativeSignIn(), signOut: () => signOut() }, +})); +vi.mock('@capacitor/core', () => ({ Capacitor: { getPlatform: () => getPlatform() } })); +vi.mock('firebase/auth', () => ({ + signInWithCredential: (...a: unknown[]) => signInWithCredential(...a), + signInWithPopup: (...a: unknown[]) => signInWithPopup(...a), + linkWithCredential: (...a: unknown[]) => linkWithCredential(...a), + linkWithPopup: (...a: unknown[]) => linkWithPopup(...a), + reauthenticateWithCredential: (...a: unknown[]) => reauthenticateWithCredential(...a), + reauthenticateWithPopup: (...a: unknown[]) => reauthenticateWithPopup(...a), + EmailAuthProvider: { credential: (email: string, password: string) => ({ email, password }) }, + GoogleAuthProvider: class { + static credential(idToken: string) { + return { idToken }; + } + static credentialFromResult(result: { idToken?: string }) { + return result.idToken ? { idToken: result.idToken } : null; + } + }, +})); + +const authWith = (user: unknown): Auth => ({ currentUser: user }) as Auth; +afterEach(() => vi.clearAllMocks()); + +it('uses Firebase popup on web and exchanges the ID token', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'web' }; + signInWithPopup.mockResolvedValue({ user, idToken: 'web-token' }); + const exchange = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'new', clientId: 'client', exchange })).resolves.toEqual({ status: true }); + expect(exchange).toHaveBeenCalledWith('web-token', user); +}); + +it('uses the native plugin token to establish the Firebase session', async () => { + getPlatform.mockReturnValue('ios'); + initialize.mockResolvedValue(undefined); + nativeSignIn.mockResolvedValue({ idToken: 'native-token' }); + const user = { uid: 'native' }; + signInWithCredential.mockResolvedValue({ user }); + const exchange = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'new', clientId: 'client', exchange })).resolves.toEqual({ status: true }); + expect(exchange).toHaveBeenCalledWith('native-token', user); +}); + +it('links Google to the current user on web and exchanges its token', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'linked' }; + linkWithPopup.mockResolvedValue({ user, idToken: 'link-token' }); + const exchange = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'link', clientId: 'client', exchange })).resolves.toEqual({ status: true }); + expect(linkWithPopup).toHaveBeenCalledWith(user, expect.anything()); + expect(exchange).toHaveBeenCalledWith('link-token', user); +}); + +it('links Google to the current user on native and exchanges its token', async () => { + getPlatform.mockReturnValue('android'); + initialize.mockResolvedValue(undefined); + nativeSignIn.mockResolvedValue({ idToken: 'native-link-token' }); + const user = { uid: 'native-linked' }; + linkWithCredential.mockResolvedValue({ user }); + const exchange = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'link', clientId: 'native-link-client', exchange })).resolves.toEqual({ + status: true, + }); + expect(linkWithCredential).toHaveBeenCalledWith(user, { idToken: 'native-link-token' }); + expect(exchange).toHaveBeenCalledWith('native-link-token', user); +}); + +it('reauthenticates and links a password in web credential mode without token exchange', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'credential-web' }; + reauthenticateWithPopup.mockResolvedValue({ user }); + linkWithCredential.mockResolvedValue({ user }); + const exchange = vi.fn(); + await expect( + kitGoogleLogin(authWith(user), { + mode: 'credential', + clientId: 'client', + emailLogin: { email: 'user@example.com', password: 'password' }, + exchange, + }), + ).resolves.toEqual({ status: true }); + expect(reauthenticateWithPopup).toHaveBeenCalledWith(user, expect.anything()); + expect(linkWithCredential).toHaveBeenCalledWith(user, { email: 'user@example.com', password: 'password' }); + expect(exchange).not.toHaveBeenCalled(); +}); + +it('reauthenticates and links a password in native credential mode without token exchange', async () => { + getPlatform.mockReturnValue('android'); + initialize.mockResolvedValue(undefined); + nativeSignIn.mockResolvedValue({ idToken: 'credential-token' }); + const user = { uid: 'credential-native' }; + reauthenticateWithCredential.mockResolvedValue({ user }); + linkWithCredential.mockResolvedValue({ user }); + const exchange = vi.fn(); + await expect( + kitGoogleLogin(authWith(user), { + mode: 'credential', + clientId: 'credential-client', + emailLogin: { email: 'user@example.com', password: 'password' }, + exchange, + }), + ).resolves.toEqual({ status: true }); + expect(reauthenticateWithCredential).toHaveBeenCalledWith(user, { idToken: 'credential-token' }); + expect(linkWithCredential).toHaveBeenCalledWith(user, { email: 'user@example.com', password: 'password' }); + expect(exchange).not.toHaveBeenCalled(); +}); + +it('reauthenticates the current native user without linking or exchange', async () => { + getPlatform.mockReturnValue('ios'); + initialize.mockResolvedValue(undefined); + nativeSignIn.mockResolvedValue({ idToken: 'reauth-token' }); + const user = { uid: 'reauthenticated' }; + reauthenticateWithCredential.mockResolvedValue({ user }); + const success = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'reauthenticate', clientId: 'reauth-client', success })).resolves.toEqual({ + status: true, + }); + expect(reauthenticateWithCredential).toHaveBeenCalledWith(user, { idToken: 'reauth-token' }); + expect(linkWithCredential).not.toHaveBeenCalled(); + expect(success).toHaveBeenCalledWith({ idToken: 'reauth-token', mode: 'reauthenticate', user }); +}); + +it('reauthenticates the current web user without linking or exchange', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'web-reauthenticated' }; + reauthenticateWithPopup.mockResolvedValue({ user, idToken: 'web-reauth-token' }); + const success = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'reauthenticate', clientId: 'client', success })).resolves.toEqual({ + status: true, + }); + expect(success).toHaveBeenCalledWith({ idToken: 'web-reauth-token', mode: 'reauthenticate', user }); + expect(linkWithCredential).not.toHaveBeenCalled(); +}); + +it('keeps the Firebase user when backend exchange rejects', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'session' }; + const auth = authWith(user); + signInWithPopup.mockResolvedValue({ user, idToken: 'web-token' }); + const failure = new Error('backend rejected'); + const error = vi.fn(); + await expect(kitGoogleLogin(auth, { mode: 'new', clientId: 'client', exchange: () => Promise.reject(failure), error })).resolves.toEqual({ + status: false, + }); + expect(auth.currentUser).toBe(user); + expect(error).toHaveBeenCalledWith('other', failure); +}); + +it('rejects a missing popup ID token', async () => { + getPlatform.mockReturnValue('web'); + const user = { uid: 'missing-token' }; + signInWithPopup.mockResolvedValue({ user }); + const error = vi.fn(); + await expect(kitGoogleLogin(authWith(user), { mode: 'new', clientId: 'client', error })).resolves.toEqual({ status: false }); + expect(error).toHaveBeenCalledWith('other', expect.objectContaining({ message: 'kit Google login: ID token is missing' })); +}); + +it('rejects a Firebase user change before exchange', async () => { + getPlatform.mockReturnValue('web'); + const original = { uid: 'original' }; + const changed = { uid: 'changed' }; + const auth = authWith(original); + signInWithPopup.mockImplementation(async () => { + (auth as unknown as { currentUser: unknown }).currentUser = changed; + return { user: original, idToken: 'web-token' }; + }); + const exchange = vi.fn(); + await expect(kitGoogleLogin(auth, { mode: 'new', clientId: 'client', exchange })).resolves.toEqual({ status: false }); + expect(exchange).not.toHaveBeenCalled(); +}); + +it('does not link a password when the current user changes during reauthentication', async () => { + getPlatform.mockReturnValue('web'); + const original = { uid: 'original-credential' }; + const auth = authWith(original); + reauthenticateWithPopup.mockImplementation(async () => { + (auth as unknown as { currentUser: unknown }).currentUser = { uid: 'changed-credential' }; + return { user: original }; + }); + await expect( + kitGoogleLogin(auth, { + mode: 'credential', + clientId: 'client', + emailLogin: { email: 'user@example.com', password: 'password' }, + }), + ).resolves.toEqual({ status: false }); + expect(linkWithCredential).not.toHaveBeenCalled(); +}); + +it('retries native initialization after an initialization failure', async () => { + getPlatform.mockReturnValue('ios'); + initialize.mockRejectedValueOnce(new Error('init failed')).mockResolvedValueOnce(undefined); + nativeSignIn.mockResolvedValue({ idToken: 'retry-token' }); + const user = { uid: 'retry' }; + signInWithCredential.mockResolvedValue({ user }); + const options = { mode: 'new' as const, clientId: 'retry-client' }; + await expect(kitGoogleLogin(authWith(user), options)).resolves.toEqual({ status: false }); + await expect(kitGoogleLogin(authWith(user), options)).resolves.toEqual({ status: true }); + expect(initialize).toHaveBeenCalledTimes(2); +}); + +it('classifies popup cancellation', async () => { + getPlatform.mockReturnValue('web'); + signInWithPopup.mockRejectedValue(Object.assign(new Error('cancelled'), { code: 'auth/popup-closed-by-user' })); + const error = vi.fn(); + await expect(kitGoogleLogin(authWith(null), { mode: 'new', clientId: 'client', error })).resolves.toEqual({ status: false }); + expect(error).toHaveBeenCalledWith('cancelled', expect.anything()); +}); + +it('classifies native cancellation', async () => { + getPlatform.mockReturnValue('android'); + initialize.mockResolvedValue(undefined); + nativeSignIn.mockRejectedValue(Object.assign(new Error('cancelled'), { code: 'SIGN_IN_CANCELED' })); + const error = vi.fn(); + await expect(kitGoogleLogin(authWith(null), { mode: 'new', clientId: 'cancel-client', error })).resolves.toEqual({ status: false }); + expect(error).toHaveBeenCalledWith('cancelled', expect.anything()); +}); + +it('makes plugin logout best effort', async () => { + signOut.mockRejectedValue(new Error('already signed out')); + await expect(kitGoogleLogout()).resolves.toBeUndefined(); +}); diff --git a/projects/kit/auth-firebase/google/src/kit-google.ts b/projects/kit/auth-firebase/google/src/kit-google.ts new file mode 100644 index 00000000..1c36590f --- /dev/null +++ b/projects/kit/auth-firebase/google/src/kit-google.ts @@ -0,0 +1,143 @@ +import { ErrorCode, GoogleSignIn } from '@capawesome/capacitor-google-sign-in'; +import { Capacitor } from '@capacitor/core'; +import type { Auth, User } from 'firebase/auth'; +import { + EmailAuthProvider, + GoogleAuthProvider, + linkWithCredential, + linkWithPopup, + reauthenticateWithCredential, + reauthenticateWithPopup, + signInWithCredential, + signInWithPopup, +} from 'firebase/auth'; + +export type KitGoogleLoginMode = + | { mode: 'new' } + | { mode: 'link' } + | { mode: 'reauthenticate' } + | { mode: 'credential'; emailLogin: { email: string; password: string } }; +export type KitGoogleErrorCategory = 'already-in-use' | 'cancelled' | 'other'; +export type KitGoogleLoginOptions = KitGoogleLoginMode & { + clientId: string; + before?: () => void | Promise; + exchange?: (idToken: string, user: User) => void | Promise; + success?: (info: { idToken: string; mode: KitGoogleLoginMode['mode']; user: User }) => void | Promise; + error?: (category: KitGoogleErrorCategory, error: unknown) => void | Promise; + finally?: () => void | Promise; +}; + +let initialization: { clientId: string; promise: Promise } | undefined; +const initialize = (clientId: string): Promise => { + if (!clientId) { + return Promise.reject(new Error('kit Google login: client ID is not configured')); + } + if (initialization?.clientId === clientId) return initialization.promise; + const promise = GoogleSignIn.initialize({ clientId }).catch((error: unknown) => { + if (initialization?.promise === promise) initialization = undefined; + throw error; + }); + initialization = { clientId, promise }; + return promise; +}; + +const requireUser = (auth: Auth): User => { + if (!auth.currentUser) throw new Error('kit Google login: no signed-in Firebase user'); + return auth.currentUser; +}; + +const assertCurrentUser = (auth: Auth, user: User): void => { + if (auth.currentUser !== user) { + throw new Error('kit Google login: Firebase user changed'); + } +}; + +const classify = (error: unknown): KitGoogleErrorCategory => { + const code = (error as { code?: string } | null)?.code; + if (code === 'auth/credential-already-in-use') return 'already-in-use'; + if ( + code === ErrorCode.SignInCanceled || + code === 'auth/user-cancelled' || + code === 'auth/popup-closed-by-user' || + code === 'auth/cancelled-popup-request' + ) + return 'cancelled'; + return 'other'; +}; + +export const kitGoogleLogin = async (auth: Auth, options: KitGoogleLoginOptions): Promise<{ status: boolean }> => { + const execute = async (): Promise => { + await options.before?.(); + const expectedUser = auth.currentUser; + let idToken: string; + let user: User; + if (Capacitor.getPlatform() === 'web') { + const provider = new GoogleAuthProvider(); + if (options.mode === 'reauthenticate') { + user = requireUser(auth); + const result = await reauthenticateWithPopup(user, provider); + idToken = GoogleAuthProvider.credentialFromResult(result)?.idToken ?? ''; + assertCurrentUser(auth, user); + await options.success?.({ idToken, mode: options.mode, user }); + assertCurrentUser(auth, user); + return; + } + if (options.mode === 'credential') { + user = requireUser(auth); + await reauthenticateWithPopup(user, provider); + assertCurrentUser(auth, user); + await linkWithCredential(user, EmailAuthProvider.credential(options.emailLogin.email, options.emailLogin.password)); + assertCurrentUser(auth, user); + await options.success?.({ idToken: '', mode: options.mode, user }); + assertCurrentUser(auth, user); + return; + } + const result = options.mode === 'new' ? await signInWithPopup(auth, provider) : await linkWithPopup(requireUser(auth), provider); + const credential = GoogleAuthProvider.credentialFromResult(result); + if (!credential?.idToken) throw new Error('kit Google login: ID token is missing'); + idToken = credential.idToken; + user = result.user; + } else { + await initialize(options.clientId); + idToken = (await GoogleSignIn.signIn()).idToken; + const credential = GoogleAuthProvider.credential(idToken); + if (options.mode === 'new') user = (await signInWithCredential(auth, credential)).user; + else { + user = requireUser(auth); + if (user !== expectedUser) throw new Error('kit Google login: Firebase user changed'); + if (options.mode === 'link') await linkWithCredential(user, credential); + else if (options.mode === 'reauthenticate') { + await reauthenticateWithCredential(user, credential); + assertCurrentUser(auth, user); + await options.success?.({ idToken, mode: options.mode, user }); + assertCurrentUser(auth, user); + return; + } else { + await reauthenticateWithCredential(user, credential); + assertCurrentUser(auth, user); + await linkWithCredential(user, EmailAuthProvider.credential(options.emailLogin.email, options.emailLogin.password)); + assertCurrentUser(auth, user); + await options.success?.({ idToken, mode: options.mode, user }); + assertCurrentUser(auth, user); + return; + } + } + } + assertCurrentUser(auth, user); + await options.exchange?.(idToken, user); + assertCurrentUser(auth, user); + await options.success?.({ idToken, mode: options.mode, user }); + assertCurrentUser(auth, user); + }; + return execute() + .then( + () => ({ status: true }), + async (error: unknown) => { + await options.error?.(classify(error), error); + return { status: false }; + }, + ) + .finally(() => options.finally?.()); +}; + +export const kitGoogleLogout = (): Promise => GoogleSignIn.signOut().catch(() => undefined); diff --git a/projects/kit/auth-firebase/google/src/public-api.ts b/projects/kit/auth-firebase/google/src/public-api.ts new file mode 100644 index 00000000..47c4a0de --- /dev/null +++ b/projects/kit/auth-firebase/google/src/public-api.ts @@ -0,0 +1 @@ +export * from './kit-google'; diff --git a/projects/kit/auth-firebase/social/src/kit-social.spec.ts b/projects/kit/auth-firebase/social/src/kit-social.spec.ts index fe1e61ba..30db434a 100644 --- a/projects/kit/auth-firebase/social/src/kit-social.spec.ts +++ b/projects/kit/auth-firebase/social/src/kit-social.spec.ts @@ -248,7 +248,7 @@ describe('kitAppleLogin', () => { signInWithPopup.mockRejectedValueOnce(fbError('auth/popup-closed-by-user')); const h2 = hooks(); expect(await kitAppleLogin(authWith(null), { mode: 'new', ...h2 })).toEqual({ status: false }); - expect(h2.error).toHaveBeenCalledWith('other', expect.anything()); + expect(h2.error).toHaveBeenCalledWith('cancelled', expect.anything()); expect(h2.finally).toHaveBeenCalledTimes(1); }); }); diff --git a/projects/kit/auth-firebase/social/src/kit-social.ts b/projects/kit/auth-firebase/social/src/kit-social.ts index 604c26ac..16847d45 100644 --- a/projects/kit/auth-firebase/social/src/kit-social.ts +++ b/projects/kit/auth-firebase/social/src/kit-social.ts @@ -85,7 +85,7 @@ const classifyOAuthError = (e: unknown): KitOAuthErrorCategory => { if (code === 'auth/credential-already-in-use') { return 'already-in-use'; } - if (code === 'auth/user-cancelled') { + if (code === 'auth/user-cancelled' || code === 'auth/popup-closed-by-user' || code === 'auth/cancelled-popup-request') { return 'cancelled'; } return 'other'; diff --git a/projects/kit/package.json b/projects/kit/package.json index 7f3ba0fa..46930dbe 100644 --- a/projects/kit/package.json +++ b/projects/kit/package.json @@ -25,6 +25,7 @@ "@capacitor-community/in-app-review": ">=7.0.0 <9.0.0", "@capacitor-community/facebook-login": ">=8.0.0 <9.0.0", "@capacitor-community/apple-sign-in": "*", + "@capawesome/capacitor-google-sign-in": "^0.1.4", "@rdlabo/capacitor-brotherprint": ">=7.0.0 <9.0.0", "dom-to-image-more": "^3.0.0", "pdf-lib": "^1.17.1", @@ -56,6 +57,9 @@ "@capacitor-community/apple-sign-in": { "optional": true }, + "@capawesome/capacitor-google-sign-in": { + "optional": true + }, "@rdlabo/capacitor-brotherprint": { "optional": true }, diff --git a/projects/kit/tsconfig.spec.json b/projects/kit/tsconfig.spec.json index 81426124..29203c47 100644 --- a/projects/kit/tsconfig.spec.json +++ b/projects/kit/tsconfig.spec.json @@ -17,6 +17,7 @@ "forms/src/**/*.spec.ts", "offline/src/**/*.spec.ts", "auth-firebase/src/**/*.spec.ts", - "auth-firebase/social/src/**/*.spec.ts" + "auth-firebase/social/src/**/*.spec.ts", + "auth-firebase/google/src/**/*.spec.ts" ] }