@b3dotfun/sdk 0.1.69-alpha.20 → 0.1.69-alpha.22

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.
Files changed (60) hide show
  1. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
  2. package/dist/cjs/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  3. package/dist/cjs/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  4. package/dist/cjs/anyspend/react/components/checkout/CartSummary.js +13 -11
  5. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  6. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
  7. package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  8. package/dist/cjs/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
  9. package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  10. package/dist/cjs/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
  11. package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  12. package/dist/cjs/anyspend/react/components/checkout/PriceSkeleton.js +9 -0
  13. package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  14. package/dist/cjs/anyspend/react/components/checkout/ShippingSelector.js +3 -2
  15. package/dist/cjs/global-account/react/components/SignInWithB3/BetterAuthResetPassword.js +3 -2
  16. package/dist/cjs/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +13 -3
  17. package/dist/cjs/global-account/react/components/SignInWithB3/components/PasswordInput.d.ts +10 -0
  18. package/dist/cjs/global-account/react/components/SignInWithB3/components/PasswordInput.js +10 -0
  19. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +6 -3
  20. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +6 -5
  21. package/dist/esm/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  22. package/dist/esm/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  23. package/dist/esm/anyspend/react/components/checkout/CartSummary.js +13 -11
  24. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  25. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.js +5 -4
  26. package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  27. package/dist/esm/anyspend/react/components/checkout/CheckoutFormPanel.js +2 -2
  28. package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  29. package/dist/esm/anyspend/react/components/checkout/DiscountCodeInput.js +3 -6
  30. package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  31. package/dist/esm/anyspend/react/components/checkout/PriceSkeleton.js +6 -0
  32. package/dist/esm/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  33. package/dist/esm/anyspend/react/components/checkout/ShippingSelector.js +3 -2
  34. package/dist/esm/global-account/react/components/SignInWithB3/BetterAuthResetPassword.js +4 -3
  35. package/dist/esm/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +14 -4
  36. package/dist/esm/global-account/react/components/SignInWithB3/components/PasswordInput.d.ts +10 -0
  37. package/dist/esm/global-account/react/components/SignInWithB3/components/PasswordInput.js +7 -0
  38. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +6 -3
  39. package/dist/styles/index.css +1 -1
  40. package/dist/types/anyspend/react/components/checkout/CartItemRow.d.ts +2 -1
  41. package/dist/types/anyspend/react/components/checkout/CartSummary.d.ts +6 -4
  42. package/dist/types/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +3 -1
  43. package/dist/types/anyspend/react/components/checkout/CheckoutFormPanel.d.ts +3 -1
  44. package/dist/types/anyspend/react/components/checkout/DiscountCodeInput.d.ts +3 -1
  45. package/dist/types/anyspend/react/components/checkout/PriceSkeleton.d.ts +5 -0
  46. package/dist/types/anyspend/react/components/checkout/ShippingSelector.d.ts +3 -1
  47. package/dist/types/global-account/react/components/SignInWithB3/components/PasswordInput.d.ts +10 -0
  48. package/package.json +1 -1
  49. package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +10 -4
  50. package/src/anyspend/react/components/checkout/CartItemRow.tsx +2 -1
  51. package/src/anyspend/react/components/checkout/CartSummary.tsx +24 -20
  52. package/src/anyspend/react/components/checkout/CheckoutCartPanel.tsx +12 -3
  53. package/src/anyspend/react/components/checkout/CheckoutFormPanel.tsx +5 -0
  54. package/src/anyspend/react/components/checkout/DiscountCodeInput.tsx +15 -5
  55. package/src/anyspend/react/components/checkout/PriceSkeleton.tsx +19 -0
  56. package/src/anyspend/react/components/checkout/ShippingSelector.tsx +5 -1
  57. package/src/global-account/react/components/SignInWithB3/BetterAuthResetPassword.tsx +6 -7
  58. package/src/global-account/react/components/SignInWithB3/BetterAuthSignIn.tsx +20 -6
  59. package/src/global-account/react/components/SignInWithB3/components/PasswordInput.tsx +62 -0
  60. package/src/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.tsx +9 -4
