nfx-ui 0.27.0 → 0.29.0

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/src/hooks/auth.ts CHANGED
@@ -7,7 +7,7 @@ import type { CurrentProfileResult, Login, Profile, Signup } from "nfx-ui/types"
7
7
  import { useEffect } from "react";
8
8
  import { useMutation } from "@tanstack/react-query";
9
9
  import { useAuthRepository } from "nfx-ui/apis/repositories";
10
- import { AUTH_EMAILS, AUTH_ME, AUTH_PHONES, AUTH_PROFILES } from "nfx-ui/constants";
10
+ import { AUTH_EMAILS, AUTH_ME, AUTH_OWNER_AUTHORITIES, AUTH_OWNER_FORGERS, AUTH_PHONES, AUTH_PROFILE_SEARCH, AUTH_PROFILES, AUTH_PUBLIC_CARD } from "nfx-ui/constants";
11
11
  import { AuthSignupPlatformEnum, LanguageEnum, ProfileKindEnum } from "nfx-ui/enums";
12
12
  import { authEventEmitter, authEvents } from "nfx-ui/events/auth";
13
13
  import { systemEventEmitter } from "nfx-ui/events/system";
@@ -156,6 +156,33 @@ export const useLoginWithEmail = () => {
156
156
  });
157
157
  };
158
158
 
