diff --git a/package.json b/package.json index e3f5309..e0d0470 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@internxt/sdk", "author": "Internxt ", - "version": "1.9.11", + "version": "1.9.12", "description": "An sdk for interacting with Internxt's services", "repository": { "type": "git", diff --git a/src/drive/payments/index.ts b/src/drive/payments/index.ts index ee41026..2be2756 100644 --- a/src/drive/payments/index.ts +++ b/src/drive/payments/index.ts @@ -2,6 +2,7 @@ import { ApiSecurity, ApiUrl, AppDetails } from '../../shared'; import { headersWithToken } from '../../shared/headers'; import { HttpClient } from '../../shared/http/client'; import AppError from '../../shared/types/errors'; +import { Tier } from './types/tiers'; import { AvailableProducts, CreateCheckoutSessionPayload, @@ -18,7 +19,7 @@ import { UpdateSubscriptionPaymentMethod, UserSubscription, UserType, -} from './types'; +} from './types/types'; export class Payments { private readonly client: HttpClient; @@ -228,6 +229,28 @@ export class Payments { return this.client.get('/products', this.headers()); } + /** + * Gets product information based on the user's subscription tier. + * + * @param {UserType} [userType] - The type of user for which to query product information. + * If not specified, UserType.Individual will be used by default. + * @returns {Promise} A promise that resolves with the product information + * available for the specified tier. + * + * @example + * // Get products for an individual user tier (default) + * const individualProducts = await getUserTier(); + * + * @example + * // Get products for a business user tier + * const businessProducts = await getUserTier(UserType.Business); + */ + public getUserTier(userType?: UserType): Promise { + const query = new URLSearchParams(); + if (userType !== undefined) query.set('tierType', userType); + return this.client.get(`/products/tier?${query.toString()}`, this.headers()); + } + /** * Returns the needed headers for the module requests * @private diff --git a/src/drive/payments/object-storage.ts b/src/drive/payments/object-storage.ts index 677cc4d..be22ba1 100644 --- a/src/drive/payments/object-storage.ts +++ b/src/drive/payments/object-storage.ts @@ -1,7 +1,7 @@ import { ApiUrl, AppDetails } from '../../shared'; import { basicHeaders } from '../../shared/headers'; import { HttpClient } from '../../shared/http/client'; -import { CreatedSubscriptionData } from './types'; +import { CreatedSubscriptionData } from './types/types'; interface ObjectStoragePlan { id: string; diff --git a/src/drive/payments/types/tiers.ts b/src/drive/payments/types/tiers.ts new file mode 100644 index 0000000..9380a9f --- /dev/null +++ b/src/drive/payments/types/tiers.ts @@ -0,0 +1,57 @@ +interface AntivirusFeatures { + enabled: boolean; +} + +interface BackupsFeatures { + enabled: boolean; +} + +export interface DriveFeatures { + enabled: boolean; + maxSpaceBytes: number; + workspaces: { + enabled: boolean; + minimumSeats: number; + maximumSeats: number; + maxSpaceBytesPerSeat: number; + }; +} + +interface MeetFeatures { + enabled: boolean; + paxPerCall: number; +} + +interface MailFeatures { + enabled: boolean; + addressesPerUser: number; +} + +export interface VpnFeatures { + enabled: boolean; + featureId: string; +} + +export enum Service { + Drive = 'drive', + Backups = 'backups', + Antivirus = 'antivirus', + Meet = 'meet', + Mail = 'mail', + Vpn = 'vpn', +} + +export interface Tier { + id: string; + label: string; + productId: string; + billingType: 'subscription' | 'lifetime'; + featuresPerService: { + [Service.Drive]: DriveFeatures; + [Service.Backups]: BackupsFeatures; + [Service.Antivirus]: AntivirusFeatures; + [Service.Meet]: MeetFeatures; + [Service.Mail]: MailFeatures; + [Service.Vpn]: VpnFeatures; + }; +} diff --git a/src/drive/payments/types.ts b/src/drive/payments/types/types.ts similarity index 98% rename from src/drive/payments/types.ts rename to src/drive/payments/types/types.ts index a1ad989..b45e10b 100644 --- a/src/drive/payments/types.ts +++ b/src/drive/payments/types/types.ts @@ -1,4 +1,4 @@ -import { AppSumoDetails } from './../../shared/types/appsumo'; +import { AppSumoDetails } from '../../../shared/types/appsumo'; export interface ProductData { id: string; name: string; diff --git a/src/index.ts b/src/index.ts index 8eb4e49..16d40f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export * as Network from './network'; export * as photos from './photos'; export * as Shared from './shared'; export * as Workspaces from './workspaces'; +export * from './meet'; diff --git a/src/meet/index.test.ts b/src/meet/index.test.ts new file mode 100644 index 0000000..f013030 --- /dev/null +++ b/src/meet/index.test.ts @@ -0,0 +1,175 @@ +import sinon from 'sinon'; +import { ApiSecurity, AppDetails } from '../shared'; +import { basicHeaders, headersWithToken } from '../shared/headers'; +import { HttpClient } from '../shared/http/client'; +import { Meet } from './index'; +import { CreateCallResponse, JoinCallPayload, JoinCallResponse, UsersInCallResponse } from './types'; + +const httpClient = HttpClient.create(''); + +describe('Meet service tests', () => { + beforeEach(() => { + sinon.stub(HttpClient, 'create').returns(httpClient); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('createCall method', () => { + it('should successfully create a call with token', async () => { + // Arrange + const expectedResponse: CreateCallResponse = { + token: 'call-token', + room: 'room-id', + paxPerCall: 10, + }; + + const { client, headers } = clientAndHeadersWithToken(); + const postCall = sinon.stub(httpClient, 'post').resolves(expectedResponse); + + // Act + const response = await client.createCall(); + + // Assert + expect(postCall.firstCall.args).toEqual(['call', {}, headers]); + expect(response).toEqual(expectedResponse); + }); + + it('should throw an error when token is missing', async () => { + // Arrange + const { client } = clientAndHeadersWithoutToken(); + + // Act & Assert + await expect(client.createCall()).rejects.toThrow('Token is required for Meet operations'); + }); + }); + + describe('joinCall method', () => { + const callId = 'call-123'; + const payload: JoinCallPayload = { + name: 'John', + lastname: 'Doe', + anonymous: false, + }; + + const joinCallResponse: JoinCallResponse = { + token: 'join-token', + room: 'room-id', + userId: 'user-123', + }; + + it('should join a call successfully with token', async () => { + // Arrange + const { client, headers } = clientAndHeadersWithToken(); + const postCall = sinon.stub(httpClient, 'post').resolves(joinCallResponse); + + // Act + const response = await client.joinCall(callId, payload); + + // Assert + expect(postCall.firstCall.args).toEqual([`call/${callId}/users/join`, payload, headers]); + expect(response).toEqual(joinCallResponse); + }); + + it('should join a call successfully without token', async () => { + // Arrange + const { client, headers } = clientAndHeadersWithoutToken(); + const postCall = sinon.stub(httpClient, 'post').resolves(joinCallResponse); + + // Act + const response = await client.joinCall(callId, payload); + + // Assert + expect(postCall.firstCall.args[0]).toEqual(`call/${callId}/users/join`); + expect(postCall.firstCall.args[1]).toEqual(payload); + expect(postCall.firstCall.args[2]).toEqual(headers); + expect(response).toEqual(joinCallResponse); + }); + }); + + describe('getCurrentUsersInCall method', () => { + const callId = 'call-123'; + const usersInCallResponse: UsersInCallResponse[] = [ + { + userId: 'user-123', + name: 'John', + lastname: 'Doe', + anonymous: false, + avatar: 'avatar-url-1', + }, + { + userId: 'user-456', + name: 'Jane', + lastname: 'Smith', + anonymous: true, + avatar: 'avatar-url-2', + }, + ]; + + it('should get current users in call successfully with token', async () => { + // Arrange + const { client, headers } = clientAndHeadersWithToken(); + const getCall = sinon.stub(httpClient, 'get').resolves(usersInCallResponse); + + // Act + const response = await client.getCurrentUsersInCall(callId); + + // Assert + expect(getCall.firstCall.args).toEqual([`call/${callId}/users`, headers]); + expect(response).toEqual(usersInCallResponse); + }); + + it('should get current users in call successfully without token', async () => { + // Arrange + const { client, headers } = clientAndHeadersWithoutToken(); + const getCall = sinon.stub(httpClient, 'get').resolves(usersInCallResponse); + + // Act + const response = await client.getCurrentUsersInCall(callId); + + // Assert + expect(getCall.firstCall.args[0]).toEqual(`call/${callId}/users`); + expect(getCall.firstCall.args[1]).toEqual(headers); + expect(response).toEqual(usersInCallResponse); + }); + }); +}); + +function clientAndHeadersWithToken( + apiUrl = '', + clientName = 'c-name', + clientVersion = '0.1', + token = 'my-token', +): { + client: Meet; + headers: object; +} { + const appDetails: AppDetails = { + clientName, + clientVersion, + }; + const apiSecurity: ApiSecurity = { + token, + }; + const client = Meet.client(apiUrl, appDetails, apiSecurity); + const headers = headersWithToken(clientName, clientVersion, token); + return { client, headers }; +} + +function clientAndHeadersWithoutToken( + apiUrl = '', + clientName = 'c-name', + clientVersion = '0.1', +): { + client: Meet; + headers: object; +} { + const appDetails: AppDetails = { + clientName, + clientVersion, + }; + const client = Meet.client(apiUrl, appDetails); + const headers = basicHeaders(clientName, clientVersion); + return { client, headers }; +} diff --git a/src/meet/index.ts b/src/meet/index.ts new file mode 100644 index 0000000..cbcfbca --- /dev/null +++ b/src/meet/index.ts @@ -0,0 +1,47 @@ +import { ApiSecurity, ApiUrl, AppDetails } from '../shared'; +import { basicHeaders, headersWithToken } from '../shared/headers'; +import { HttpClient } from '../shared/http/client'; +import { CreateCallResponse, JoinCallPayload, JoinCallResponse, UsersInCallResponse } from './types'; + +export class Meet { + private readonly client: HttpClient; + private readonly appDetails: AppDetails; + private readonly apiSecurity?: ApiSecurity; + + private constructor(apiUrl: ApiUrl, appDetails: AppDetails, apiSecurity?: ApiSecurity) { + this.client = HttpClient.create(apiUrl, apiSecurity?.unauthorizedCallback); + this.appDetails = appDetails; + this.apiSecurity = apiSecurity; + } + + public static client(apiUrl: ApiUrl, appDetails: AppDetails, apiSecurity?: ApiSecurity) { + return new Meet(apiUrl, appDetails, apiSecurity); + } + + async createCall(): Promise { + return this.client.post('call', {}, this.headersWithToken()); + } + + async joinCall(callId: string, payload: JoinCallPayload): Promise { + const headers = this.apiSecurity?.token ? this.headersWithToken() : this.basicHeaders(); + + return this.client.post(`call/${callId}/users/join`, { ...payload }, headers); + } + + async getCurrentUsersInCall(callId: string): Promise { + const headers = this.apiSecurity?.token ? this.headersWithToken() : this.basicHeaders(); + + return this.client.get(`call/${callId}/users`, headers); + } + + private headersWithToken() { + if (!this.apiSecurity?.token) { + throw new Error('Token is required for Meet operations'); + } + return headersWithToken(this.appDetails.clientName, this.appDetails.clientVersion, this.apiSecurity.token); + } + + private basicHeaders() { + return basicHeaders(this.appDetails.clientName, this.appDetails.clientVersion); + } +} diff --git a/src/meet/types.ts b/src/meet/types.ts new file mode 100644 index 0000000..43b557d --- /dev/null +++ b/src/meet/types.ts @@ -0,0 +1,25 @@ +export interface CreateCallResponse { + token: string; + room: string; + paxPerCall: number; +} + +export interface JoinCallPayload { + name: string; + lastname: string; + anonymous: boolean; +} + +export interface JoinCallResponse { + token: string; + room: string; + userId: string; +} + +export interface UsersInCallResponse { + userId: string; + name: string; + lastname: string; + anonymous: boolean; + avatar: string; +} diff --git a/test/drive/payments/index.test.ts b/test/drive/payments/index.test.ts index 927f5ad..78c16fd 100644 --- a/test/drive/payments/index.test.ts +++ b/test/drive/payments/index.test.ts @@ -1,8 +1,8 @@ import sinon from 'sinon'; +import { Payments } from '../../../src/drive'; +import { CreatePaymentSessionPayload, StripeSessionMode, UserType } from '../../../src/drive/payments/types/types'; import { ApiSecurity, AppDetails } from '../../../src/shared'; import { headersWithToken } from '../../../src/shared/headers'; -import { Payments } from '../../../src/drive'; -import { CreatePaymentSessionPayload, StripeSessionMode, UserType } from '../../../src/drive/payments/types'; import { HttpClient } from '../../../src/shared/http/client'; const httpClient = HttpClient.create('');