@gymspace/sdk 1.9.1 → 1.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gymspace/sdk",
3
- "version": "1.9.1",
3
+ "version": "1.9.4",
4
4
  "description": "GymSpace TypeScript SDK for API integration",
5
5
  "author": "GymSpace Team",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -18,7 +18,14 @@ export class ApiClient {
18
18
  private axiosInstance: AxiosInstance;
19
19
  private config: GymSpaceConfig;
20
20
  private refreshToken: string | null = null;
21
+ private rememberMe: boolean | null = null;
21
22
  private onTokenRefreshed?: (accessToken: string, refreshToken?: string) => void;
23
+ private onAuthFailure?: (error: AuthenticationError) => void;
24
+ private isRefreshing = false;
25
+ private failedRequestsQueue: Array<{
26
+ resolve: (token: string) => void;
27
+ reject: (error: Error) => void;
28
+ }> = [];
22
29
 
23
30
  public getAccessToken(): string | null {
24
31
  return this.config.apiKey || null;
@@ -109,6 +116,104 @@ export class ApiClient {
109
116
  return response;
110
117
  },
111
118
  async (error: AxiosError) => {
119
+ // Handle 401 with automatic token refresh
120
+ if (error.response?.status === 401) {
121
+ const originalRequest = error.config;
122
+
123
+ // Skip auth requests (login, refresh endpoints)
124
+ if ((originalRequest as any)?.skipAuth) {
125
+ throw this.handleError(error);
126
+ }
127
+
128
+ // If already refreshing, queue this request
129
+ if (this.isRefreshing && this.refreshToken) {
130
+ return new Promise((resolve, reject) => {
131
+ this.failedRequestsQueue.push({
132
+ resolve: (token: string) => {
133
+ // Retry with new token
134
+ if (originalRequest) {
135
+ originalRequest.headers['Authorization'] = `Bearer ${token}`;
136
+ this.axiosInstance.request(originalRequest).then(resolve).catch(reject);
137
+ } else {
138
+ reject(error);
139
+ }
140
+ },
141
+ reject: (err: Error) => {
142
+ reject(err);
143
+ },
144
+ });
145
+ });
146
+ }
147
+
148
+ // Start refresh process
149
+ if (this.refreshToken) {
150
+ this.isRefreshing = true;
151
+
152
+ try {
153
+ console.log('Access token expired, attempting refresh...');
154
+
155
+ // Call the member refresh endpoint directly
156
+ const response = await axios.post(
157
+ `${this.config.baseURL}/auth/members/refresh`,
158
+ { refresh_token: this.refreshToken },
159
+ { headers: { 'Content-Type': 'application/json' } },
160
+ );
161
+
162
+ const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data;
163
+
164
+ console.log('Token refresh successful');
165
+
166
+ // Update SDK state
167
+ this.setAuthToken(newAccessToken);
168
+ if (newRefreshToken) {
169
+ this.setRefreshToken(newRefreshToken);
170
+ }
171
+
172
+ // Notify listeners
173
+ if (this.onTokenRefreshed) {
174
+ this.onTokenRefreshed(newAccessToken, newRefreshToken);
175
+ }
176
+
177
+ // Process queued requests
178
+ this.failedRequestsQueue.forEach((request) => {
179
+ request.resolve(newAccessToken);
180
+ });
181
+ this.failedRequestsQueue = [];
182
+
183
+ // Retry original request
184
+ if (originalRequest) {
185
+ originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
186
+ return this.axiosInstance.request(originalRequest);
187
+ }
188
+
189
+ throw error;
190
+ } catch (refreshError: any) {
191
+ console.error('Token refresh failed:', refreshError.message);
192
+
193
+ // Reject queued requests
194
+ this.failedRequestsQueue.forEach((request) => {
195
+ request.reject(refreshError);
196
+ });
197
+ this.failedRequestsQueue = [];
198
+
199
+ // Clear auth on refresh failure
200
+ this.clearAuth();
201
+
202
+ const authError = new AuthenticationError(
203
+ refreshError.response?.data?.message || 'Token refresh failed',
204
+ );
205
+
206
+ if (this.onAuthFailure) {
207
+ this.onAuthFailure(authError);
208
+ }
209
+
210
+ throw authError;
211
+ } finally {
212
+ this.isRefreshing = false;
213
+ }
214
+ }
215
+ }
216
+
112
217
  throw this.handleError(error);
113
218
  },
114
219
  );
@@ -167,6 +272,10 @@ export class ApiClient {
167
272
  headers['X-Gym-Id'] = options.gymId;
168
273
  }
169
274
 
275
+ if (options?.gymContextMode) {
276
+ headers['X-Gym-Context-Mode'] = options.gymContextMode;
277
+ }
278
+
170
279
  return { headers };
171
280
  }
