diff --git a/.env.template b/.env.template index 9f8a6d08bffe..444c5bf41d8b 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,7 @@ DRIVE_API_URL=http://drive-server:8000/api DRIVE_NEW_API_URL=http://drive-server-wip:3004/api +PAYMENTS_API_URL=http://payments-server:8003 +MEET_API_URL=http://meet-server:3006 CRYPTO_SECRET=6KYQBP847D4ATSFA MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023 MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e diff --git a/package-lock.json b/package-lock.json index dd7ae91bd055..2650a90fe783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/eslint-config-internxt": "^2.0.0", "@internxt/lib": "^1.2.0", - "@internxt/sdk": "=1.9.8", + "@internxt/sdk": "=1.9.13", "@internxt/ui": "^0.0.22", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", @@ -3452,9 +3452,9 @@ "integrity": "sha512-Vepa2uEBPuvtqAPDhA5mgGYl6byZMaRWH0z67vPXoOb4cIfv++S8xXGaOmAUVBktwbhCE4lWuTgT6BN2N19INQ==" }, "node_modules/@internxt/sdk": { - "version": "1.9.8", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.9.8.tgz", - "integrity": "sha512-/oC4EQUgstaWrIa8+T75JmZhSmNREHKx2yVZ3ELBHewAe5iFA0lQxAmiLEE2hr7qmVIAr9r3EN1d3IcQMJBYrg==", + "version": "1.9.13", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.9.13.tgz", + "integrity": "sha512-cQRWcduqZSBe+q1xNFQyJ1sGWzc3te60YJ2ek3Vsv+f1p3LwnoFAYqq6y7yr3T4kOL+QzfwOsedqLQiEnuAxtw==", "license": "MIT", "dependencies": { "axios": "^0.24.0", diff --git a/package.json b/package.json index fdd2968a6eee..a456e031fc98 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/eslint-config-internxt": "^2.0.0", "@internxt/lib": "^1.2.0", - "@internxt/sdk": "=1.9.8", + "@internxt/sdk": "=1.9.13", "@internxt/ui": "^0.0.22", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", diff --git a/react/features/base/meet/services/__tests__/payments.service.test.ts b/react/features/base/meet/services/__tests__/payments.service.test.ts new file mode 100644 index 000000000000..5ff7135fff7a --- /dev/null +++ b/react/features/base/meet/services/__tests__/payments.service.test.ts @@ -0,0 +1,158 @@ +import { afterEach, beforeEach, describe, expect, it, MockInstance, vi } from "vitest"; +import { PaymentsService } from "../payments.service"; +import { SdkManager } from "../sdk-manager.service"; + +vi.mock("../sdk-manager.service", () => { + return { + SdkManager: { + instance: { + getPayments: vi.fn(), + }, + }, + }; +}); + +describe("PaymentsService", () => { + const originalConsoleError = console.error; + + beforeEach(() => { + console.error = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + describe("instance", () => { + it("When accessing the instance, then a singleton instance is returned", () => { + const instance = PaymentsService.instance; + + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(PaymentsService); + }); + }); + + describe("checkMeetAvailability", () => { + it("When checking meet availability with meet features, then meet object is returned", async () => { + const mockMeetObject = { + allowed: true, + maxHours: 10, + maxParticipants: 5, + }; + + const mockUserTier = { + featuresPerService: { + meet: mockMeetObject, + }, + }; + + const mockPaymentsClient = { + getUserTier: vi.fn().mockResolvedValue(mockUserTier), + }; + + const getPaymentsMock = SdkManager.instance.getPayments as unknown as MockInstance; + getPaymentsMock.mockReturnValue(mockPaymentsClient); + + const result = await PaymentsService.instance.checkMeetAvailability(); + + expect(getPaymentsMock).toHaveBeenCalledTimes(1); + expect(getPaymentsMock).toHaveBeenCalledWith(); + expect(getPaymentsMock.mock.calls[0].length).toBe(0); + + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledTimes(1); + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledWith(); + expect(mockPaymentsClient.getUserTier.mock.calls[0].length).toBe(0); + + expect(result).toEqual(mockMeetObject); + }); + + it("When checking meet availability without meet features, then undefined is returned", async () => { + const mockUserTier = { + featuresPerService: { + drive: { allowed: true }, + }, + }; + + const mockPaymentsClient = { + getUserTier: vi.fn().mockResolvedValue(mockUserTier), + }; + + const getPaymentsMock = SdkManager.instance.getPayments as unknown as MockInstance; + getPaymentsMock.mockReturnValue(mockPaymentsClient); + + const result = await PaymentsService.instance.checkMeetAvailability(); + + expect(getPaymentsMock).toHaveBeenCalledTimes(1); + expect(getPaymentsMock).toHaveBeenCalledWith(); + expect(getPaymentsMock.mock.calls[0].length).toBe(0); + + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledTimes(1); + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledWith(); + expect(mockPaymentsClient.getUserTier.mock.calls[0].length).toBe(0); + + expect(result).toBeUndefined(); + }); + + it("When checking meet availability with empty featuresPerService, then undefined is returned", async () => { + const mockUserTier = { + featuresPerService: {}, + }; + + const mockPaymentsClient = { + getUserTier: vi.fn().mockResolvedValue(mockUserTier), + }; + + const getPaymentsMock = SdkManager.instance.getPayments as unknown as MockInstance; + getPaymentsMock.mockReturnValue(mockPaymentsClient); + + const result = await PaymentsService.instance.checkMeetAvailability(); + + expect(getPaymentsMock).toHaveBeenCalledTimes(1); + expect(getPaymentsMock).toHaveBeenCalledWith(); + expect(getPaymentsMock.mock.calls[0].length).toBe(0); + + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledTimes(1); + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledWith(); + expect(mockPaymentsClient.getUserTier.mock.calls[0].length).toBe(0); + + expect(result).toBeUndefined(); + }); + + it("When getUserTier throws an error, then the error is propagated", async () => { + const mockError = new Error("Failed to get user tier"); + + const mockPaymentsClient = { + getUserTier: vi.fn().mockRejectedValue(mockError), + }; + + const getPaymentsMock = SdkManager.instance.getPayments as unknown as MockInstance; + getPaymentsMock.mockReturnValue(mockPaymentsClient); + + await expect(PaymentsService.instance.checkMeetAvailability()).rejects.toThrow(mockError); + + expect(getPaymentsMock).toHaveBeenCalledTimes(1); + expect(getPaymentsMock).toHaveBeenCalledWith(); + expect(getPaymentsMock.mock.calls[0].length).toBe(0); + + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledTimes(1); + expect(mockPaymentsClient.getUserTier).toHaveBeenCalledWith(); + expect(mockPaymentsClient.getUserTier.mock.calls[0].length).toBe(0); + }); + + it("When getPayments throws an error, then the error is propagated", async () => { + const mockError = new Error("Failed to get payments client"); + + const getPaymentsMock = SdkManager.instance.getPayments as unknown as MockInstance; + getPaymentsMock.mockImplementation(() => { + throw mockError; + }); + + await expect(PaymentsService.instance.checkMeetAvailability()).rejects.toThrow(mockError); + + expect(getPaymentsMock).toHaveBeenCalledTimes(1); + expect(getPaymentsMock).toHaveBeenCalledWith(); + expect(getPaymentsMock.mock.calls[0].length).toBe(0); + }); + }); +}); diff --git a/react/features/base/meet/services/__tests__/sdk-manager.service.test.ts b/react/features/base/meet/services/__tests__/sdk-manager.service.test.ts index 93cfc481750c..1267de3d1d5c 100644 --- a/react/features/base/meet/services/__tests__/sdk-manager.service.test.ts +++ b/react/features/base/meet/services/__tests__/sdk-manager.service.test.ts @@ -1,8 +1,8 @@ +import { Auth, Drive, Meet } from "@internxt/sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import "../../__tests__/setup"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { SdkManager } from "../sdk-manager.service"; import { ConfigService } from "../config.service"; -import { Auth, Drive } from "@internxt/sdk"; +import { SdkManager } from "../sdk-manager.service"; vi.mock("@internxt/sdk", () => ({ Auth: { @@ -22,9 +22,41 @@ vi.mock("@internxt/sdk", () => ({ unauthorizedCallback: vi.fn(), })), }, + Payments: { + client: vi.fn().mockImplementation((baseUrl, appDetails, security) => ({ + baseUrl, + appDetails, + security, + unauthorizedCallback: vi.fn(), + })), + }, + }, + Meet: { + client: vi.fn().mockImplementation((baseUrl, appDetails, security) => ({ + baseUrl, + appDetails, + security, + unauthorizedCallback: vi.fn(), + })), }, })); +const mockLocalStorage = { + getItem: vi.fn().mockImplementation((key) => { + if (key === "xNewToken") return "mock-new-token"; + return null; + }), + clearCredentials: vi.fn(), +}; + +Object.defineProperty(global, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +// @ts-ignore - Ignore TypeScript error for test purposes +SdkManager.instance.localStorage = mockLocalStorage; + describe("SdkManager", () => { beforeEach(() => { SdkManager.clean(); @@ -89,18 +121,13 @@ describe("SdkManager", () => { const config: Record = { DRIVE_API_URL: "https://test-drive-api.com", DRIVE_NEW_API_URL: "https://test-drive-new-api.com", + PAYMENTS_API_URL: "https://test-payments-api.com", + MEET_API_URL: "https://test-meet-api.com", }; return config[key]; }); }); - it("When getting auth client, then the correct client is returned with proper configuration", () => { - SdkManager.init(mockApiSecurity); - const authClient = SdkManager.instance.getAuth(); - expect(authClient).toBeDefined(); - expect(Auth.client).toHaveBeenCalledWith("https://test-drive-api.com", expect.any(Object), mockApiSecurity); - }); - it("When getting new auth client, then the correct client is returned with proper configuration", () => { SdkManager.init(mockApiSecurity); const newAuthClient = SdkManager.instance.getNewAuth(); @@ -123,12 +150,44 @@ describe("SdkManager", () => { ); }); + it("When getting payments client, then the correct client is returned with proper configuration", () => { + const getItemSpy = vi.spyOn(global.localStorage, "getItem"); + + const paymentsClient = SdkManager.instance.getPayments(); + expect(paymentsClient).toBeDefined(); + + expect(getItemSpy).toHaveBeenCalledWith("xNewToken"); + + expect(Drive.Payments.client).toHaveBeenCalledWith( + "https://test-payments-api.com", + expect.any(Object), + expect.objectContaining({ + token: "mock-new-token", + }) + ); + }); + + it("When getting meet client, then the correct client is returned with proper configuration", () => { + const getItemSpy = vi.spyOn(global.localStorage, "getItem"); + + const meetClient = SdkManager.instance.getMeet(); + expect(meetClient).toBeDefined(); + + expect(getItemSpy).toHaveBeenCalledWith("xNewToken"); + + expect(Meet.client).toHaveBeenCalledWith( + "https://test-meet-api.com", + expect.any(Object), + expect.objectContaining({ + token: "mock-new-token", + }) + ); + }); + it("When getting clients without api security, then they are created with undefined security", () => { - SdkManager.instance.getAuth(); SdkManager.instance.getNewAuth(); SdkManager.instance.getUsers(); - expect(Auth.client).toHaveBeenCalledWith("https://test-drive-api.com", expect.any(Object), undefined); expect(Auth.client).toHaveBeenCalledWith("https://test-drive-new-api.com", expect.any(Object), undefined); expect(Drive.Users.client).toHaveBeenCalledWith( "https://test-drive-api.com", @@ -136,5 +195,12 @@ describe("SdkManager", () => { undefined ); }); + + it("Tests the unauthorizedCallback in getNewTokenApiSecurity", () => { + SdkManager.instance.getPayments(); + const securityArg = (Drive.Payments.client as any).mock.calls[0][2]; + securityArg.unauthorizedCallback(); + expect(mockLocalStorage.clearCredentials).toHaveBeenCalled(); + }); }); }); diff --git a/react/features/base/meet/services/meeting.service.test.ts b/react/features/base/meet/services/meeting.service.test.ts index 7391fa413180..bcb7da589929 100644 --- a/react/features/base/meet/services/meeting.service.test.ts +++ b/react/features/base/meet/services/meeting.service.test.ts @@ -1,14 +1,32 @@ +import { JoinCallPayload } from "@internxt/sdk/dist/meet/types"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { get8x8BetaJWT } from "../../connection/options8x8"; import MeetingService from "./meeting.service"; +import { SdkManager } from "./sdk-manager.service"; + +// Crear tipos para los mocks que faciliten la verificación estricta +type MockedGet8x8BetaJWT = ReturnType & typeof get8x8BetaJWT; +type MockedGetMeet = ReturnType & typeof SdkManager.instance.getMeet; vi.mock("../../connection/options8x8", () => ({ get8x8BetaJWT: vi.fn(), })); +vi.mock("./sdk-manager.service", () => ({ + SdkManager: { + instance: { + getMeet: vi.fn(), + }, + }, +})); + describe("MeetingService", () => { const originalConsoleError = console.error; + // Obtener las versiones mockeadas con tipos adecuados + const mockedGet8x8BetaJWT = get8x8BetaJWT as MockedGet8x8BetaJWT; + const mockedGetMeet = SdkManager.instance.getMeet as MockedGetMeet; + beforeEach(() => { console.error = vi.fn(); vi.clearAllMocks(); @@ -19,7 +37,7 @@ describe("MeetingService", () => { }); describe("getInstance", () => { - it("should create a singleton instance", () => { + it("When getting multiple instances, then they should be the same singleton instance", () => { const instance1 = MeetingService.getInstance(); const instance2 = MeetingService.getInstance(); @@ -29,61 +47,278 @@ describe("MeetingService", () => { }); describe("generateMeetingRoom", () => { - it("should generate a meeting room successfully", async () => { + it("When generating a meeting room with valid token, then the room ID is returned", async () => { const mockToken = "valid-jwt-token"; const mockMeetData = { token: "meeting-token", room: "meeting-room-123", }; - (get8x8BetaJWT as any).mockResolvedValue(mockMeetData); + mockedGet8x8BetaJWT.mockResolvedValue(mockMeetData); const meetingService = MeetingService.getInstance(); const result = await meetingService.generateMeetingRoom(mockToken); - expect(get8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledTimes(1); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT.mock.calls[0].length).toBe(1); + expect(mockedGet8x8BetaJWT.mock.calls[0][0]).toBe(mockToken); expect(result).toBe("meeting-room-123"); }); - it("should return error when meeting room generation fails", async () => { + it("When generating a meeting room with invalid token, then an error is thrown", async () => { const mockToken = "invalid-jwt-token"; const mockError = new Error("Failed to generate meeting"); - (get8x8BetaJWT as any).mockRejectedValue(mockError); + mockedGet8x8BetaJWT.mockRejectedValue(mockError); const meetingService = MeetingService.getInstance(); await expect(meetingService.generateMeetingRoom(mockToken)).rejects.toThrow(mockError); - expect(get8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledTimes(1); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT.mock.calls[0].length).toBe(1); + expect(mockedGet8x8BetaJWT.mock.calls[0][0]).toBe(mockToken); }); - it("should handle null or undefined meeting data properly", async () => { + it("When meeting data is missing room property, then falsy value is returned", async () => { const mockToken = "valid-jwt-token"; const mockMeetData = { token: "meeting-token", }; - (get8x8BetaJWT as any).mockResolvedValue(mockMeetData); + mockedGet8x8BetaJWT.mockResolvedValue(mockMeetData); const meetingService = MeetingService.getInstance(); const result = await meetingService.generateMeetingRoom(mockToken); - expect(get8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledTimes(1); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledWith(mockToken); + expect(mockedGet8x8BetaJWT.mock.calls[0].length).toBe(1); + expect(mockedGet8x8BetaJWT.mock.calls[0][0]).toBe(mockToken); expect(result).toBeFalsy(); }); - it("should handle empty string token", async () => { + it("When token is empty string, then an error is thrown", async () => { const mockToken = ""; const mockError = new Error("Invalid token"); - (get8x8BetaJWT as any).mockRejectedValue(mockError); + mockedGet8x8BetaJWT.mockRejectedValue(mockError); const meetingService = MeetingService.getInstance(); await expect(meetingService.generateMeetingRoom(mockToken)).rejects.toThrow(mockError); - expect(get8x8BetaJWT).toHaveBeenCalledWith(""); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledTimes(1); + expect(mockedGet8x8BetaJWT).toHaveBeenCalledWith(""); + expect(mockedGet8x8BetaJWT.mock.calls[0].length).toBe(1); + expect(mockedGet8x8BetaJWT.mock.calls[0][0]).toBe(""); // Verify parameter + }); + }); + + describe("createCall", () => { + it("When creating a call, then the call details are returned", async () => { + const mockCreateCallResponse = { + id: "call-123", + createdAt: new Date().toISOString(), + status: "created", + }; + + const mockMeetClient = { + createCall: vi.fn().mockResolvedValue(mockCreateCallResponse), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + const result = await meetingService.createCall(); + + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.createCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.createCall).toHaveBeenCalledWith(); + expect(mockMeetClient.createCall.mock.calls[0].length).toBe(0); + expect(result).toEqual(mockCreateCallResponse); + }); + + it("When call creation fails, then an error is thrown", async () => { + const mockError = new Error("Failed to create call"); + + const mockMeetClient = { + createCall: vi.fn().mockRejectedValue(mockError), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + + await expect(meetingService.createCall()).rejects.toThrow(mockError); + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.createCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.createCall).toHaveBeenCalledWith(); + expect(mockMeetClient.createCall.mock.calls[0].length).toBe(0); + }); + }); + + describe("joinCall", () => { + it("When joining a call with valid ID and payload, then session details are returned", async () => { + const mockCallId = "call-123"; + const mockPayload: JoinCallPayload = { + name: "John", + lastname: "Doe", + anonymous: false, + }; + + const mockJoinCallResponse = { + sessionId: "session-456", + token: "join-token-789", + }; + + const mockMeetClient = { + joinCall: vi.fn().mockResolvedValue(mockJoinCallResponse), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + const result = await meetingService.joinCall(mockCallId, mockPayload); + + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.joinCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.joinCall).toHaveBeenCalledWith(mockCallId, mockPayload); + expect(mockMeetClient.joinCall.mock.calls[0].length).toBe(2); + expect(mockMeetClient.joinCall.mock.calls[0][0]).toBe(mockCallId); + expect(mockMeetClient.joinCall.mock.calls[0][1]).toEqual(mockPayload); + expect(result).toEqual(mockJoinCallResponse); + }); + + it("When joining a call with invalid ID, then an error is thrown", async () => { + const mockCallId = "invalid-call-id"; + const mockPayload: JoinCallPayload = { + name: "John", + lastname: "Doe", + anonymous: false, + }; + + const mockError = new Error("Failed to join call"); + + const mockMeetClient = { + joinCall: vi.fn().mockRejectedValue(mockError), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + + await expect(meetingService.joinCall(mockCallId, mockPayload)).rejects.toThrow(mockError); + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.joinCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.joinCall).toHaveBeenCalledWith(mockCallId, mockPayload); + expect(mockMeetClient.joinCall.mock.calls[0].length).toBe(2); + expect(mockMeetClient.joinCall.mock.calls[0][0]).toBe(mockCallId); + expect(mockMeetClient.joinCall.mock.calls[0][1]).toEqual(mockPayload); + }); + + it("When joining a call with empty ID, then an error is thrown", async () => { + const mockCallId = ""; + const mockPayload: JoinCallPayload = { + name: "John", + lastname: "Doe", + anonymous: false, + }; + + const mockError = new Error("Invalid call ID"); + + const mockMeetClient = { + joinCall: vi.fn().mockRejectedValue(mockError), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + + await expect(meetingService.joinCall(mockCallId, mockPayload)).rejects.toThrow(mockError); + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.joinCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.joinCall).toHaveBeenCalledWith(mockCallId, mockPayload); + expect(mockMeetClient.joinCall.mock.calls[0].length).toBe(2); + expect(mockMeetClient.joinCall.mock.calls[0][0]).toBe(""); + }); + }); + + describe("getCurrentUsersInCall", () => { + it("When getting users in a call with participants, then an array of users is returned", async () => { + const mockCallId = "call-123"; + const mockUsersResponse = [ + { id: "user-1", name: "John", lastname: "Doe", joinedAt: new Date().toISOString() }, + { id: "user-2", name: "Jane", lastname: "Smith", joinedAt: new Date().toISOString() }, + ]; + + const mockMeetClient = { + getCurrentUsersInCall: vi.fn().mockResolvedValue(mockUsersResponse), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + const result = await meetingService.getCurrentUsersInCall(mockCallId); + + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledWith(mockCallId); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0].length).toBe(1); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0][0]).toBe(mockCallId); + expect(result).toEqual(mockUsersResponse); + }); + + it("When getting users in an empty call, then an empty array is returned", async () => { + const mockCallId = "empty-call-123"; + const mockUsersResponse: any[] = []; + + const mockMeetClient = { + getCurrentUsersInCall: vi.fn().mockResolvedValue(mockUsersResponse), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + const result = await meetingService.getCurrentUsersInCall(mockCallId); + + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledWith(mockCallId); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0].length).toBe(1); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0][0]).toBe(mockCallId); + expect(result).toEqual([]); + expect(result.length).toBe(0); + }); + + it("When getting users with invalid call ID, then an error is thrown", async () => { + const mockCallId = "invalid-call-id"; + const mockError = new Error("Failed to get users in call"); + + const mockMeetClient = { + getCurrentUsersInCall: vi.fn().mockRejectedValue(mockError), + }; + + mockedGetMeet.mockReturnValue(mockMeetClient); + + const meetingService = MeetingService.getInstance(); + + await expect(meetingService.getCurrentUsersInCall(mockCallId)).rejects.toThrow(mockError); + expect(mockedGetMeet).toHaveBeenCalledTimes(1); + expect(mockedGetMeet).toHaveBeenCalledWith(); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledTimes(1); + expect(mockMeetClient.getCurrentUsersInCall).toHaveBeenCalledWith(mockCallId); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0].length).toBe(1); + expect(mockMeetClient.getCurrentUsersInCall.mock.calls[0][0]).toBe(mockCallId); }); }); }); diff --git a/react/features/base/meet/services/meeting.service.ts b/react/features/base/meet/services/meeting.service.ts index 713ffbc10c9c..cf2ada01c51e 100644 --- a/react/features/base/meet/services/meeting.service.ts +++ b/react/features/base/meet/services/meeting.service.ts @@ -1,4 +1,11 @@ +import { + CreateCallResponse, + JoinCallPayload, + JoinCallResponse, + UsersInCallResponse, +} from "@internxt/sdk/dist/meet/types"; import { get8x8BetaJWT } from "../../connection/options8x8"; +import { SdkManager } from "./sdk-manager.service"; class MeetingService { private static instance: MeetingService; @@ -21,6 +28,39 @@ class MeetingService { const meetData = await get8x8BetaJWT(token); return meetData?.room; } + + /** + * Creates a new call and returns its details + * @returns The call details with ID and other properties + * @async + **/ + public createCall = async (): Promise => { + const meetClient = SdkManager.instance.getMeet(); + return await meetClient.createCall(); + }; + + /** + * Joins an existing call by its ID + * @param callId The ID of the call to join + * @param payload The join call payload (name, lastname, anonymous) + * @returns The join call response with session details + * @async + **/ + public joinCall = async (callId: string, payload: JoinCallPayload): Promise => { + const meetClient = SdkManager.instance.getMeet(); + return await meetClient.joinCall(callId, payload); + }; + + /** + * Gets the list of current users in a call + * @param callId The ID of the call to get users from + * @returns Array of users currently in the call + * @async + **/ + public getCurrentUsersInCall = async (callId: string): Promise => { + const meetClient = SdkManager.instance.getMeet(); + return await meetClient.getCurrentUsersInCall(callId); + }; } export default MeetingService; diff --git a/react/features/base/meet/services/payments.service.ts b/react/features/base/meet/services/payments.service.ts new file mode 100644 index 000000000000..c232c752ef8c --- /dev/null +++ b/react/features/base/meet/services/payments.service.ts @@ -0,0 +1,13 @@ +import { SdkManager } from "./sdk-manager.service"; + +export class PaymentsService { + public static readonly instance: PaymentsService = new PaymentsService(); + + public checkMeetAvailability = async () => { + const paymentsClient = SdkManager.instance.getPayments(); + const userTier = await paymentsClient.getUserTier(); + const meetObject = userTier.featuresPerService["meet"]; + + return meetObject; + }; +} diff --git a/react/features/base/meet/services/sdk-manager.service.ts b/react/features/base/meet/services/sdk-manager.service.ts index dfe3f6c396f2..801116b5b8cc 100644 --- a/react/features/base/meet/services/sdk-manager.service.ts +++ b/react/features/base/meet/services/sdk-manager.service.ts @@ -1,7 +1,8 @@ -import { Auth, Drive } from '@internxt/sdk'; -import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; -import packageJson from '../../../../../package.json'; -import { ConfigService } from './config.service'; +import { Auth, Drive, Meet } from "@internxt/sdk"; +import { ApiSecurity, AppDetails } from "@internxt/sdk/dist/shared"; +import packageJson from "../../../../../package.json"; +import LocalStorageManager from "../LocalStorageManager"; +import { ConfigService } from "./config.service"; export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; /** @@ -11,6 +12,7 @@ export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; export class SdkManager { public static readonly instance: SdkManager = new SdkManager(); private static apiSecurity?: SdkManagerApiSecurity; + private readonly localStorage = LocalStorageManager; /** * Sets the security details needed to create SDK clients @@ -42,6 +44,17 @@ export class SdkManager { return SdkManager.apiSecurity as SdkManagerApiSecurity; }; + private getNewTokenApiSecurity(): ApiSecurity { + return { + token: localStorage.getItem("xNewToken") ?? "", + unauthorizedCallback: () => { + if (this.localStorage.clearCredentials) { + this.localStorage.clearCredentials(); + } + }, + }; + } + /** * Returns the application details from package.json * @returns The name and the version of the app from package.json @@ -53,16 +66,6 @@ export class SdkManager { }; }; - /** Auth old client SDK */ - getAuth() { - const DRIVE_API_URL = ConfigService.instance.get("DRIVE_API_URL"); - - const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); - const appDetails = SdkManager.getAppDetails(); - - return Auth.client(DRIVE_API_URL, appDetails, apiSecurity); - } - getNewAuth() { const DRIVE_NEW_API_URL = ConfigService.instance.get("DRIVE_NEW_API_URL"); @@ -81,4 +84,24 @@ export class SdkManager { return Drive.Users.client(DRIVE_API_URL, appDetails, apiSecurity); } + + /** Payments SDK */ + getPayments() { + const PAYMENTS_API_URL = ConfigService.instance.get("PAYMENTS_API_URL"); + + const apiSecurity = this.getNewTokenApiSecurity(); + const appDetails = SdkManager.getAppDetails(); + + return Drive.Payments.client(PAYMENTS_API_URL, appDetails, apiSecurity); + } + + /** Meet SDK */ + getMeet() { + const MEET_API_URL = ConfigService.instance.get("MEET_API_URL"); + + const apiSecurity = this.getNewTokenApiSecurity(); + const appDetails = SdkManager.getAppDetails(); + + return Meet.client(MEET_API_URL, appDetails, apiSecurity); + } } diff --git a/react/features/base/meet/services/types/config.types.ts b/react/features/base/meet/services/types/config.types.ts index 777979c951b1..5e9ca4437261 100644 --- a/react/features/base/meet/services/types/config.types.ts +++ b/react/features/base/meet/services/types/config.types.ts @@ -1,8 +1,10 @@ export interface ConfigKeys { - readonly DRIVE_API_URL: string; - readonly DRIVE_NEW_API_URL: string; - readonly CRYPTO_SECRET: string; - readonly MAGIC_IV: string; - readonly MAGIC_SALT: string; - readonly JITSI_APP_ID: string; + readonly DRIVE_API_URL: string; + readonly DRIVE_NEW_API_URL: string; + readonly PAYMENTS_API_URL: string; + readonly MEET_API_URL: string; + readonly CRYPTO_SECRET: string; + readonly MAGIC_IV: string; + readonly MAGIC_SALT: string; + readonly JITSI_APP_ID: string; } diff --git a/react/features/base/meet/views/Home/hooks/useLoginModal.ts b/react/features/base/meet/views/Home/hooks/useLoginModal.ts index 2869bd24b85d..5bea283f7b02 100644 --- a/react/features/base/meet/views/Home/hooks/useLoginModal.ts +++ b/react/features/base/meet/views/Home/hooks/useLoginModal.ts @@ -61,7 +61,6 @@ export function useLoginModal({ onClose, onLogin, translate }: UseAuthModalProps try { return await AuthService.instance.doLogin(email, password, twoFactorCode); } catch (err) { - console.error("Error in authenticateUser", err); throw new Error(translate("meet.auth.modal.error.invalidCredentials")); } }, @@ -124,6 +123,7 @@ export function useLoginModal({ onClose, onLogin, translate }: UseAuthModalProps saveRoomId(meetData.room); saveUserSession(loginCredentials); + onClose(); }; diff --git a/react/features/base/meet/views/Home/hooks/useSignUp.ts b/react/features/base/meet/views/Home/hooks/useSignUp.ts index 2f2e0fe2ae27..ea57d766d456 100644 --- a/react/features/base/meet/views/Home/hooks/useSignUp.ts +++ b/react/features/base/meet/views/Home/hooks/useSignUp.ts @@ -66,10 +66,9 @@ export const useSignup = ({ onClose, onSignup, translate, referrer }: useSignupP mnemonic: mnemonic, }); } - console.log("aqui"); + onClose(); } catch (error: any) { - console.error("Signup error:", error); setSignupError(error.message ?? translate("meet.auth.modal.signup.error.signupFailed")); } finally { setIsSigningUp(false); diff --git a/webpack.config.js b/webpack.config.js index 28d2f029fc30..201be69138a0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -223,6 +223,7 @@ function getConfig(options = {}) { // Provide some empty Node modules (required by AtlasKit, olm). crypto: "crypto-browserify", vm: "vm-browserify", + buffer: require.resolve("buffer/"), fs: false, path: false, process: false,