From fa10d39e39d1e0e9309fe6eebc28a8d867b6b921 Mon Sep 17 00:00:00 2001
From: undefined <circle-github-actions@circle.com>
Date: Mon, 16 Dec 2024 10:14:01 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20synced=20local=20'src/'=20with?=
 =?UTF-8?q?=20remote=20'src/'?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/index.test.ts | 198 +++++++++++++++++++++++++++++++++++++++++++++-
 src/index.ts      |  31 ++++++--
 2 files changed, 222 insertions(+), 7 deletions(-)

diff --git a/src/index.test.ts b/src/index.test.ts
index cafb1b6..a5bba6a 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -16,14 +16,25 @@
 /* eslint-disable  @typescript-eslint/no-unsafe-argument */
 /* eslint-disable  @typescript-eslint/no-unsafe-member-access */
 /* eslint-disable  @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable  @typescript-eslint/no-unsafe-return */
 
 /**
  * @jest-environment jsdom
  */
+import { FirebaseError } from 'firebase/app'
+import * as firebaseAuth from 'firebase/auth'
+
+import { SocialLoginProvider } from './types'
 
 import { W3SSdk } from './'
 
-import type { ChallengeResult, Error } from './types'
+import type { ChallengeResult, Configs, Error } from './types'
+
+jest.mock('firebase/auth', () => ({
+  ...jest.requireActual('firebase/auth'),
+  signInWithPopup: jest.fn(),
+  getAuth: jest.fn(),
+}))
 
 describe('W3SSdk', () => {
   let sdk: W3SSdk
@@ -101,3 +112,188 @@ describe('W3SSdk', () => {
     )
   })
 })
+
+describe('W3SSdk > Apple OAuth', () => {
+  const localStorageMock = (() => {
+    let store: { [key: string]: string } = {}
+
+    return {
+      getItem(key: string): string {
+        return store[key] || ''
+      },
+      setItem(key: string, value: string): void {
+        store[key] = value
+      },
+      removeItem(key: string): void {
+        delete store[key]
+      },
+      clear(): void {
+        store = {}
+      },
+      length: 0,
+      key(index: number): string | null {
+        return Object.keys(store)[index] || null
+      },
+    }
+  })()
+
+  Object.defineProperty(window, 'localStorage', { value: localStorageMock })
+
+  let sdk: W3SSdk
+  const configs: Configs = {
+    appSettings: {
+      appId: 'test-app-id',
+    },
+    loginConfigs: {
+      deviceToken: 'device-token',
+      deviceEncryptionKey: 'device-encryption-key',
+      apple: {
+        apiKey: 'test-api-key',
+        authDomain: 'test-auth-domain',
+        projectId: 'test-project-id',
+        storageBucket: 'test-storage-bucket',
+        messagingSenderId: 'test-messaging-sender-id',
+        appId: 'test-app-id',
+      },
+    },
+  }
+
+  beforeEach(() => {
+    jest.clearAllMocks()
+    window.localStorage.clear()
+  })
+
+  it('should perform Apple login successfully', async () => {
+    const onLoginComplete = jest.fn()
+    sdk = new W3SSdk(configs, onLoginComplete)
+
+    const mockFirebaseApp = {}
+    Object.defineProperty(sdk, 'firebaseApp', {
+      get: jest.fn(() => mockFirebaseApp),
+      configurable: true,
+    })
+
+    // Mock signInWithPopup to resolve to a UserCredential
+    const userCredentialMock = {
+      user: { uid: 'test-uid' },
+      credential: { idToken: 'test-id-token' },
+    }
+    ;(firebaseAuth.signInWithPopup as jest.Mock).mockResolvedValueOnce(
+      userCredentialMock,
+    )
+
+    // Mock extractTokenFromResultAndSave to return true
+    const extractTokenSpy = jest
+      .spyOn(sdk as any, 'extractTokenFromResultAndSave')
+      .mockReturnValue(true)
+
+    // Mock verifyTokenViaService
+    const verifyTokenSpy = jest
+      .spyOn(sdk as any, 'verifyTokenViaService')
+      .mockImplementation(() => {})
+
+    await sdk.performLogin(SocialLoginProvider.APPLE)
+
+    expect(firebaseAuth.signInWithPopup).toHaveBeenCalled()
+    expect(extractTokenSpy).toHaveBeenCalledWith(userCredentialMock)
+    expect(verifyTokenSpy).toHaveBeenCalled()
+
+    expect(window.localStorage.getItem('socialLoginProvider')).toBe('')
+  })
+
+  it('should handle signInWithPopup error during Apple login', async () => {
+    const onLoginComplete = jest.fn()
+    sdk = new W3SSdk(configs, onLoginComplete)
+
+    // Simulate firebaseApp being initialized
+    const mockFirebaseApp = {}
+    Object.defineProperty(sdk, 'firebaseApp', {
+      get: jest.fn(() => mockFirebaseApp),
+      configurable: true,
+    })
+
+    // Mock getAuth
+    const mockAuth = { getProvider: jest.fn() }
+    ;(firebaseAuth.getAuth as jest.Mock).mockReturnValue(mockAuth)
+
+    // Mock signInWithPopup to reject
+    const error = new Error('sign in error')
+    ;(firebaseAuth.signInWithPopup as jest.Mock).mockRejectedValueOnce(error)
+
+    // Mock handleLoginFailure
+    const handleLoginFailureSpy = jest
+      .spyOn(sdk as any, 'handleLoginFailure')
+      .mockImplementation(() => {})
+
+    await sdk.performLogin(SocialLoginProvider.APPLE)
+
+    expect(firebaseAuth.signInWithPopup).toHaveBeenCalled()
+    expect(handleLoginFailureSpy).toHaveBeenCalled()
+  })
+
+  it('should not handle signInWithPopup auth/cancelled-popup-request error during Apple login', async () => {
+    const onLoginComplete = jest.fn()
+    sdk = new W3SSdk(configs, onLoginComplete)
+
+    // Simulate firebaseApp being initialized
+    const mockFirebaseApp = {}
+    Object.defineProperty(sdk, 'firebaseApp', {
+      get: jest.fn(() => mockFirebaseApp),
+      configurable: true,
+    })
+
+    // Mock getAuth
+    const mockAuth = { getProvider: jest.fn() }
+    ;(firebaseAuth.getAuth as jest.Mock).mockReturnValue(mockAuth)
+
+    // Mock signInWithPopup to reject
+    const error = new FirebaseError(
+      'auth/cancelled-popup-request',
+      'sign in error',
+    )
+    ;(firebaseAuth.signInWithPopup as jest.Mock).mockRejectedValueOnce(error)
+
+    // Mock handleLoginFailure
+    const handleLoginFailureSpy = jest
+      .spyOn(sdk as any, 'handleLoginFailure')
+      .mockImplementation(() => {})
+
+    await sdk.performLogin(SocialLoginProvider.APPLE)
+
+    expect(firebaseAuth.signInWithPopup).toHaveBeenCalled()
+    expect(handleLoginFailureSpy).toHaveBeenCalledTimes(0)
+  })
+
+  it('should not handle signInWithPopup auth/popup-closed-by-user error during Apple login', async () => {
+    const onLoginComplete = jest.fn()
+    sdk = new W3SSdk(configs, onLoginComplete)
+
+    // Simulate firebaseApp being initialized
+    const mockFirebaseApp = {}
+    Object.defineProperty(sdk, 'firebaseApp', {
+      get: jest.fn(() => mockFirebaseApp),
+      configurable: true,
+    })
+
+    // Mock getAuth
+    const mockAuth = { getProvider: jest.fn() }
+    ;(firebaseAuth.getAuth as jest.Mock).mockReturnValue(mockAuth)
+
+    // Mock signInWithPopup to reject
+    const error = new FirebaseError(
+      'auth/popup-closed-by-user',
+      'sign in error',
+    )
+    ;(firebaseAuth.signInWithPopup as jest.Mock).mockRejectedValueOnce(error)
+
+    // Mock handleLoginFailure
+    const handleLoginFailureSpy = jest
+      .spyOn(sdk as any, 'handleLoginFailure')
+      .mockImplementation(() => {})
+
+    await sdk.performLogin(SocialLoginProvider.APPLE)
+
+    expect(firebaseAuth.signInWithPopup).toHaveBeenCalled()
+    expect(handleLoginFailureSpy).toHaveBeenCalledTimes(0)
+  })
+})
diff --git a/src/index.ts b/src/index.ts
index 3b1ba36..6b432e9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { getApps, initializeApp } from 'firebase/app'
+import { FirebaseError, getApps, initializeApp } from 'firebase/app'
 import {
   OAuthProvider,
   getAuth,
   getRedirectResult,
-  signInWithRedirect,
+  signInWithPopup,
 } from 'firebase/auth'
 import { decode } from 'jsonwebtoken'
 import { v4 as uuidv4 } from 'uuid'