172
281
 
@@ -228,6 +337,20 @@ export class ApiClient {
228
337
  return this.refreshToken;
229
338
  }
230
339
 
340
+ /**
341
+ * Set rememberMe preference from login response
342
+ */
343
+ setRememberMe(value: boolean): void {
344
+ this.rememberMe = value;
345
+ }
346
+
347
+ /**
348
+ * Get rememberMe preference
349
+ */
350
+ getRememberMe(): boolean | null {
351
+ return this.rememberMe;
352
+ }
353
+
231
354
  /**
232
355
  * Set callback for when tokens are refreshed by backend
233
356
  */
@@ -242,6 +365,13 @@ export class ApiClient {
242
365
  return this.onTokenRefreshed;
243
366
  }
244
367
 
368
+ /**
369
+ * Set callback for when authentication fails irrecoverably
370
+ */
371
+ setOnAuthFailure(callback: (error: AuthenticationError) => void): void {
372
+ this.onAuthFailure = callback;
373
+ }
374
+
245
375
  setGymId(gymId: string): void {
246
376
  this.axiosInstance.defaults.headers.common['X-Gym-Id'] = gymId;
247
377
  }
@@ -249,6 +379,7 @@ export class ApiClient {
249
379
  clearAuth(): void {
250
380
  delete this.config.apiKey;
251
381
  this.refreshToken = null;
382
+ this.rememberMe = null;
252
383
  delete this.axiosInstance.defaults.headers.common['Authorization'];
253
384
  delete this.axiosInstance.defaults.headers.common['X-Gym-Id'];
254
385
  }
@@ -256,4 +387,40 @@ export class ApiClient {
256
387
  getBaseUrl(): string {
257
388
  return this.config.baseURL;
258
389
  }
390
+
391
+ /**
392
+ * Get config for creating scoped SDK instances
393
+ * Exposes minimal config needed without exposing internal structure
394
+ */
395
+ getConfigForScoped(): {
396
+ baseURL: string;
397
+ apiKey?: string;
398
+ refreshToken?: string;
399
+ headers?: Record<string, string>;
400
+ } {
401
+ return {
402
+ baseURL: this.config.baseURL,
403
+ apiKey: this.config.apiKey,
404
+ refreshToken: this.refreshToken,
405
+ headers: this.config.headers,
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Get axios instance defaults for scoped SDK
411
+ */
412
+ getAxiosDefaults(): Record<string, unknown> {
413
+ return { ...this.axiosInstance.defaults.headers.common };
414
+ }
415
+
416
+ /**
417
+ * Set common headers on axios instance (for scoped SDK setup)
418
+ */
419
+ setCommonHeaders(headers: Record<string, string>): void {
420
+ Object.entries(headers).forEach(([key, value]) => {
421
+ if (value !== undefined) {
422
+ this.axiosInstance.defaults.headers.common[key] = value;
423
+ }
424
+ });
425
+ }
259
426
  }
@@ -15,6 +15,7 @@ export interface RegisterOwnerDto {
15
15
  export interface LoginDto {
16
16
  email: string;
17
17
  password: string;
18
+ rememberMe?: boolean;
18
19
  }
19
20
 
20
21
  export interface LoginResponseDto {
@@ -119,9 +120,7 @@ export interface CollaboratorRoleDto {
119
120
  gymName: string;
120
121
  }
121
122
 
122
- export interface CurrentSessionResponse {
123
- accessToken: string;
124
- refreshToken?: string;
123
+ export interface CurrentSessionBase {
125
124
  user: {
126
125
  id: string;
127
126
  email: string;
@@ -178,4 +177,16 @@ export interface CurrentSessionResponse {
178
177
  permissions: string[];
179
178
  collaborator?: CollaboratorRoleDto;
180
179
  isAuthenticated: boolean;
181
- }
180
+ }
181
+
182
+ export type CurrentSessionResponse = CurrentSessionBase & {
183
+ access_token: string;
184
+ refresh_token?: string;
185
+ accessToken?: string;
186
+ refreshToken?: string;
187
+ };
188
+
189
+ export type CurrentSessionResponseLegacy = CurrentSessionBase & {
190
+ accessToken: string;
191
+ refreshToken?: string;
192
+ };
@@ -77,6 +77,8 @@ export interface Client {
77
77
  medicalConditions?: string;
78
78
  createdAt: string;
79
79
  updatedAt: string;
80
+ pinSet?: boolean;
81
+ lastLoginAt?: string;
80
82
  contracts?: Array<{
81
83
  id: string;
82
84
  status: string;
@@ -1,4 +1,4 @@
1
- import { ContractStatus, CancellationReason, SuspensionType } from '@gymspace/shared';
1
+ import { ContractStatus, ContractInstallmentStatus, CancellationReason, SuspensionType } from '@gymspace/shared';
2
2
  import { PaginationQueryDto } from '../types';
3
3
 
4
4
  export interface CreateContractDto {
@@ -12,6 +12,19 @@ export interface CreateContractDto {
12
12
  customPrice?: number;
13
13
  receiptIds?: string[];
14
14
  metadata?: Record<string, any>;
15
+ paymentPlan?: ContractPaymentPlanInput;
16
+ }
17
+
18
+ export interface ContractInstallmentScheduleDto {
19
+ dueDate: string;
20
+ amount: number;
21
+ }
22
+
23
+ export interface ContractPaymentPlanInput {
24
+ installmentsCount?: number;
25
+ downPaymentAmount?: number;
26
+ firstDueDate?: string;
27
+ schedule?: ContractInstallmentScheduleDto[];
15
28
  }
16
29
 
17
30
  export interface RenewContractDto {
@@ -93,6 +106,7 @@ export interface Contract {
93
106
  contractDocumentId?: string | null;
94
107
  paymentReceiptIds?: string[] | null;
95
108
  receiptIds?: string[];
109
+ accessGymIds?: string[];
96
110
  metadata?: Record<string, any>;
97
111
  createdByUserId?: string;
98
112
  updatedByUserId?: string | null;
@@ -156,6 +170,76 @@ export interface Contract {
156
170
  };
157
171
  }
158
172
 
173
+ export interface ContractInstallment {
174
+ id: string;
175
+ contractId: string;
176
+ paymentPlanId: string;
177
+ installmentNumber: number;
178
+ dueDate: string;
179
+ amount: string;
180
+ paidAmount: string;
181
+ status: ContractInstallmentStatus;
182
+ paidAt?: string | null;
183
+ lastReminderAt?: string | null;
184
+ createdAt: string;
185
+ updatedAt: string;
186
+ deletedAt?: string | null;
187
+ }
188
+
189
+ export interface ContractPayment {
190
+ id: string;
191
+ contractId: string;
192
+ installmentId?: string | null;
193
+ paymentMethodId: string;
194
+ amount: string;
195
+ paidAt: string;
196
+ receiptIds?: string[];
197
+ notes?: string | null;
198
+ createdByUserId: string;
199
+ createdAt: string;
200
+ updatedAt: string;
201
+ deletedAt?: string | null;
202
+ paymentMethod?: {
203
+ id: string;
204
+ name: string;
205
+ };
206
+ installment?: {
207
+ id: string;
208
+ installmentNumber: number;
209
+ dueDate: string;
210
+ };
211
+ }
212
+
213
+ export interface ContractInstallmentsResponse {
214
+ contractId: string;
215
+ totalAmount: number;
216
+ totalPaid: number;
217
+ outstandingAmount: number;
218
+ nextDueDate?: string | null;
219
+ installments: ContractInstallment[];
220
+ }
221
+
222
+ export interface ContractPaymentsResponse {
223
+ contractId: string;
224
+ totalPaid: number;
225
+ payments: ContractPayment[];
226
+ }
227
+
228
+ export interface RegisterContractPaymentDto {
229
+ amount: number;
230
+ paymentMethodId: string;
231
+ paidAt?: string;
232
+ receiptIds?: string[];
233
+ notes?: string;
234
+ installmentId?: string;
235
+ applyMode?: 'oldest_due' | 'installment';
236
+ }
237
+
238
+ export interface RegisterContractPaymentResponse {
239
+ success: boolean;
240
+ payments: ContractPayment[];
241
+ }
242
+
159
243
  export interface GetContractsParams extends PaginationQueryDto {
160
244
  status?: ContractStatus;
161
245
  clientName?: string;
@@ -1,8 +1,4 @@
1
- // Feature Types
2
- export enum FeatureType {
3
- AI_GENERATION = 'ai_generation',
4
- BULK_WHATSAPP = 'bulk_whatsapp',
5
- }
1
+ import { FeatureType } from '@gymspace/shared';
6
2
 
7
3
  // Credit Account Models
8
4
  export interface CreditAccount {
@@ -29,12 +29,22 @@ export interface SalesRevenue {
29
29
 
30
30
  export interface Debts {
31
31
  totalDebt: number;
32
+ salesDebt?: number;
33
+ contractsDebt?: number;
32
34
  clientsWithDebt: number;
33
35
  averageDebt: number;
34
36
  startDate: string;
35
37
  endDate: string;
36
38
  }
37
39
 
40
+ export interface Collections {
41
+ totalCollected: number;
42
+ salesCollected: number;
43
+ contractsCollected: number;
44
+ startDate: string;
45
+ endDate: string;
46
+ }
47
+
38
48
  export interface CheckIns {
39
49
  totalCheckIns: number;
40
50
  uniqueClients: number;
@@ -1,3 +1,5 @@
1
+ import type { TemplateCode } from '@gymspace/shared';
2
+
1
3
  export interface CreateGymDto {
2
4
  name: string;
3
5
  description?: string;
@@ -59,6 +61,15 @@ export interface ContractSettings {
59
61
  expiringSoonDays?: number;
60
62
  gracePeriodDays?: number;
61
63
  enableExpirationNotifications?: boolean;
64
+ installmentsEnabled?: boolean;
65
+ defaultInstallmentsCount?: number;
66
+ defaultDownPaymentAmount?: number;
67
+ allowContractInstallmentOverride?: boolean;
68
+ allowReminderOverride?: boolean;
69
+ reminderDaysBeforeDue?: number[];
70
+ reminderDaysAfterDue?: number[];
71
+ reminderTemplateCode?: TemplateCode;
72
+ debtAccessPolicy?: 'allow_always' | 'block_overdue';
62
73
  }
63
74
 
64
75
  /**
@@ -77,6 +88,15 @@ export interface UpdateGymContractSettingsDto {
77
88
  expiringSoonDays?: number;
78
89
  gracePeriodDays?: number;
79
90
  enableExpirationNotifications?: boolean;
91
+ installmentsEnabled?: boolean;
92
+ defaultInstallmentsCount?: number;
93
+ defaultDownPaymentAmount?: number;
94
+ allowContractInstallmentOverride?: boolean;
95
+ allowReminderOverride?: boolean;
96
+ reminderDaysBeforeDue?: number[];
97
+ reminderDaysAfterDue?: number[];
98
+ reminderTemplateCode?: TemplateCode;
99
+ debtAccessPolicy?: 'allow_always' | 'block_overdue';
80
100
  }
81
101
 
82
102
  export interface Gym {
@@ -9,6 +9,7 @@ export type {
9
9
  export * from './gyms';
10
10
  // collaborators types are in @gymspace/shared
11
11
  export * from './clients';
12
+ export * from './members';
12
13
  export * from './membership-plans';
13
14
  export * from './contracts';
14
15
  export * from './dashboard';
@@ -0,0 +1,133 @@
1
+ // Member invite types
2
+ export interface CreateMemberInviteDto {
3
+ sendWhatsApp?: boolean;
4
+ customMessage?: string;
5
+ expiresInHours?: number;
6
+ }
7
+
8
+ export interface MemberInvite {
9
+ id: string;
10
+ token: string;
11
+ code: string;
12
+ status: 'pending' | 'used' | 'expired';
13
+ expiresAt: string;
14
+ usedAt?: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ export interface MemberInviteResponse {
19
+ invite: MemberInvite;
20
+ whatsappSent?: boolean;
21
+ }
22
+
23
+ export interface MemberInviteValidationResponse {
24
+ valid: boolean;
25
+ status: 'pending' | 'used' | 'expired';
26
+ gym: {
27
+ name: string;
28
+ logo?: string;
29
+ address?: string;
30
+ slug: string;
31
+ };
32
+ client: {
33
+ email: string;
34
+ name: string;
35
+ };
36
+ invite: {
37
+ id: string;
38
+ expiresAt: string;
39
+ status: 'pending' | 'used' | 'expired';
40
+ };
41
+ }
42
+
43
+ // Member auth types
44
+ export interface MemberPinLoginDto {
45
+ identifier: string;
46
+ identifierType: 'phone' | 'email' | 'document';
47
+ documentType?: string;
48
+ pin: string;
49
+ gymSlug?: string;
50
+ gymId?: string;
51
+ rememberMe?: boolean;
52
+ }
53
+
54
+ export interface SetPinDto {
55
+ token: string;
56
+ pin: string;
57
+ pinConfirm: string;
58
+ }
59
+
60
+ export interface MemberLoginResponse {
61
+ accessToken: string;
62
+ refreshToken: string;
63
+ member: MemberProfile;
64
+ }
65
+
66
+ // Member profile types
67
+ export interface MemberProfile {
68
+ id: string;
69
+ gymId: string;
70
+ clientNumber: string;
71
+ name: string;
72
+ phone?: string;
73
+ email?: string;
74
+ documentValue?: string;
75
+ documentType?: string;
76
+ status: 'active' | 'inactive';
77
+ pinSet: boolean;
78
+ lastLoginAt?: string;
79
+ createdAt: string;
80
+ updatedAt: string;
81
+ }
82
+
83
+ // Membership types
84
+ export interface MemberMembership {
85
+ contract: {
86
+ id: string;
87
+ status: string;
88
+ startDate: string;
89
+ endDate: string;
90
+ };
91
+ plan: {
92
+ id: string;
93
+ name: string;
94
+ description?: string;
95
+ };
96
+ status: 'active' | 'inactive' | 'suspended' | 'cancelled';
97
+ daysRemaining: number;
98
+ checkInsRemaining?: number;
99
+ }
100
+
101
+ // Check-in types
102
+ export interface MemberCheckIn {
103
+ id: string;
104
+ timestamp: string;
105
+ createdAt: string;
106
+ contract?: {
107
+ id: string;
108
+ planName: string;
109
+ };
110
+ }
111
+
112
+ // QR code types
113
+ export interface QrTokenResponse {
114
+ qrToken: string;
115
+ expiresAt: string;
116
+ }
117
+
118
+ // Self check-in types
119
+ export interface CheckInResponse {
120
+ success: boolean;
121
+ message: string;
122
+ checkIn: {
123
+ id: string;
124
+ gymClientId: string;
125
+ gymId: string;
126
+ timestamp: string;
127
+ createdAt: string;
128
+ };
129
+ }
130
+
131
+ export interface CheckInDto {
132
+ qrToken: string;
133
+ }
@@ -12,6 +12,7 @@ export interface CreateMembershipPlanDto {
12
12
  features?: string[];
13
13
  status?: 'active' | 'inactive' | 'archived';
14
14
  assetsIds?: string[];
15
+ allowAccessToGymIds?: string[];
15
16
  }
16
17
 
17
18
  export interface UpdateMembershipPlanDto {
@@ -29,6 +30,7 @@ export interface UpdateMembershipPlanDto {
29
30
  status?: 'active' | 'inactive' | 'archived';
30
31
  isActive?: boolean;
31
32
  assetsIds?: string[];
33
+ allowAccessToGymIds?: string[];
32
34
  }
33
35
 
34
36
  export interface MembershipPlan {
@@ -55,6 +57,7 @@ export interface MembershipPlan {
55
57
  status: 'active' | 'inactive' | 'archived';
56
58
  isActive: boolean;
57
59
  assetsIds?: string[];
60
+ allowAccessToGymIds?: string[];
58
61
  createdAt: string;
59
62
  updatedAt: string;
60
63
  _count?: {
@@ -18,6 +18,7 @@ import {
18
18
  ResendResetCodeDto,
19
19
  ResendResetCodeResponseDto,
20
20
  } from '../models/auth';
21
+ import { MemberPinLoginDto, SetPinDto, MemberLoginResponse } from '../models/members';
21
22
  import { SubscriptionPlan } from '../models/subscriptions';
22
23
  import { RequestOptions } from '../types';
23
24
  import { BaseResource } from './base';
@@ -29,8 +30,16 @@ export class AuthResource extends BaseResource {
29
30
  return this.client.post(`${this.basePath}/register/owner`, data, options);
30
31
  }
31
32
 
32
- async login(data: LoginDto, options?: RequestOptions): Promise<LoginResponseDto> {
33
- return this.client.post<LoginResponseDto>(`${this.basePath}/login`, data, options);
33
+ async login(
34
+ data: LoginDto,
35
+ options?: RequestOptions,
36
+ rememberMe?: boolean,
37
+ ): Promise<LoginResponseDto> {
38
+ return this.client.post<LoginResponseDto>(
39
+ `${this.basePath}/login`,
40
+ { ...data, rememberMe },
41
+ options,
42
+ );
34
43
  }
35
44
 
36
45
  async refreshToken(refreshToken: string, options?: RequestOptions): Promise<LoginResponseDto> {
@@ -137,4 +146,49 @@ export class AuthResource extends BaseResource {
137
146
  options,
138
147
  );
139
148
  }
149
+
150
+ /**
151
+ * Member login with PIN
152
+ * POST /auth/members/pin/login
153
+ */
154
+ async memberPinLogin(
155
+ data: MemberPinLoginDto,
156
+ options?: RequestOptions,
157
+ ): Promise<MemberLoginResponse> {
158
+ return this.client.post<MemberLoginResponse>(
159
+ `${this.basePath}/members/pin/login`,
160
+ data,
161
+ options,
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Consume member invite and set initial PIN
167
+ * POST /auth/members/invite/consume
168
+ */
169
+ async consumeMemberInvite(
170
+ data: SetPinDto,
171
+ options?: RequestOptions,
172
+ ): Promise<{ success: boolean; message: string; gymSlug?: string }> {
173
+ return this.client.post<{ success: boolean; message: string; gymSlug?: string }>(
174
+ `${this.basePath}/members/invite/consume`,
175
+ data,
176
+ options,
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Refresh member access token
182
+ * POST /auth/members/refresh
183
+ */
184
+ async refreshMemberToken(
185
+ refreshToken: string,
186
+ options?: RequestOptions,
187
+ ): Promise<MemberLoginResponse> {
188
+ return this.client.post<MemberLoginResponse>(
189
+ `${this.basePath}/members/refresh`,
190
+ { refresh_token: refreshToken },
191
+ options,
192
+ );
193
+ }
140
194
  }