b5-api-client 0.0.25 → 0.0.27

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.
@@ -14,15 +14,40 @@ import {
14
14
  isSignInWithEmailLink,
15
15
  sendSignInLinkToEmail,
16
16
  signInWithEmailLink,
17
- Auth
17
+ Auth,
18
+ applyActionCode
18
19
  } from "firebase/auth";
19
- import { getMessaging, getToken, onMessage, MessagePayload } from "firebase/messaging";
20
- import { CreateUserRequest, KioscoinUser } from "../types";
21
- import P2PMarketplaceAPIClient from "../P2PMarketplaceAPIClient";
20
+ import { getMessaging, getToken, onMessage } from "firebase/messaging";
21
+ import { CreateUserRequest, KioscoinUser, UpdateUserSettingsRequest } from "../types";
22
+ import P2PMarketplaceAPIClient, { AuthTokenProvider } from "../P2PMarketplaceAPIClient";
22
23
  import { FirebaseLoginServiceConfig, LoginService } from "./LoginService";
23
24
  import { userContext } from './UserContext';
24
25
 
25
26
 
27
+ const DEFAULT_EMAIL_VERIFICATION_PAGE =
28
+ process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_URL ??
29
+ "http://localhost:3000/auth/email-verified";
30
+
31
+ const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, "");
32
+
33
+ const appendQueryParams = (
34
+ base: string,
35
+ params: Record<string, string | undefined>
36
+ ): string => {
37
+ const query = Object.entries(params)
38
+ .filter(([, value]) => value !== undefined && value !== null)
39
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`)
40
+ .join("&");
41
+
42
+ if (!query) {
43
+ return base;
44
+ }
45
+
46
+ const separator = base.includes("?") ? "&" : "?";
47
+ return `${base}${separator}${query}`;
48
+ };
49
+
50
+
26
51
  // Auth provider type for better type safety
27
52
  export enum AuthProvider {
28
53
  EMAIL = 'EMAIL',
@@ -124,18 +149,35 @@ export class FirebaseUnifiedService implements LoginService {
124
149
  private messaging: ReturnType<typeof getMessaging> | null = null;
125
150
  private auth: Auth | null = null;
126
151
  private readonly actionCodeSettings: { url: string; handleCodeInApp: boolean };
152
+ private readonly emailVerificationUrl: string;
153
+ private readonly emailVerificationContinueUrl?: string;
127
154
  private client: P2PMarketplaceAPIClient;
155
+ private readonly initializationPromise: Promise<void>;
128
156
 
129
157
  constructor(client: P2PMarketplaceAPIClient, config?: FirebaseLoginServiceConfig) {
130
158
  this.client = client;
131
- this.actionCodeSettings = config?.actionCodeSettings ?? {
132
- url: 'http://localhost:3000/auth/verify-email',
133
- handleCodeInApp: true,
159
+ const fallbackVerificationPage = config?.emailVerificationUrl ?? DEFAULT_EMAIL_VERIFICATION_PAGE;
160
+ this.emailVerificationUrl = trimTrailingSlash(fallbackVerificationPage);
161
+ this.emailVerificationContinueUrl = config?.emailVerificationContinueUrl;
162
+ const configuredActionSettings = config?.actionCodeSettings;
163
+ const defaultActionUrl =
164
+ configuredActionSettings?.url ??
165
+ process.env.REACT_APP_EMAIL_LINK_URL ??
166
+ process.env.NEXT_PUBLIC_EMAIL_LINK_URL ??
167
+ "http://localhost:3000/auth/verify-email";
168
+
169
+ this.actionCodeSettings = {
170
+ url: defaultActionUrl,
171
+ handleCodeInApp: configuredActionSettings?.handleCodeInApp ?? true,
134
172
  };
135
- this.initializeFirebase();
173
+ this.initializationPromise = this.initializeFirebase();
174
+ this.client.setAuthTokenProvider(this.buildAuthTokenProvider());
136
175
  }
137
176
 
138
177
  private async initializeFirebase() {
178
+ if (this.app) {
179
+ return;
180
+ }
139
181
  try {
140
182
  const firebaseConfig = await this.fetchFirebaseConfig();
141
183
  this.app = initializeApp(firebaseConfig);
@@ -145,6 +187,42 @@ export class FirebaseUnifiedService implements LoginService {
145
187
  }
146
188
  }
147
189
 
190
+ private async ensureAuthInstance(): Promise<Auth | null> {
191
+ try {
192
+ await this.initializationPromise;
193
+ } catch (_error) {
194
+ return null;
195
+ }
196
+
197
+ return this.auth;
198
+ }
199
+
200
+ private buildAuthTokenProvider(): AuthTokenProvider {
201
+ return {
202
+ getToken: async () => {
203
+ const auth = await this.ensureAuthInstance();
204
+ const currentUser = auth?.currentUser;
205
+ if (!currentUser) {
206
+ return null;
207
+ }
208
+ return currentUser.getIdToken();
209
+ },
210
+ refreshToken: async () => {
211
+ const auth = await this.ensureAuthInstance();
212
+ const currentUser = auth?.currentUser;
213
+ if (!currentUser) {
214
+ return null;
215
+ }
216
+ const refreshedToken = await currentUser.getIdToken(true);
217
+ userContext.updateUser({ idToken: refreshedToken });
218
+ return refreshedToken;
219
+ },
220
+ onRefreshFailure: (error) => {
221
+ console.error("Failed to refresh Firebase ID token:", error);
222
+ }
223
+ };
224
+ }
225
+
148
226
  private async fetchFirebaseConfig(): Promise<FirebaseConfig> {
149
227
  try {
150
228
  const response = await this.client.getConfig<{ firebase: FirebaseConfig }>();
@@ -155,6 +233,63 @@ export class FirebaseUnifiedService implements LoginService {
155
233
  }
156
234
  }
157
235
 
236
+ private buildEmailVerificationSettings(loginId: string) {
237
+ return {
238
+ url: this.buildEmailVerificationUrl(loginId),
239
+ handleCodeInApp: false,
240
+ };
241
+ }
242
+
243
+ private buildEmailVerificationUrl(loginId: string): string {
244
+ return appendQueryParams(this.emailVerificationUrl, {
245
+ loginId,
246
+ continueUrl: this.emailVerificationContinueUrl,
247
+ });
248
+ }
249
+
250
+ private async ensureBackendVerification(
251
+ firebaseUser: User,
252
+ backendUser: KioscoinUser,
253
+ idToken: string
254
+ ): Promise<KioscoinUser> {
255
+ const firebaseVerified = firebaseUser.emailVerified;
256
+ const backendVerified = backendUser.settings?.hasVerified ?? false;
257
+
258
+ if (!firebaseVerified || backendVerified) {
259
+ return backendUser;
260
+ }
261
+
262
+ if (!backendUser.id) {
263
+ console.warn("Missing backend user id when attempting to mark verification status.");
264
+ return backendUser;
265
+ }
266
+
267
+ const settingsUpdate: UpdateUserSettingsRequest["settings"] = {
268
+ paymentMethods: (backendUser.settings?.paymentMethods ?? []).map((method) => ({ ...method })),
269
+ hasVerified: true,
270
+ };
271
+
272
+ const preferredToken = backendUser.settings?.preferredTokenCode;
273
+ if (preferredToken !== undefined && preferredToken !== null) {
274
+ settingsUpdate.preferredTokenCode = preferredToken;
275
+ }
276
+
277
+ const request: UpdateUserSettingsRequest = {
278
+ userId: backendUser.id,
279
+ settings: settingsUpdate,
280
+ };
281
+
282
+ try {
283
+ const response = await this.client.updateUserSettings(request, {
284
+ Authorization: `Bearer ${idToken}`,
285
+ });
286
+ return response.users?.[0] ?? backendUser;
287
+ } catch (error) {
288
+ console.error("Failed to update backend verification status:", error);
289
+ return backendUser;
290
+ }
291
+ }
292
+
158
293
  // Messaging methods
159
294
  public async requestNotificationPermission(): Promise<string | null> {
160
295
  if (!this.app) {
@@ -210,23 +345,34 @@ export class FirebaseUnifiedService implements LoginService {
210
345
  // Authentication methods
211
346
  async signInWithEmail(email: string, password: string): Promise<AuthResult> {
212
347
  try {
213
- if (!this.auth) {
348
+ const auth = await this.ensureAuthInstance();
349
+ if (!auth) {
214
350
  console.error("Firebase Auth not initialized");
215
351
  return AuthResult.failure("Firebase Auth not initialized");
216
352
  }
217
- const userCredential = await signInWithEmailAndPassword(this.auth, email, password);
353
+ const userCredential = await signInWithEmailAndPassword(auth, email, password);
218
354
  const idToken = await userCredential.user.getIdToken();
219
355
  const backendUser = await this.getUserFromBackend(userCredential.user.uid, idToken);
220
356
 
221
357
  if (backendUser) {
222
- const authResult = this._createSuccessAuthResult(userCredential.user, backendUser, AuthProvider.EMAIL, idToken);
358
+ const syncedBackendUser = await this.ensureBackendVerification(
359
+ userCredential.user,
360
+ backendUser,
361
+ idToken
362
+ );
363
+ const authResult = this._createSuccessAuthResult(
364
+ userCredential.user,
365
+ syncedBackendUser,
366
+ AuthProvider.EMAIL,
367
+ idToken
368
+ );
223
369
  // Update UserContext
224
370
  userContext.setUser({
225
371
  id: userCredential.user.uid,
226
- username: backendUser.username || '',
372
+ username: syncedBackendUser.username || '',
227
373
  email: userCredential.user.email || '',
228
- idToken: idToken,
229
- isAdmin: backendUser.admin || false
374
+ idToken,
375
+ isAdmin: syncedBackendUser.admin || false
230
376
  });
231
377
  return authResult;
232
378
  } else {
@@ -244,16 +390,17 @@ export class FirebaseUnifiedService implements LoginService {
244
390
 
245
391
  async createUserWithEmail(email: string, password: string, username: string): Promise<AuthResult> {
246
392
  try {
247
- if (!this.auth) {
393
+ const auth = await this.ensureAuthInstance();
394
+ if (!auth) {
248
395
  return AuthResult.failure("Firebase Auth not initialized");
249
396
  }
250
397
 
251
398
  // First create the user in Firebase
252
- const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
399
+ const userCredential = await createUserWithEmailAndPassword(auth, email, password);
253
400
  const user = userCredential.user;
254
401
  const idToken = await user.getIdToken();
255
402
  // Send email verification
256
- await sendEmailVerification(user);
403
+ await sendEmailVerification(user, this.buildEmailVerificationSettings(user.uid));
257
404
 
258
405
  const backendUser = await this.createUserInBackend(user.uid, username, idToken);
259
406
 
@@ -270,20 +417,22 @@ export class FirebaseUnifiedService implements LoginService {
270
417
 
271
418
  async signInWithGoogle(): Promise<AuthResult> {
272
419
  try {
273
- if (!this.auth) {
420
+ const auth = await this.ensureAuthInstance();
421
+ if (!auth) {
274
422
  return AuthResult.failure("Firebase Auth not initialized");
275
423
  }
276
424
 
277
425
  const provider = new GoogleAuthProvider();
278
- const userCredential = await signInWithPopup(this.auth, provider);
426
+ const userCredential = await signInWithPopup(auth, provider);
279
427
  const user = userCredential.user;
428
+ const idToken = await user.getIdToken();
280
429
 
281
430
  // Get or create user in your Kotlin backend
282
- const backendUser = await this.getUserFromBackend(user.uid, user.displayName || '');
283
- const idToken = await user.getIdToken();
431
+ const backendUser = await this.getUserFromBackend(user.uid, idToken);
284
432
  if (backendUser) {
433
+ const syncedBackendUser = await this.ensureBackendVerification(user, backendUser, idToken);
285
434
  // Use the new helper method
286
- return this._createSuccessAuthResult(user, backendUser, AuthProvider.GOOGLE, idToken);
435
+ return this._createSuccessAuthResult(user, syncedBackendUser, AuthProvider.GOOGLE, idToken);
287
436
  } else {
288
437
  return AuthResult.failure('Failed to get or create user in backend');
289
438
  }
@@ -295,20 +444,22 @@ export class FirebaseUnifiedService implements LoginService {
295
444
 
296
445
  async signInWithFacebook(): Promise<AuthResult> {
297
446
  try {
298
- if (!this.auth) {
447
+ const auth = await this.ensureAuthInstance();
448
+ if (!auth) {
299
449
  return AuthResult.failure("Firebase Auth not initialized");
300
450
  }
301
451
 
302
452
  const provider = new FacebookAuthProvider();
303
- const userCredential = await signInWithPopup(this.auth, provider);
453
+ const userCredential = await signInWithPopup(auth, provider);
304
454
  const user = userCredential.user;
455
+ const idToken = await user.getIdToken();
305
456
 
306
457
  // Get or create user in your Kotlin backend
307
- const backendUser = await this.getUserFromBackend(user.uid, user.displayName || '');
308
- const idToken = await user.getIdToken();
458
+ const backendUser = await this.getUserFromBackend(user.uid, idToken);
309
459
  if (backendUser) {
460
+ const syncedBackendUser = await this.ensureBackendVerification(user, backendUser, idToken);
310
461
  // Use the new helper method
311
- return this._createSuccessAuthResult(user, backendUser, AuthProvider.FACEBOOK, idToken);
462
+ return this._createSuccessAuthResult(user, syncedBackendUser, AuthProvider.FACEBOOK, idToken);
312
463
  } else {
313
464
  return AuthResult.failure('Failed to get or create user in backend');
314
465
  }
@@ -320,20 +471,22 @@ export class FirebaseUnifiedService implements LoginService {
320
471
 
321
472
  async signInWithTwitter(): Promise<AuthResult> {
322
473
  try {
323
- if (!this.auth) {
474
+ const auth = await this.ensureAuthInstance();
475
+ if (!auth) {
324
476
  return AuthResult.failure("Firebase Auth not initialized");
325
477
  }
326
478
 
327
479
  const provider = new TwitterAuthProvider();
328
- const userCredential = await signInWithPopup(this.auth, provider);
480
+ const userCredential = await signInWithPopup(auth, provider);
329
481
  const user = userCredential.user;
482
+ const idToken = await user.getIdToken();
330
483
 
331
484
  // Get or create user in your Kotlin backend
332
- const backendUser = await this.getUserFromBackend(user.uid, user.displayName || '');
333
- const idToken = await user.getIdToken();
485
+ const backendUser = await this.getUserFromBackend(user.uid, idToken);
334
486
  if (backendUser) {
487
+ const syncedBackendUser = await this.ensureBackendVerification(user, backendUser, idToken);
335
488
  // Use the new helper method
336
- return this._createSuccessAuthResult(user, backendUser, AuthProvider.TWITTER, idToken);
489
+ return this._createSuccessAuthResult(user, syncedBackendUser, AuthProvider.TWITTER, idToken);
337
490
  } else {
338
491
  return AuthResult.failure('Failed to get or create user in backend');
339
492
  }
@@ -345,13 +498,14 @@ export class FirebaseUnifiedService implements LoginService {
345
498
 
346
499
  async sendEmailVerification(): Promise<boolean> {
347
500
  try {
348
- if (!this.auth) {
501
+ const auth = await this.ensureAuthInstance();
502
+ if (!auth) {
349
503
  return false;
350
504
  }
351
505
 
352
- const user = this.auth.currentUser;
506
+ const user = auth.currentUser;
353
507
  if (user) {
354
- await sendEmailVerification(user);
508
+ await sendEmailVerification(user, this.buildEmailVerificationSettings(user.uid));
355
509
  return true;
356
510
  }
357
511
  return false;
@@ -363,11 +517,12 @@ export class FirebaseUnifiedService implements LoginService {
363
517
 
364
518
  async sendPasswordResetEmail(email: string): Promise<boolean> {
365
519
  try {
366
- if (!this.auth) {
520
+ const auth = await this.ensureAuthInstance();
521
+ if (!auth) {
367
522
  return false;
368
523
  }
369
524
 
370
- await sendPasswordResetEmail(this.auth, email);
525
+ await sendPasswordResetEmail(auth, email);
371
526
  return true;
372
527
  } catch (error) {
373
528
  console.error('Error sending password reset email:', error);
@@ -377,11 +532,13 @@ export class FirebaseUnifiedService implements LoginService {
377
532
 
378
533
  async signOut(): Promise<void> {
379
534
  try {
380
- if (!this.auth) {
535
+ const auth = await this.ensureAuthInstance();
536
+ if (!auth) {
537
+ userContext.clearUser();
381
538
  return;
382
539
  }
383
540
 
384
- await signOut(this.auth);
541
+ await signOut(auth);
385
542
  // Clear UserContext
386
543
  userContext.clearUser();
387
544
  } catch (error) {
@@ -398,11 +555,12 @@ export class FirebaseUnifiedService implements LoginService {
398
555
 
399
556
  async sendSignInLinkToEmail(email: string): Promise<boolean> {
400
557
  try {
401
- if (!this.auth) {
558
+ const auth = await this.ensureAuthInstance();
559
+ if (!auth) {
402
560
  return false;
403
561
  }
404
562
 
405
- await sendSignInLinkToEmail(this.auth, email, this.actionCodeSettings);
563
+ await sendSignInLinkToEmail(auth, email, this.actionCodeSettings);
406
564
  // Save the email locally to use it later when the user clicks the link
407
565
  window.localStorage.setItem('emailForSignIn', email);
408
566
  return true;
@@ -421,26 +579,29 @@ export class FirebaseUnifiedService implements LoginService {
421
579
 
422
580
  async signInWithEmailLink(email: string): Promise<AuthResult> {
423
581
  try {
424
- if (!this.auth) {
582
+ const auth = await this.ensureAuthInstance();
583
+ if (!auth) {
425
584
  return AuthResult.failure("Firebase Auth not initialized");
426
585
  }
427
586
 
428
- if (!this.isSignInWithEmailLink()) {
587
+ if (!isSignInWithEmailLink(auth, window.location.href)) {
429
588
  return AuthResult.failure("Invalid sign-in link");
430
589
  }
431
590
 
432
- const userCredential = await signInWithEmailLink(this.auth, email, window.location.href);
591
+ const userCredential = await signInWithEmailLink(auth, email, window.location.href);
433
592
  const user = userCredential.user;
434
593
 
435
594
  // Clear the email from localStorage
436
595
  window.localStorage.removeItem('emailForSignIn');
437
596
 
438
- // Get or create user in your Kotlin backend
439
- const backendUser = await this.getUserFromBackend(user.uid, user.displayName || '');
440
597
  const idToken = await user.getIdToken();
598
+
599
+ // Get or create user in your Kotlin backend
600
+ const backendUser = await this.getUserFromBackend(user.uid, idToken);
441
601
  if (backendUser) {
602
+ const syncedBackendUser = await this.ensureBackendVerification(user, backendUser, idToken);
442
603
  // Use the new helper method
443
- return this._createSuccessAuthResult(user, backendUser, AuthProvider.EMAIL, idToken);
604
+ return this._createSuccessAuthResult(user, syncedBackendUser, AuthProvider.EMAIL, idToken);
444
605
  } else {
445
606
  return AuthResult.failure('Failed to get or create user in backend');
446
607
  }
@@ -454,6 +615,15 @@ export class FirebaseUnifiedService implements LoginService {
454
615
  return window.localStorage.getItem('emailForSignIn');
455
616
  }
456
617
 
618
+ async applyEmailVerificationCode(oobCode: string): Promise<void> {
619
+ const auth = await this.ensureAuthInstance();
620
+ if (!auth) {
621
+ throw new Error("Firebase Auth not initialized");
622
+ }
623
+
624
+ await applyActionCode(auth, oobCode);
625
+ }
626
+
457
627
  // Helper method to create success AuthResult
458
628
  private _createSuccessAuthResult(
459
629
  user: User,
@@ -473,10 +643,10 @@ export class FirebaseUnifiedService implements LoginService {
473
643
  }
474
644
 
475
645
  // Backend integration methods
476
- private async getUserFromBackend(userId: string, loginId: string): Promise<KioscoinUser | null> {
646
+ private async getUserFromBackend(userId: string, authToken: string): Promise<KioscoinUser | null> {
477
647
  try {
478
648
  const response = await this.client.getUser(userId, {
479
- Authorization: `Bearer ${loginId}`,
649
+ Authorization: `Bearer ${authToken}`,
480
650
  });
481
651
  return response.users[0];
482
652
  } catch (error) {
@@ -21,6 +21,8 @@ export interface FirebaseLoginServiceConfig {
21
21
  url: string;
22
22
  handleCodeInApp: boolean;
23
23
  };
24
+ emailVerificationUrl?: string;
25
+ emailVerificationContinueUrl?: string;
24
26
  }
25
27
 
26
28
  export function createLoginService(config: LoginServiceConfig): LoginService {
package/src/types.ts CHANGED
@@ -76,8 +76,9 @@ export interface UsersResponse {
76
76
  }
77
77
 
78
78
  export interface UserSettings {
79
- preferredTokenCode?: string;
79
+ preferredTokenCode?: string | null;
80
80
  paymentMethods: PaymentMethod[];
81
+ hasVerified?: boolean;
81
82
  }
82
83
 
83
84
  export interface UpdateUserSettingsRequest {
@@ -85,6 +86,10 @@ export interface UpdateUserSettingsRequest {
85
86
  settings: UserSettings;
86
87
  }
87
88
 
89
+ export interface VerifyEmailRequest {
90
+ loginId: string;
91
+ }
92
+
88
93
  export interface PaymentMethod {
89
94
  type: string;
90
95
  alias?: string;
@@ -406,3 +411,53 @@ export interface KioscoinOperationResponse {
406
411
  error?: OperationError;
407
412
  order?: Order;
408
413
  }
414
+
415
+ export type MessageContentType = 'TEXT' | 'ACTION' | 'EVENT' | 'NOTIFICATION';
416
+
417
+ export interface MessageContentBase {
418
+ type: MessageContentType;
419
+ }
420
+
421
+ export interface MessageContentText extends MessageContentBase {
422
+ type: 'TEXT';
423
+ text: string;
424
+ }
425
+
426
+ export interface MessageContentAction extends MessageContentBase {
427
+ type: 'ACTION';
428
+ content: string;
429
+ }
430
+
431
+ export interface MessageContentEvent extends MessageContentBase {
432
+ type: 'EVENT';
433
+ event: OrderEvent;
434
+ }
435
+
436
+ export interface MessageContentNotification extends MessageContentBase {
437
+ type: 'NOTIFICATION';
438
+ notification: NotificationDto;
439
+ }
440
+
441
+ export type MessageContent =
442
+ | MessageContentText
443
+ | MessageContentAction
444
+ | MessageContentEvent
445
+ | MessageContentNotification;
446
+
447
+ export interface Message {
448
+ sender: string;
449
+ timestamp: number;
450
+ content: MessageContent;
451
+ }
452
+
453
+ export interface NotificationDto {
454
+ id: string;
455
+ userId: string;
456
+ message: Message;
457
+ createdAt: string;
458
+ readAt?: string | null;
459
+ }
460
+
461
+ export interface NotificationsResponse {
462
+ notifications: NotificationDto[];
463
+ }