@@ -168,13 +168,13 @@ export class W3SSdk {
    * Performs social login.
    * @param provider - Social login provider.
    */
-  performLogin(provider: SocialLoginProvider): void {
+  async performLogin(provider: SocialLoginProvider): Promise<void> {
     if (provider === SocialLoginProvider.GOOGLE) {
       this.performGoogleLogin()
     } else if (provider === SocialLoginProvider.FACEBOOK) {
       this.performFacebookLogin()
     } else if (provider === SocialLoginProvider.APPLE) {
-      this.performAppleLogin()
+      await this.performAppleLogin()
     } else {
       void this.onLoginComplete?.(
         {
@@ -381,7 +381,7 @@ export class W3SSdk {
     }, 1000 * 10)
   }
 
-  private performAppleLogin() {
+  private async performAppleLogin() {
     if (!this.firebaseApp) {
       void this.onLoginComplete?.(
         {
@@ -398,7 +398,26 @@ export class W3SSdk {
     const provider = new OAuthProvider('apple.com')
     const auth = getAuth(this.firebaseApp)
 
-    void signInWithRedirect(auth, provider)
+    try {
+      const cred = await signInWithPopup(auth, provider)
+
+      if (!this.extractTokenFromResultAndSave(cred)) {
+        return
+      }
+
+      // Send the token to the verification service and reset the social login provider
+      this.verifyTokenViaService()
+      this.window.localStorage.setItem('socialLoginProvider', '')
+    } catch (error) {
+      if (
+        (error instanceof FirebaseError &&
+          error.code !== 'auth/cancelled-popup-request' &&
+          error.code !== 'auth/popup-closed-by-user') ||
+        !(error instanceof FirebaseError)
+      ) {
+        this.handleLoginFailure()
+      }
+    }
   }
 
   private performFacebookLogin() {