159
+ export const useLoginWithPhone = () => {
160
+ const auth = useAuthRepository();
161
+ return useMutation({
162
+ mutationFn: async ({ rememberMe, ...params }: { phone: string; password: string; rememberMe: boolean }) =>
163
+ auth.LoginWithPhone({
164
+ ...params,
165
+ deviceId: await ensureDeviceIdStorage(),
166
+ }),
167
+ onSuccess: (result, variables) => {
168
+ if (!result?.accessToken) return;
169
+ setCurrentProfileId(EMPTY_PROFILE_ID);
170
+ setTokens(
171
+ {
172
+ accessToken: result.accessToken,
173
+ refreshToken: safeStringable(result.refreshToken),
174
+ },
175
+ { rememberMe: variables.rememberMe },
176
+ );
177
+ setIsAuthValid(true);
178
+ if (result.accountId) setCurrentAccountId(result.accountId);
179
+ },
180
+ onError: (error: AxiosError) => {
181
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useLoginWithPhone] error"));
182
+ },
183
+ });
184
+ };
185
+
159
186
  export const useSelectProfile = () => {
160
187
  const auth = useAuthRepository();
161
188
  return useMutation({
@@ -203,14 +230,31 @@ export const usePatchProfile = (options?: { silent?: boolean }) => {
203
230
  export const useUpdateProfileSettings = () => {
204
231
  const auth = useAuthRepository();
205
232
  const { kind } = useAuthQueryScope();
233
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
206
234
  return useMutation({
207
235
  mutationFn: (body: Profile.Request.PatchProfileSettings) => auth.PatchProfileSettings(kind, body),
236
+ onSuccess: () => {
237
+ const aID = AuthStore.getState().currentAccountId;
238
+ authEventEmitter.emit(authEvents.UPDATE_ACCOUNT_SUCCESS, aID);
239
+ systemEventEmitter.showSuccess(t("updateSettingsSuccess", { defaultValue: "Settings updated." }));
240
+ },
208
241
  onError: (error: AxiosError) => {
209
242
  systemEventEmitter.showError(getApiErrorMessage(error, "[useUpdateProfileSettings] error"));
210
243
  },
211
244
  });
212
245
  };
213
246
 
247
+ export const useUpdatePreference = () => {
248
+ const auth = useAuthRepository();
249
+ const { kind } = useAuthQueryScope();
250
+ return useMutation({
251
+ mutationFn: (preference: string) => auth.UpdatePreference(kind, preference),
252
+ onError: (error: AxiosError) => {
253
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useUpdatePreference] error"));
254
+ },
255
+ });
256
+ };
257
+
214
258
  export const useListProfiles = <K extends ProfileKindEnum>(kind: K) => {
215
259
  const auth = useAuthRepository();
216
260
  const { aID, isAuthValid } = useAuthQueryScope();
@@ -269,6 +313,10 @@ export const useConfirmProfileAvatar = () => {
269
313
  const { kind } = useAuthQueryScope();
270
314
  return useMutation({
271
315
  mutationFn: (body: Profile.Request.ConfirmProfileAvatar) => auth.ConfirmProfileAvatar(kind, body),
316
+ onSuccess: () => {
317
+ const aID = AuthStore.getState().currentAccountId;
318
+ authEventEmitter.emit(authEvents.UPDATE_ACCOUNT_SUCCESS, aID);
319
+ },
272
320
  onError: (error: AxiosError) => {
273
321
  systemEventEmitter.showError(getApiErrorMessage(error, "[useConfirmProfileAvatar] error"));
274
322
  },
@@ -278,8 +326,14 @@ export const useConfirmProfileAvatar = () => {
278
326
  export const useClearProfileAvatar = () => {
279
327
  const auth = useAuthRepository();
280
328
  const { kind } = useAuthQueryScope();
329
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
281
330
  return useMutation({
282
331
  mutationFn: () => auth.ClearProfileAvatar(kind),
332
+ onSuccess: () => {
333
+ const aID = AuthStore.getState().currentAccountId;
334
+ authEventEmitter.emit(authEvents.UPDATE_ACCOUNT_SUCCESS, aID);
335
+ systemEventEmitter.showSuccess(t("clearAvatarSuccess", { defaultValue: "Avatar removed." }));
336
+ },
283
337
  onError: (error: AxiosError) => {
284
338
  systemEventEmitter.showError(getApiErrorMessage(error, "[useClearProfileAvatar] error"));
285
339
  },
@@ -441,5 +495,177 @@ export const useConfirmProfileBackgrounds = () => {
441
495
  });
442
496
  };
443
497
 
498
+ export const useCreatePhone = () => {
499
+ const auth = useAuthRepository();
500
+ const accountId = AuthStore.getState().currentAccountId;
501
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
502
+ return useMutation({
503
+ mutationFn: (body: Login.Request.CreatePhone) => auth.CreatePhone(body),
504
+ onSuccess: () => {
505
+ authEventEmitter.invalidatePhones(accountId);
506
+ systemEventEmitter.showSuccess(t("createPhoneSuccess", { defaultValue: "Phone added." }));
507
+ },
508
+ onError: (error: AxiosError) => {
509
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useCreatePhone] error"));
510
+ },
511
+ });
512
+ };
513
+
514
+ export const useSendPhoneVerificationCode = () => {
515
+ const auth = useAuthRepository();
516
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
517
+ return useMutation({
518
+ mutationFn: (phoneId: string) => auth.SendPhoneVerificationCode(phoneId),
519
+ onSuccess: () => {
520
+ systemEventEmitter.showSuccess(t("sendVerificationCodeSuccess", { defaultValue: "Code sent" }));
521
+ },
522
+ onError: (error: AxiosError) => {
523
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useSendPhoneVerificationCode] error"));
524
+ },
525
+ });
526
+ };
527
+
528
+ export const useVerifyPhone = () => {
529
+ const auth = useAuthRepository();
530
+ const accountId = AuthStore.getState().currentAccountId;
531
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
532
+ return useMutation({
533
+ mutationFn: ({ phoneId, verificationCode }: { phoneId: string; verificationCode: string }) =>
534
+ auth.VerifyPhone(phoneId, { verificationCode }),
535
+ onSuccess: () => {
536
+ authEventEmitter.invalidatePhones(accountId);
537
+ systemEventEmitter.showSuccess(t("verifyPhoneSuccess", { defaultValue: "Phone verified." }));
538
+ },
539
+ onError: (error: AxiosError) => {
540
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useVerifyPhone] error"));
541
+ },
542
+ });
543
+ };
544
+
545
+ export const useUpdatePhone = () => {
546
+ const auth = useAuthRepository();
547
+ const accountId = AuthStore.getState().currentAccountId;
548
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
549
+ return useMutation({
550
+ mutationFn: ({ phoneId, phone }: { phoneId: string; phone: string }) => auth.UpdatePhone(phoneId, { phone }),
551
+ onSuccess: () => {
552
+ authEventEmitter.invalidatePhones(accountId);
553
+ systemEventEmitter.showSuccess(t("updatePhoneSuccess", { defaultValue: "Phone updated." }));
554
+ },
555
+ onError: (error: AxiosError) => {
556
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useUpdatePhone] error"));
557
+ },
558
+ });
559
+ };
560
+
561
+ export const useSetPrimaryPhone = () => {
562
+ const auth = useAuthRepository();
563
+ const accountId = AuthStore.getState().currentAccountId;
564
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
565
+ return useMutation({
566
+ mutationFn: (phoneId: string) => auth.SetPrimaryPhone(phoneId),
567
+ onSuccess: () => {
568
+ authEventEmitter.invalidatePhones(accountId);
569
+ authEventEmitter.emit(authEvents.UPDATE_ACCOUNT_SUCCESS, accountId);
570
+ systemEventEmitter.showSuccess(t("setPrimaryPhoneSuccess", { defaultValue: "Primary phone updated." }));
571
+ },
572
+ onError: (error: AxiosError) => {
573
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useSetPrimaryPhone] error"));
574
+ },
575
+ });
576
+ };
577
+
578
+ export const useDeletePhone = () => {
579
+ const auth = useAuthRepository();
580
+ const accountId = AuthStore.getState().currentAccountId;
581
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
582
+ return useMutation({
583
+ mutationFn: (phoneId: string) => auth.DeletePhone(phoneId),
584
+ onSuccess: () => {
585
+ authEventEmitter.invalidatePhones(accountId);
586
+ systemEventEmitter.showSuccess(t("deletePhoneSuccess", { defaultValue: "Phone removed." }));
587
+ },
588
+ onError: (error: AxiosError) => {
589
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useDeletePhone] error"));
590
+ },
591
+ });
592
+ };
593
+
594
+ export const useSearchForgerProfiles = (query: string) => {
595
+ const auth = useAuthRepository();
596
+ const { aID, isAuthValid } = useAuthQueryScope();
597
+ const trimmed = query.trim();
598
+ return useUnifiedQuery(
599
+ ({ query: q }) => auth.SearchForgerProfiles({ query: q, limit: 50, offset: 0 }),
600
+ AUTH_PROFILE_SEARCH(aID, ProfileKindEnum.FORGER, trimmed),
601
+ { query: trimmed },
602
+ { enabled: isAuthValid && Boolean(aID) && trimmed.length > 0 },
603
+ );
604
+ };
605
+
606
+ export const useSearchAuthorityProfiles = (query: string) => {
607
+ const auth = useAuthRepository();
608
+ const { aID, isAuthValid } = useAuthQueryScope();
609
+ const trimmed = query.trim();
610
+ return useUnifiedQuery(
611
+ ({ query: q }) => auth.SearchAuthorityProfiles({ query: q, limit: 50, offset: 0 }),
612
+ AUTH_PROFILE_SEARCH(aID, ProfileKindEnum.AUTHORITY, trimmed),
613
+ { query: trimmed },
614
+ { enabled: isAuthValid && Boolean(aID) && trimmed.length > 0 },
615
+ );
616
+ };
617
+
618
+ export const useGetPublicProfileCard = (profileId: string) => {
619
+ const auth = useAuthRepository();
620
+ const { isAuthValid } = useAuthQueryScope();
621
+ return useUnifiedQuery(
622
+ () => auth.GetPublicProfileCard(profileId),
623
+ AUTH_PUBLIC_CARD(profileId),
624
+ undefined,
625
+ { enabled: isAuthValid && Boolean(profileId) },
626
+ );
627
+ };
628
+
629
+ export const useListOwnerForgerProfiles = (query = "") => {
630
+ const auth = useAuthRepository();
631
+ const { aID, isAuthValid } = useAuthQueryScope();
632
+ const trimmed = query.trim();
633
+ return useUnifiedQuery(
634
+ ({ query: q }) => auth.ListOwnerForgerProfiles({ limit: 50, offset: 0, query: q || undefined }),
635
+ [...AUTH_OWNER_FORGERS(aID), trimmed],
636
+ { query: trimmed },
637
+ { enabled: isAuthValid && Boolean(aID) },
638
+ );
639
+ };
640
+
641
+ export const useListOwnerAuthorityProfiles = (query = "") => {
642
+ const auth = useAuthRepository();
643
+ const { aID, isAuthValid } = useAuthQueryScope();
644
+ const trimmed = query.trim();
645
+ return useUnifiedQuery(
646
+ ({ query: q }) => auth.ListOwnerAuthorityProfiles({ limit: 50, offset: 0, query: q || undefined }),
647
+ [...AUTH_OWNER_AUTHORITIES(aID), trimmed],
648
+ { query: trimmed },
649
+ { enabled: isAuthValid && Boolean(aID) },
650
+ );
651
+ };
652
+
653
+ export const useUpdateAuthorityProfileRoles = () => {
654
+ const auth = useAuthRepository();
655
+ const accountId = AuthStore.getState().currentAccountId;
656
+ const { t } = useTranslation("hooks", { keyPrefix: "account" });
657
+ return useMutation({
658
+ mutationFn: ({ profileId, authorityRoles }: { profileId: string; authorityRoles: Profile.Request.UpdateAuthorityProfileRoles["authorityRoles"] }) =>
659
+ auth.UpdateAuthorityProfileRoles(profileId, { authorityRoles }),
660
+ onSuccess: () => {
661
+ authEventEmitter.invalidateOwner(accountId);
662
+ systemEventEmitter.showSuccess(t("updateRolesSuccess", { defaultValue: "Roles updated." }));
663
+ },
664
+ onError: (error: AxiosError) => {
665
+ systemEventEmitter.showError(getApiErrorMessage(error, "[useUpdateAuthorityProfileRoles] error"));
666
+ },
667
+ });
668
+ };
669
+
444
670
  export { hasSelectedProfile, AuthStore, useAuthStore };
445
671
  export { LanguageEnum, AuthSignupPlatformEnum };
@@ -30,3 +30,28 @@ export const useInitLoginForm = () => {
30
30
  },
31
31
  });
32
32
  };