@@ -0,0 +1,5 @@
1
+ interface PriceSkeletonProps {
2
+ className?: string;
3
+ }
4
+ export declare function PriceSkeleton({ className }: PriceSkeletonProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -5,7 +5,9 @@ interface ShippingSelectorProps {
5
5
  onSelect: (option: ShippingOption) => void;
6
6
  tokenSymbol?: string;
7
7
  tokenDecimals?: number;
8
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
9
+ pricesLoading?: boolean;
8
10
  className?: string;
9
11
  }
10
- export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function ShippingSelector({ options, selectedId, onSelect, tokenSymbol, tokenDecimals, pricesLoading, className, }: ShippingSelectorProps): import("react/jsx-runtime").JSX.Element | null;
11
13
  export {};
@@ -0,0 +1,10 @@
1
+ interface PasswordInputProps {
2
+ value: string;
3
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
4
+ disabled?: boolean;
5
+ placeholder?: string;
6
+ onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
7
+ className?: string;
8
+ }
9
+ export declare function PasswordInput({ value, onChange, disabled, placeholder, onKeyDown, className, }: PasswordInputProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.69-alpha.20",
3
+ "version": "0.1.69-alpha.22",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -295,10 +295,14 @@ export function AnySpendCheckout({
295
295
  variablePricingAmount,
296
296
  ]);
297
297
 
298
- // Get destination token metadata
299
- const { data: tokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
300
- const tokenSymbol = tokenData?.symbol || "";
301
- const tokenDecimals = tokenData?.decimals || 18;
298
+ // Get destination token metadata. While loading, skip price-dependent UI
299
+ // since the 18-decimal fallback is wrong for non-18 tokens (e.g. USDC).
300
+ const { data: tokenData, isLoading: isTokenDataLoading } = useTokenData(
301
+ destinationTokenChainId,
302
+ destinationTokenAddress,
303
+ );
304
+ const tokenSymbol = tokenData?.symbol ?? "";
305
+ const tokenDecimals = tokenData?.decimals ?? 18;
302
306
 
303
307
  // Resolve USD equivalent for non-stablecoin tokens (shown in cart summary)
304
308
  const isStablecoin = useMemo(() => {
@@ -412,6 +416,7 @@ export function AnySpendCheckout({
412
416
  validateDiscount={validateDiscount}
413
417
  tokenSymbol={tokenSymbol}
414
418
  tokenDecimals={tokenDecimals}
419
+ pricesLoading={isTokenDataLoading}
415
420
  classes={classes}
416
421
  formData={formData}
417
422
  onFormDataChange={handleFormDataChange}
@@ -456,6 +461,7 @@ export function AnySpendCheckout({
456
461
  totalAmount={computedTotal}
457
462
  tokenSymbol={tokenSymbol}
458
463
  tokenDecimals={tokenDecimals}
464
+ pricesLoading={isTokenDataLoading}
459
465
  organizationName={organizationName}
460
466
  organizationLogo={organizationLogo}
461
467
  classes={classes}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
+ import type { ReactNode } from "react";
4
5
  import type { CheckoutItem, AnySpendCheckoutClasses } from "./AnySpendCheckout";
5
6
 
6
7
  interface CartItemRowProps {
7
8
  item: CheckoutItem;
8
- formattedPrice: string;
9
+ formattedPrice: ReactNode;
9
10
  classes?: AnySpendCheckoutClasses;
10
11
  }
11
12
 
@@ -2,13 +2,12 @@
2
2
 
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
4
  import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number";
5
- import { useMemo } from "react";
6
- import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
7
- import type { CheckoutSummaryLine } from "./AnySpendCheckout";
5
+ import { useMemo, type ReactNode } from "react";
6
+ import type { AnySpendCheckoutClasses, CheckoutSummaryLine } from "./AnySpendCheckout";
8
7
 
9
8
  interface CartSummaryProps {
10
9
  /** Formatted total (final amount after all adjustments) */
11
- total: string;
10
+ total: ReactNode;
12
11
  tokenSymbol?: string;
13
12
  classes?: AnySpendCheckoutClasses;
14
13
  /** Formatted subtotal (sum of items only — shown when adjustments exist) */
@@ -20,6 +19,8 @@ interface CartSummaryProps {
20
19
  summaryLines?: CheckoutSummaryLine[];
21
20
  /** Formatted USD equivalent (e.g. "$5.56") — shown for non-stablecoin tokens */
22
21
  usdEquivalent?: string | null;
22
+ /** When true, hide adjustment rows and USD equivalent (caller passes a skeleton as `total`). */
23
+ pricesLoading?: boolean;
23
24
  }
24
25
 
25
26
  export function CartSummary({
@@ -33,35 +34,38 @@ export function CartSummary({
33
34
  discount,
34
35
  summaryLines,
35
36
  usdEquivalent,
37
+ pricesLoading = false,
36
38
  }: CartSummaryProps) {
37
- const hasAdjustments =
38
- !!shipping?.amount || !!tax?.amount || !!discount?.amount || (summaryLines && summaryLines.length > 0);
39
-
40
39
  const formattedShipping = useMemo(
41
- () => (shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null),
42
- [shipping?.amount, tokenDecimals],
40
+ () => (!pricesLoading && shipping?.amount ? formatTokenAmount(safeBigInt(shipping.amount), tokenDecimals) : null),
41
+ [pricesLoading, shipping?.amount, tokenDecimals],
43
42
  );
44
43
 
45
44
  const formattedTax = useMemo(
46
- () => (tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null),
47
- [tax?.amount, tokenDecimals],
45
+ () => (!pricesLoading && tax?.amount ? formatTokenAmount(safeBigInt(tax.amount), tokenDecimals) : null),
46
+ [pricesLoading, tax?.amount, tokenDecimals],
48
47
  );
49
48
 
50
49
  const formattedDiscount = useMemo(
51
- () => (discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null),
52
- [discount?.amount, tokenDecimals],
50
+ () => (!pricesLoading && discount?.amount ? formatTokenAmount(safeBigInt(discount.amount), tokenDecimals) : null),
51
+ [pricesLoading, discount?.amount, tokenDecimals],
53
52
  );
54
53
 
55
54
  const formattedSummaryLines = useMemo(
56
55
  () =>
57
- summaryLines?.map(line => ({
58
- ...line,
59
- formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
60
- isNegative: safeBigInt(line.amount) < BigInt(0),
61
- })),
62
- [summaryLines, tokenDecimals],
56
+ pricesLoading
57
+ ? undefined
58
+ : summaryLines?.map(line => ({
59
+ ...line,
60
+ formattedAmount: formatTokenAmount(safeBigInt(line.amount), tokenDecimals),
61
+ isNegative: safeBigInt(line.amount) < BigInt(0),
62
+ })),
63
+ [pricesLoading, summaryLines, tokenDecimals],
63
64
  );
64
65
 
66
+ const hasAdjustments =
67
+ !!formattedShipping || !!formattedTax || !!formattedDiscount || !!formattedSummaryLines?.length;
68
+
65
69
  return (
66
70
  <div className={cn("border-t border-gray-200 pt-3 dark:border-neutral-700", classes?.cartSummary)}>
67
71
  {/* Subtotal — only shown when adjustments exist */}
@@ -147,7 +151,7 @@ export function CartSummary({
147
151
  {total} {tokenSymbol}
148
152
  </span>
149
153
  </div>
150
- {usdEquivalent && (
154
+ {!pricesLoading && usdEquivalent && (
151
155
  <div className="flex justify-end">
152
156
  <span className="text-xs text-gray-400 dark:text-gray-500">~{usdEquivalent} USD</span>
153
157
  </div>
@@ -7,12 +7,15 @@ import type { CheckoutItem, CheckoutSummaryLine, AnySpendCheckoutClasses } from
7
7
  import { CartItemRow } from "./CartItemRow";
8
8
  import { CartSummary } from "./CartSummary";
9
9
  import { PoweredByBranding } from "./PoweredByBranding";
10
+ import { PriceSkeleton } from "./PriceSkeleton";
10
11
 
11
12
  interface CheckoutCartPanelProps {
12
13
  items: CheckoutItem[];
13
14
  totalAmount: string;
14
15
  tokenSymbol?: string;
15
16
  tokenDecimals?: number;
17
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
18
+ pricesLoading?: boolean;
16
19
  organizationName?: string;
17
20
  organizationLogo?: string;
18
21
  classes?: AnySpendCheckoutClasses;
@@ -31,6 +34,7 @@ export function CheckoutCartPanel({
31
34
  totalAmount,
32
35
  tokenSymbol = "",
33
36
  tokenDecimals = 18,
37
+ pricesLoading = false,
34
38
  organizationName,
35
39
  organizationLogo,
36
40
  classes,
@@ -56,7 +60,7 @@ export function CheckoutCartPanel({
56
60
  }, [items, tokenDecimals]);
57
61
 
58
62
  return (
59
- <div className={cn("anyspend-cart-panel flex flex-col", classes?.cartPanel)}>
63
+ <div className={cn("anyspend-cart-panel flex flex-col", classes?.cartPanel)} aria-busy={pricesLoading || undefined}>
60
64
  <h2
61
65
  className={cn(
62
66
  "anyspend-cart-title mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100",
@@ -69,14 +73,18 @@ export function CheckoutCartPanel({
69
73
  <div className="anyspend-cart-items divide-y divide-gray-100 dark:divide-gray-800">
70
74
  {items.map((item, index) => {
71
75
  const itemTotal = safeBigInt(item.amount) * BigInt(item.quantity);
72
- const formattedPrice = `${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`;
76
+ const formattedPrice: ReactNode = pricesLoading ? (
77
+ <PriceSkeleton />
78
+ ) : (
79
+ `${formatTokenAmount(itemTotal, tokenDecimals)} ${tokenSymbol}`
80
+ );
73
81
 
74
82
  return <CartItemRow key={item.id || index} item={item} formattedPrice={formattedPrice} classes={classes} />;
75
83
  })}
76
84
  </div>
77
85
 
78
86
  <CartSummary
79
- total={formattedTotal}
87
+ total={pricesLoading ? <PriceSkeleton className="w-20" /> : formattedTotal}
80
88
  tokenSymbol={tokenSymbol}
81
89
  classes={classes}
82
90
  subtotal={formattedSubtotal}
@@ -86,6 +94,7 @@ export function CheckoutCartPanel({
86
94
  discount={discount}
87
95
  summaryLines={summaryLines}
88
96
  usdEquivalent={usdEquivalent}
97
+ pricesLoading={pricesLoading}
89
98
  />
90
99
 
91
100
  {footer === undefined ? (
@@ -31,6 +31,8 @@ interface CheckoutFormPanelProps {
31
31
  /** Token info for display */
32
32
  tokenSymbol?: string;
33
33
  tokenDecimals?: number;
34
+ /** True while token decimals/symbol are still loading — prevents rendering mis-decimalized prices. */
35
+ pricesLoading?: boolean;
34
36
  /** CSS class overrides */
35
37
  classes?: AnySpendCheckoutClasses;
36
38
  /** Current form data (lifted state) */
@@ -62,6 +64,7 @@ export function CheckoutFormPanel({
62
64
  validateDiscount,
63
65
  tokenSymbol,
64
66
  tokenDecimals,
67
+ pricesLoading = false,
65
68
  classes,
66
69
  formData,
67
70
  onFormDataChange,
@@ -144,6 +147,7 @@ export function CheckoutFormPanel({
144
147
  onSelect={onShippingChange}
145
148
  tokenSymbol={tokenSymbol}
146
149
  tokenDecimals={tokenDecimals}
150
+ pricesLoading={pricesLoading}
147
151
  />
148
152
  )}
149
153
  {enableDiscountCode && validateDiscount && (
@@ -153,6 +157,7 @@ export function CheckoutFormPanel({
153
157
  onRemove={onDiscountRemoved}
154
158
  tokenSymbol={tokenSymbol}
155
159
  tokenDecimals={tokenDecimals}
160
+ pricesLoading={pricesLoading}
156
161
  />
157
162
  )}
158
163
  </>
@@ -5,6 +5,7 @@ import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number
5
5
  import { X, Loader2, Check } from "lucide-react";
6
6
  import { useState, useCallback } from "react";
7
7
  import type { DiscountResult } from "../../../types/forms";
8
+ import { PriceSkeleton } from "./PriceSkeleton";
8
9
 
9
10
  interface DiscountCodeInputProps {
10
11
  onApply: (code: string) => Promise<DiscountResult>;
@@ -13,6 +14,8 @@ interface DiscountCodeInputProps {
13
14
  loading?: boolean;
14
15
  tokenSymbol?: string;
15
16
  tokenDecimals?: number;
17
+ /** True while token decimals/symbol are still loading — renders applied amount as skeleton. */
18
+ pricesLoading?: boolean;
16
19
  className?: string;
17
20
  }
18
21
 
@@ -29,6 +32,7 @@ export function DiscountCodeInput({
29
32
  loading = false,
30
33
  tokenSymbol = "",
31
34
  tokenDecimals = 6,
35
+ pricesLoading = false,
32
36
  className,
33
37
  }: DiscountCodeInputProps) {
34
38
  const [code, setCode] = useState("");
@@ -67,11 +71,17 @@ export function DiscountCodeInput({
67
71
  <div className="flex items-center gap-2">
68
72
  <Check className="h-4 w-4 text-green-600 dark:text-green-400" />
69
73
  <span className="anyspend-discount-value text-sm font-medium text-green-700 dark:text-green-300">
70
- {appliedDiscount.discount_type === "percentage"
71
- ? `${appliedDiscount.discount_value}% off`
72
- : appliedDiscount.discount_amount
73
- ? `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
74
- : "Discount applied"}
74
+ {appliedDiscount.discount_type === "percentage" ? (
75
+ `${appliedDiscount.discount_value}% off`
76
+ ) : appliedDiscount.discount_amount ? (
77
+ pricesLoading ? (
78
+ <PriceSkeleton />
79
+ ) : (
80
+ `-${formatAmount(appliedDiscount.discount_amount, tokenDecimals, tokenSymbol)}`
81
+ )
82
+ ) : (
83
+ "Discount applied"
84
+ )}
75
85
  </span>
76
86
  </div>
77
87
  <button
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
+
5
+ interface PriceSkeletonProps {
6
+ className?: string;
7
+ }
8
+
9
+ export function PriceSkeleton({ className }: PriceSkeletonProps) {
10
+ return (
11
+ <span
12
+ className={cn(
13
+ "animate-pulse-fade bg-b3-react-background inline-block h-4 w-16 rounded-md align-middle",
14
+ className,
15
+ )}
16
+ aria-hidden="true"
17
+ />
18
+ );
19
+ }
@@ -3,6 +3,7 @@
3
3
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
4
4
  import { formatTokenAmount, safeBigInt } from "@b3dotfun/sdk/shared/utils/number";
5
5
  import type { ShippingOption } from "../../../types/forms";
6
+ import { PriceSkeleton } from "./PriceSkeleton";
6
7
 
7
8
  interface ShippingSelectorProps {
8
9
  options: ShippingOption[];
@@ -10,6 +11,8 @@ interface ShippingSelectorProps {
10
11
  onSelect: (option: ShippingOption) => void;
11
12
  tokenSymbol?: string;
12
13
  tokenDecimals?: number;
14
+ /** True while token decimals/symbol are still loading — renders price as skeleton. */
15
+ pricesLoading?: boolean;
13
16
  className?: string;
14
17
  }
15
18
 
@@ -25,6 +28,7 @@ export function ShippingSelector({
25
28
  onSelect,
26
29
  tokenSymbol = "",
27
30
  tokenDecimals = 6,
31
+ pricesLoading = false,
28
32
  className,
29
33
  }: ShippingSelectorProps) {
30
34
  if (options.length === 0) return null;
@@ -63,7 +67,7 @@ export function ShippingSelector({
63
67
  </div>
64
68
  </div>
65
69
  <div className="anyspend-shipping-option-price text-sm font-medium text-gray-900 dark:text-gray-100">
66
- {formatAmount(option.amount, tokenDecimals, tokenSymbol)}
70
+ {pricesLoading ? <PriceSkeleton /> : formatAmount(option.amount, tokenDecimals, tokenSymbol)}
67
71
  </div>
68
72
  </label>
69
73
  ))}
@@ -1,7 +1,8 @@
1
- import { Button, Input } from "@b3dotfun/sdk/global-account/react";
1
+ import { Button } from "@b3dotfun/sdk/global-account/react";
2
2
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
3
3
  import { useState } from "react";
4
4
  import { useBetterAuth } from "../../hooks/useBetterAuth";
5
+ import { PasswordInput } from "./components/PasswordInput";
5
6
 
6
7
  const debug = debugB3React("BetterAuthResetPassword");
7
8
 
@@ -103,13 +104,12 @@ export function BetterAuthResetPassword({ token, onSuccess, onError, className }
103
104
  <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-700 dark:text-gray-300">
104
105
  New password
105
106
  </label>
106
- <Input
107
- type="password"
107
+ <PasswordInput
108
108
  placeholder="At least 8 characters"
109
109
  value={password}
110
110
  onChange={e => setPassword(e.target.value)}
111
111
  disabled={isLoading}
112
- className="h-11 px-4 text-[15px]"
112
+ className="h-11 px-4 pr-11 text-[15px]"
113
113
  />
114
114
  </div>
115
115
 
@@ -117,8 +117,7 @@ export function BetterAuthResetPassword({ token, onSuccess, onError, className }
117
117
  <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-700 dark:text-gray-300">
118
118
  Confirm password
119
119
  </label>
120
- <Input
121
- type="password"
120
+ <PasswordInput
122
121
  placeholder="Repeat your password"
123
122
  value={confirmPassword}
124
123
  onChange={e => setConfirmPassword(e.target.value)}
@@ -126,7 +125,7 @@ export function BetterAuthResetPassword({ token, onSuccess, onError, className }
126
125
  onKeyDown={e => {
127
126
  if (e.key === "Enter") handleSubmit();
128
127
  }}
129
- className="h-11 px-4 text-[15px]"
128
+ className="h-11 px-4 pr-11 text-[15px]"
130
129
  />
131
130
  </div>
132
131
 
@@ -1,4 +1,4 @@
1
- import { Button, Input, useAuthStore } from "@b3dotfun/sdk/global-account/react";
1
+ import { Button, Input, Loading, useAuthStore } from "@b3dotfun/sdk/global-account/react";
2
2
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
3
3
  import { useState } from "react";
4
4
  import {
@@ -6,6 +6,7 @@ import {
6
6
  type BetterAuthSocialProvider,
7
7
  useBetterAuth,
8
8
  } from "../../hooks/useBetterAuth";
9
+ import { PasswordInput } from "./components/PasswordInput";
9
10
  import { strategyIcons, strategyLabels } from "./utils/signInUtils";
10
11
 
11
12
  const debug = debugB3React("BetterAuthSignIn");
@@ -66,6 +67,8 @@ export function BetterAuthSignIn({
66
67
  }: BetterAuthSignInProps) {
67
68
  const { signInWithEmail, signUpWithEmail, signInWithSocial, requestPasswordReset } = useBetterAuth();
68
69
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
70
+ const isAuthenticating = useAuthStore(state => state.isAuthenticating);
71
+ const hasStartedConnecting = useAuthStore(state => state.hasStartedConnecting);
69
72
 
70
73
  const [mode, setMode] = useState<"sign-in" | "sign-up" | "forgot-password">("sign-in");
71
74
  const [email, setEmail] = useState("");
@@ -83,7 +86,9 @@ export function BetterAuthSignIn({
83
86
 
84
87
  const resolvedSubtitle =
85
88
  mode === "forgot-password"
86
- ? "Enter your email and we'll send you a reset link"
89
+ ? resetEmailSent
90
+ ? "We've sent a password reset link to your email"
91
+ : "Enter your email and we'll send you a reset link"
87
92
  : subtitle || "Enter your credentials to access your account";
88
93
 
89
94
  const handleForgotPassword = async () => {
@@ -111,6 +116,16 @@ export function BetterAuthSignIn({
111
116
  .map(id => DEFAULT_SOCIAL_PROVIDERS.find(p => p.id === id))
112
117
  .filter((p): p is SocialProviderConfig => !!p);
113
118
 
119
+ // Show loading during session restore (before any user interaction) to prevent
120
+ // the login form from flashing briefly after OAuth redirect.
121
+ if (isAuthenticating && !hasStartedConnecting) {
122
+ return (
123
+ <div className={`flex w-full max-w-[400px] items-center justify-center px-6 py-20 ${className || ""}`}>
124
+ <Loading variant="primary" size="lg" />
125
+ </div>
126
+ );
127
+ }
128
+
114
129
  if (isAuthenticated) {
115
130
  return null;
116
131
  }
@@ -334,16 +349,15 @@ export function BetterAuthSignIn({
334
349
  </button>
335
350
  )}
336
351
  </div>
337
- <Input
338
- type="password"
339
- placeholder="Password"
352
+ <PasswordInput
353
+ key={mode}
340
354
  value={password}
341
355
  onChange={e => setPassword(e.target.value)}
342
356
  disabled={isLoading}
343
357
  onKeyDown={e => {
344
358
  if (e.key === "Enter") handleEmailSubmit();
345
359
  }}
346
- className="h-11 px-4 text-[15px]"
360
+ className="h-11 px-4 pr-11 text-[15px]"
347
361
  />
348
362
  </div>
349
363
 
@@ -0,0 +1,62 @@
1
+ import { Input } from "@b3dotfun/sdk/global-account/react";
2
+ import { useState } from "react";
3
+
4
+ interface PasswordInputProps {
5
+ value: string;
6
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
7
+ disabled?: boolean;
8
+ placeholder?: string;
9
+ onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
10
+ className?: string;
11
+ }
12
+
13
+ export function PasswordInput({
14
+ value,
15
+ onChange,
16
+ disabled,
17
+ placeholder = "Password",
18
+ onKeyDown,
19
+ className,
20
+ }: PasswordInputProps) {
21
+ const [showPassword, setShowPassword] = useState(false);
22
+
23
+ return (
24
+ <div className="relative">
25
+ <Input
26
+ type={showPassword ? "text" : "password"}
27
+ placeholder={placeholder}
28
+ value={value}
29
+ onChange={onChange}
30
+ disabled={disabled}
31
+ onKeyDown={onKeyDown}
32
+ className={className}
33
+ />
34
+ <button
35
+ type="button"
36
+ onClick={() => setShowPassword(!showPassword)}
37
+ aria-label={showPassword ? "Hide password" : "Show password"}
38
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
39
+ tabIndex={-1}
40
+ >
41
+ {showPassword ? (
42
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
43
+ <path
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
47
+ />
48
+ </svg>
49
+ ) : (
50
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
55
+ />
56
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
57
+ </svg>
58
+ )}
59
+ </button>
60
+ </div>
61
+ );
62
+ }
@@ -7,6 +7,7 @@ import {
7
7
  useBetterAuth,
8
8
  } from "../../../hooks/useBetterAuth";
9
9
  import { AuthButton } from "../components/AuthButton";
10
+ import { PasswordInput } from "../components/PasswordInput";
10
11
 
11
12
  const debug = debugB3React("LoginStepBetterAuth");
12
13
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -160,7 +161,11 @@ export function LoginStepBetterAuth({ onSuccess, onError }: LoginStepBetterAuthP
160
161
  {mode === "forgot-password" ? (
161
162
  <div className="mb-6 w-full space-y-3 px-3">
162
163
  <p className="text-center text-sm font-medium text-gray-900 dark:text-gray-100">Reset password</p>
163
- <p className="text-center text-xs text-gray-500">Enter your email and we'll send you a reset link</p>
164
+ <p className="text-center text-xs text-gray-500">
165
+ {resetEmailSent
166
+ ? "We've sent a password reset link to your email"
167
+ : "Enter your email and we'll send you a reset link"}
168
+ </p>
164
169
 
165
170
  {resetEmailSent ? (
166
171
  <div className="space-y-3 py-4 text-center">
@@ -223,15 +228,15 @@ export function LoginStepBetterAuth({ onSuccess, onError }: LoginStepBetterAuthP
223
228
  disabled={isLoading}
224
229
  />
225
230
 
226
- <Input
227
- type="password"
228
- placeholder="Password"
231
+ <PasswordInput
232
+ key={mode}
229
233
  value={password}
230
234
  onChange={event => setPassword(event.target.value)}
231
235
  disabled={isLoading}
232
236
  onKeyDown={event => {
233
237
  if (event.key === "Enter") handleEmailSubmit();
234
238
  }}
239
+ className="pr-11"
235
240
  />
236
241
 
237
242
  {error && <p className="text-sm text-red-500">{error}</p>}