33
+
34
+ export function createLoginWithPhoneSchema(t: TFunction) {
35
+ return z.object({
36
+ phone: z.string().min(1, t("form.validation.phoneRequired")).min(6, t("form.validation.phoneInvalid")),
37
+ password: z.string().min(1, t("form.validation.passwordRequired")),
38
+ rememberMe: z.boolean().optional(),
39
+ });
40
+ }
41
+
42
+ export type LoginWithPhoneFormData = z.infer<ReturnType<typeof createLoginWithPhoneSchema>>;
43
+
44
+ export const useInitLoginWithPhoneForm = () => {
45
+ const { t } = useTranslation("pages.Account.Login");
46
+ const schema = createLoginWithPhoneSchema(t);
47
+
48
+ return useForm<LoginWithPhoneFormData>({
49
+ resolver: zodResolver(schema),
50
+ mode: "onChange",
51
+ defaultValues: {
52
+ phone: "",
53
+ password: "",
54
+ rememberMe: false,
55
+ },
56
+ });
57
+ };
@@ -319,6 +319,58 @@ meter {
319
319
  border-radius: max(var(--tab-inner-border-radius), var(--radius-full)) !important;
320
320
  }
321
321
 
322
+ /*
323
+ * Tabs hover: PulsoLink-WEB pattern. Radix paints both the active underline
324
+ * (::before) and TabsTriggerInner, which reads as two color layers. Keep the
325
+ * bar; only fill the inner on inactive hover. Active hover stays underline-only.
326
+ */
327
+ .radix-themes .rt-BaseTabListTrigger::before {
328
+ content: "";
329
+ height: 2px;
330
+ position: absolute;
331
+ bottom: 0;
332
+ left: 0;
333
+ right: 0;
334
+ background-color: var(--accent-indicator);
335
+ transform: scaleX(0);
336
+ transform-origin: center;
337
+ transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
338
+ pointer-events: none;
339
+ }
340
+
341
+ .radix-themes .rt-BaseTabListTrigger[data-state="active"]::before,
342
+ .radix-themes .rt-BaseTabListTrigger[data-active]::before {
343
+ transform: scaleX(1);
344
+ }
345
+
346
+ .radix-themes .rt-BaseTabListTrigger[data-state="active"] .rt-BaseTabListTriggerInner,
347
+ .radix-themes .rt-BaseTabListTrigger[data-active] .rt-BaseTabListTriggerInner {
348
+ font-weight: var(--font-weight-bold);
349
+ background-color: transparent;
350
+ }
351
+
352
+ @media (hover: hover) {
353
+ .radix-themes .rt-BaseTabListTrigger:hover::before {
354
+ transform: scaleX(1);
355
+ }
356
+
357
+ .radix-themes :where(.rt-BaseTabListTrigger:hover:not([data-state="active"]):not([data-active])) :where(.rt-BaseTabListTriggerInner) {
358
+ background-color: var(--accent-a3);
359
+ color: var(--accent-11);
360
+ }
361
+
362
+ .radix-themes .rt-BaseTabListTrigger[data-state="active"]:hover .rt-BaseTabListTriggerInner,
363
+ .radix-themes .rt-BaseTabListTrigger[data-active]:hover .rt-BaseTabListTriggerInner {
364
+ background-color: transparent;
365
+ }
366
+ }
367
+
368
+ @media (prefers-reduced-motion: reduce) {
369
+ .radix-themes .rt-BaseTabListTrigger::before {
370
+ transition: none;
371
+ }
372
+ }
373
+
322
374
  .radix-themes[data-radius="full"] .rt-Card {
323
375
  --card-border-radius: var(--radius-6);
324
376
  }