@lmnto/h-mall-shared 1.0.4 → 1.0.6

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": "@lmnto/h-mall-shared",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "scripts": {
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ type PaymentCountdownProps = {
6
+ /** ISO date string from API (e.g. expiration_estimate_date) */
7
+ expiresAtIso: string | null | undefined;
8
+ label: string;
9
+ onExpire?: () => void;
10
+ className?: string;
11
+ /** `inline` = compact pill for header row; `block` = full-width bar (default) */
12
+ variant?: "block" | "inline";
13
+ };
14
+
15
+ export default function PaymentCountdown({
16
+ expiresAtIso,
17
+ label,
18
+ onExpire,
19
+ className = "",
20
+ variant = "block",
21
+ }: PaymentCountdownProps) {
22
+ const [secondsLeft, setSecondsLeft] = useState<number | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!expiresAtIso) {
26
+ setSecondsLeft(null);
27
+ return;
28
+ }
29
+ const end = new Date(expiresAtIso).getTime();
30
+ if (!Number.isFinite(end)) {
31
+ setSecondsLeft(null);
32
+ return;
33
+ }
34
+
35
+ let expiredNotified = false;
36
+ const tick = () => {
37
+ const s = Math.max(0, Math.floor((end - Date.now()) / 1000));
38
+ setSecondsLeft(s);
39
+ if (s <= 0 && !expiredNotified) {
40
+ expiredNotified = true;
41
+ onExpire?.();
42
+ }
43
+ };
44
+
45
+ tick();
46
+ const id = setInterval(tick, 1000);
47
+ return () => clearInterval(id);
48
+ }, [expiresAtIso, onExpire]);
49
+
50
+ if (secondsLeft == null) return null;
51
+
52
+ const mm = Math.floor(secondsLeft / 60);
53
+ const ss = secondsLeft % 60;
54
+ const display = `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
55
+
56
+ if (variant === "inline") {
57
+ return (
58
+ <div
59
+ className={`inline-flex shrink-0 items-center gap-3 rounded-full border border-amber-200/80 bg-amber-50 px-4 py-2 text-xs font-medium text-amber-950 ${className}`}
60
+ >
61
+ <span className="text-slate-600">{label}</span>
62
+ <span className="min-w-[3.25rem] font-semibold tabular-nums text-slate-900">
63
+ {display}
64
+ </span>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div
71
+ className={`rounded-xl border border-slate-200 bg-slate-50/90 px-4 py-3 ${className}`}
72
+ >
73
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
74
+ <span className="text-sm font-medium leading-5 text-slate-600">
75
+ {label}
76
+ </span>
77
+ <span className="text-sm font-semibold tabular-nums text-slate-900">
78
+ {display}
79
+ </span>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
@@ -19,6 +19,8 @@ type RequestProcessingDialogProps = {
19
19
  paymentProcessFailed: boolean;
20
20
  onRetry?: () => void;
21
21
  setOpen: (value: boolean) => void;
22
+ /** When set (e.g. NOWPayments deposit instructions), replaces the loading state in the same modal. */
23
+ paymentDetails?: React.ReactNode;
22
24
  };
23
25
 
24
26
  export function RequestProcessingDialog({
@@ -27,13 +29,20 @@ export function RequestProcessingDialog({
27
29
  orderId,
28
30
  paymentProcessFailed,
29
31
  onRetry,
32
+ paymentDetails,
30
33
  }: RequestProcessingDialogProps) {
31
34
  //
32
35
  const scopeT = _useScopedI18n("payment.requestProcessingDialog");
36
+ const hasPaymentDetails = Boolean(paymentDetails);
33
37
 
34
38
  return (
35
39
  <>
36
- <Dialog open={open} size="xs" handler={() => {}} className="p-2 md:p-5">
40
+ <Dialog
41
+ open={open}
42
+ size={hasPaymentDetails ? "md" : "xs"}
43
+ handler={() => {}}
44
+ className={`p-2 md:p-4 ${hasPaymentDetails ? "max-w-md w-[calc(100vw-1.5rem)]" : ""}`}
45
+ >
37
46
  <DialogHeader className="p-0">
38
47
  <div className="text-right size-full">
39
48
  <IconButton
@@ -61,31 +70,37 @@ export function RequestProcessingDialog({
61
70
  </IconButton>
62
71
  </div>
63
72
  </DialogHeader>
64
- <DialogBody className="flex flex-col items-center gap-8 text-center">
65
- <>
66
- <LoadingIconComponent />
67
- <div>
68
- {/* <AppLoadingComponent /> */}
69
- <Typography variant="h3" className="text-xl text-black-500">
70
- {scopeT("title")}...
71
- </Typography>
72
- </div>
73
+ <DialogBody
74
+ className={
75
+ hasPaymentDetails
76
+ ? "flex max-h-[80vh] flex-col gap-3 overflow-y-auto px-1 text-left sm:px-2"
77
+ : "flex flex-col items-center gap-8 text-center"
78
+ }
79
+ >
80
+ {hasPaymentDetails ? (
81
+ <>
82
+ <div className="w-full space-y-1 text-center">
83
+ <Typography
84
+ variant="h3"
85
+ className="text-xl font-semibold text-slate-900"
86
+ >
87
+ {scopeT("completePaymentTitle")}
88
+ </Typography>
89
+ <p className="text-sm text-slate-500">
90
+ {scopeT("orderId")}{" "}
91
+ <span className="font-mono font-medium text-slate-700">
92
+ {orderId}
93
+ </span>
94
+ </p>
95
+ </div>
73
96
 
74
- <div className="bg-gray-50 py-4 px-9 border border-slate-100 rounded-lg">
75
- <Typography
76
- variant="h6"
77
- className="text-xs leading-5 font-medium text-black-500"
78
- >
79
- {scopeT("orderId")} : {orderId}
80
- </Typography>
81
- </div>
97
+ {paymentDetails}
82
98
 
83
- <div>
84
99
  {paymentProcessFailed ? (
85
- <>
100
+ <div className="text-center">
86
101
  <Typography
87
102
  variant="lead"
88
- className="text-sm font-medium text-slate-500 mb-3"
103
+ className="mb-3 text-sm font-medium text-slate-500"
89
104
  >
90
105
  {scopeT("errorMsg")}
91
106
  </Typography>
@@ -96,26 +111,65 @@ export function RequestProcessingDialog({
96
111
  >
97
112
  {scopeT("retryBtn")}
98
113
  </ButtonCustom>
99
- </>
100
- ) : (
101
- <>
102
- <Typography
103
- variant="lead"
104
- className="text-sm font-medium text-slate-500 mb-3"
105
- >
106
- {scopeT("subTitle")}
107
- </Typography>
114
+ </div>
115
+ ) : null}
116
+ </>
117
+ ) : (
118
+ <>
119
+ <LoadingIconComponent />
120
+ <div>
121
+ {/* <AppLoadingComponent /> */}
122
+ <Typography variant="h3" className="text-xl text-black-500">
123
+ {scopeT("title")}...
124
+ </Typography>
125
+ </div>
108
126
 
109
- <Typography
110
- variant="lead"
111
- className="text-sm font-medium text-slate-500"
112
- >
113
- {scopeT("message")}
114
- </Typography>
115
- </>
116
- )}
117
- </div>
118
- </>
127
+ <div className="rounded-lg border border-slate-100 bg-gray-50 px-9 py-4">
128
+ <Typography
129
+ variant="h6"
130
+ className="text-xs font-medium leading-5 text-black-500"
131
+ >
132
+ {scopeT("orderId")} : {orderId}
133
+ </Typography>
134
+ </div>
135
+
136
+ <div>
137
+ {paymentProcessFailed ? (
138
+ <>
139
+ <Typography
140
+ variant="lead"
141
+ className="mb-3 text-sm font-medium text-slate-500"
142
+ >
143
+ {scopeT("errorMsg")}
144
+ </Typography>
145
+
146
+ <ButtonCustom
147
+ icon={<IoRefresh className="size-4" />}
148
+ onClick={onRetry}
149
+ >
150
+ {scopeT("retryBtn")}
151
+ </ButtonCustom>
152
+ </>
153
+ ) : (
154
+ <>
155
+ <Typography
156
+ variant="lead"
157
+ className="mb-3 text-sm font-medium text-slate-500"
158
+ >
159
+ {scopeT("subTitle")}
160
+ </Typography>
161
+
162
+ <Typography
163
+ variant="lead"
164
+ className="text-sm font-medium text-slate-500"
165
+ >
166
+ {scopeT("message")}
167
+ </Typography>
168
+ </>
169
+ )}
170
+ </div>
171
+ </>
172
+ )}
119
173
  </DialogBody>
120
174
  </Dialog>
121
175
  </>
@@ -36,7 +36,11 @@ export const METHOD_TO_PAY = {
36
36
  card: "card",
37
37
  wallet: "wallet",
38
38
  aleta: "aleta",
39
- };
39
+ nowpayments: "nowpayments",
40
+ gift: "gift",
41
+ } as const;
42
+
43
+ export type MethodToPayCode = (typeof METHOD_TO_PAY)[keyof typeof METHOD_TO_PAY];
40
44
 
41
45
  export const ORDER_STATUS = {
42
46
  PENDING: "pending",
@@ -7,6 +7,7 @@ export const FeatureCodes = {
7
7
  DOWNLOAD_SHIPPING_ORDER_INVOICE: "download-shipping-order-invoice",
8
8
  SMART_PAY_PLAN: "smart-plan-pay",
9
9
  ALETA_PAYMENT: "aleta",
10
+ NOWPAYMENTS_PAYMENT: "nowpayments-payment",
10
11
  AI_CATEGORY: "ai-category",
11
12
  GEN_3_EXTENSION_HOME_DIALOG: "gen2-to-gen3-migration",
12
13
  ORDER_V2_API: "order-v2-api",
@@ -112,7 +112,10 @@ export function WebSocketWrapper({ children }: WebSocketWrapperProps) {
112
112
  }, 50);
113
113
 
114
114
  const sendEventMessage = useCallback(
115
- (event: string, msg: string | number) => {
115
+ (
116
+ event: string,
117
+ msg: string | number | { orderId: string | number; payCurrency?: string },
118
+ ) => {
116
119
  if (!socket.current) return;
117
120
 
118
121
  setPaymentProcessFailed(false);
@@ -150,6 +153,9 @@ export type WebSocketWrapperContextDto = {
150
153
  paymentProcessResponse: any;
151
154
  isPaymentCompleted: boolean;
152
155
  initConnection: (...args: any) => void;
153
- sendEventMessage: (event: string, msg: string | number) => void;
156
+ sendEventMessage: (
157
+ event: string,
158
+ msg: string | number | { orderId: string | number; payCurrency?: string },
159
+ ) => void;
154
160
  disconnectSocket: () => void;
155
161
  };
@@ -103,20 +103,26 @@ export function useCart() {
103
103
 
104
104
  const updatePaymentMethod = useMutation({
105
105
  mutationKey: [QueryKeys.cart.UPDATE_PAYMENT_METHOD],
106
- mutationFn: ({
107
- paymentMethodId,
108
- paymentOptionId,
109
- fromAssetSelectedPercentage,
110
- }: {
106
+ mutationFn: (params: {
111
107
  paymentMethodId: number;
112
108
  paymentOptionId: number;
113
109
  fromAssetSelectedPercentage: number;
114
- }) =>
115
- CartsService.cartsControllerUpdatePaymentMethod({
116
- paymentMethodId: paymentMethodId,
117
- paymentOptionId: paymentOptionId,
118
- fromAssetSelectedPercentage: fromAssetSelectedPercentage,
119
- }),
110
+ optionAssetSymbol?: string;
111
+ optionAssetName?: string;
112
+ payCurrencyCode?: string;
113
+ nowpaymentsCurrencyId?: number;
114
+ }) => {
115
+ if (
116
+ process.env.NODE_ENV === "development" ||
117
+ process.env.NEXT_PUBLIC_DEBUG_PAYMENT_FLOW === "1"
118
+ ) {
119
+ console.info(
120
+ "[H-Mall Payment][useCart.updatePaymentMethod] request",
121
+ params,
122
+ );
123
+ }
124
+ return CartsService.cartsControllerUpdatePaymentMethod(params);
125
+ },
120
126
  onMutate: () => setLoading(true),
121
127
  onSuccess: () => revalidateCart(),
122
128
  onError: () => setLoading(false),
@@ -181,6 +181,8 @@ export default {
181
181
  loading: "Loading...",
182
182
  grandTotal: "Grand Total",
183
183
  continueShoppingBtn: "Continue Shopping",
184
+ totalsUpdateHint:
185
+ "Estimated — totals update when you confirm this payment method.",
184
186
  },
185
187
  shippingOrderSummaryComponent: {
186
188
  title: "Shipping Summary",
@@ -337,6 +339,7 @@ export default {
337
339
  },
338
340
  requestProcessingDialog: {
339
341
  title: "Processing Your Request",
342
+ completePaymentTitle: "Complete your payment",
340
343
  subTitle: "Hang tight! while we process your order.",
341
344
  message:
342
345
  "Just a moment! Please do not refresh while we complete your order.",
@@ -348,6 +351,23 @@ export default {
348
351
  insufficientBalanceMsg: "Insufficient wallet balance. Please recharge.",
349
352
  avlBalance: "Avl Balance",
350
353
  },
354
+ nowpayments: {
355
+ cryptoLabel: "Pay with cryptocurrency",
356
+ loadingCurrencies: "Loading currencies…",
357
+ depositAddress: "Deposit address",
358
+ copyAddress: "Copy",
359
+ copied: "Copied",
360
+ expectedAmount: "Amount to send",
361
+ qrLabel: "Scan QR code",
362
+ timeRemaining: "Time remaining",
363
+ openWidget: "Open payment widget",
364
+ waitingConfirmation:
365
+ "We will confirm your payment automatically. You can leave this page open.",
366
+ expiredMessage:
367
+ "This payment request has expired or is no longer valid. You can generate a new address without losing your order.",
368
+ regenerateBtn: "Generate new payment",
369
+ regenerateLoading: "Generating…",
370
+ },
351
371
  disclaimerAlertDialog: {
352
372
  header: "Disclaimer Alert",
353
373
  acceptCheckboxPrivacy: "I have read and accept membership",
@@ -171,6 +171,8 @@ export default {
171
171
  loading: "Caricamento...",
172
172
  grandTotal: "Totale generale",
173
173
  continueShoppingBtn: "Continua lo shopping",
174
+ totalsUpdateHint:
175
+ "Stima — i totali si aggiornano quando confermi questo metodo di pagamento.",
174
176
  },
175
177
  shippingOrderSummaryComponent: {
176
178
  title: "Riepilogo della Spedizione",
@@ -326,6 +328,7 @@ export default {
326
328
  },
327
329
  requestProcessingDialog: {
328
330
  title: "Elaborazione della tua richiesta",
331
+ completePaymentTitle: "Completa il pagamento",
329
332
  subTitle: "Attendi! mentre elaboriamo il tuo ordine.",
330
333
  message:
331
334
  "Un momento! Si prega di non aggiornare la pagina mentre completiamo il tuo ordine.",
@@ -338,6 +341,23 @@ export default {
338
341
  "Saldo del portafoglio insufficiente. Si prega di ricaricare.",
339
342
  avlBalance: "Saldo disponibile",
340
343
  },
344
+ nowpayments: {
345
+ cryptoLabel: "Paga in criptovaluta",
346
+ loadingCurrencies: "Caricamento valute…",
347
+ depositAddress: "Indirizzo di deposito",
348
+ copyAddress: "Copia",
349
+ copied: "Copiato",
350
+ expectedAmount: "Importo da inviare",
351
+ qrLabel: "Scansiona il codice QR",
352
+ timeRemaining: "Tempo rimanente",
353
+ openWidget: "Apri il widget di pagamento",
354
+ waitingConfirmation:
355
+ "Confermeremo automaticamente il pagamento. Puoi lasciare aperta questa pagina.",
356
+ expiredMessage:
357
+ "Questa richiesta di pagamento è scaduta o non è più valida. Puoi generare un nuovo indirizzo senza perdere l'ordine.",
358
+ regenerateBtn: "Genera nuovo pagamento",
359
+ regenerateLoading: "Generazione…",
360
+ },
341
361
  disclaimerAlertDialog: {
342
362
  header: "Avviso di esclusione di responsabilità",
343
363
  acceptCheckboxPrivacy: "Ho letto e accetto l'adesione",
@@ -6,4 +6,12 @@ export type UpdateCartPaymentMethodDto = {
6
6
  paymentMethodId: number;
7
7
  paymentOptionId: number;
8
8
  fromAssetSelectedPercentage: number;
9
+ /** Same cart_asset keys as other providers: pay-instrument symbol (e.g. crypto ticker). */
10
+ optionAssetSymbol?: string;
11
+ /** Same cart_asset keys: display label (e.g. name + network). */
12
+ optionAssetName?: string;
13
+ /** Persisted on `cart_assets.asset_option` JSON: pay ticker (e.g. NOWPayments). */
14
+ payCurrencyCode?: string;
15
+ /** NOWPayments full-currencies id, stored on `cart_assets.asset_option` JSON. */
16
+ nowpaymentsCurrencyId?: number;
9
17
  };
@@ -64,4 +64,23 @@ export class OrderCallbackService {
64
64
  mediaType: "application/json",
65
65
  });
66
66
  }
67
+
68
+ /**
69
+ * NOWPayments IPN (server-to-server). Requires valid x-nowpayments-sig.
70
+ * @param xNowpaymentsSig HMAC-SHA512 hex of sorted JSON body
71
+ */
72
+ public static orderCallbackControllerHandleNowpaymentsIpn(
73
+ requestBody: Record<string, unknown>,
74
+ xNowpaymentsSig: string,
75
+ ): CancelablePromise<any> {
76
+ return __request(OpenAPI, {
77
+ method: "POST",
78
+ url: "/v1/order-webhooks/nowpayments",
79
+ body: requestBody,
80
+ mediaType: "application/json",
81
+ headers: {
82
+ "x-nowpayments-sig": xNowpaymentsSig,
83
+ },
84
+ });
85
+ }
67
86
  }
@@ -39,6 +39,25 @@ export class OrdersService {
39
39
  });
40
40
  }
41
41
 
42
+ /**
43
+ * Start payment for an order (HTTP equivalent of socket `order-payment-process`).
44
+ * Returns payment processor payload immediately (e.g. NOWPayments deposit details).
45
+ */
46
+ public static ordersControllerProcessOrderPayment(
47
+ orderId: string,
48
+ requestBody?: { payCurrency?: string },
49
+ ): CancelablePromise<any> {
50
+ return __request(OpenAPI, {
51
+ method: "POST",
52
+ url: "/v1/orders/{orderId}/payments/process",
53
+ path: {
54
+ orderId: orderId,
55
+ },
56
+ body: requestBody,
57
+ mediaType: "application/json",
58
+ });
59
+ }
60
+
42
61
  /**
43
62
  * @param page
44
63
  * @param limit
@@ -69,6 +88,46 @@ export class OrdersService {
69
88
  });
70
89
  }
71
90
 
91
+ /**
92
+ * Owner-only proxy for NOWPayments GET /v1/payment/{id} (rate-limited server-side).
93
+ * @param paymentId NOWPayments payment_id
94
+ * @returns any
95
+ * @throws ApiError
96
+ */
97
+ public static ordersControllerPollNowpaymentsPayment(
98
+ paymentId: string,
99
+ ): CancelablePromise<any> {
100
+ return __request(OpenAPI, {
101
+ method: "GET",
102
+ url: "/v1/orders/payments/nowpayments/{paymentId}",
103
+ path: {
104
+ paymentId: paymentId,
105
+ },
106
+ isServerAction: true,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Replace an expired or failed NOWPayments invoice (order must still be unpaid).
112
+ * @param orderId Public order id
113
+ * @param requestBody Optional payCurrency
114
+ */
115
+ public static ordersControllerRecreateNowpaymentsPayment(
116
+ orderId: string,
117
+ requestBody?: { payCurrency?: string },
118
+ ): CancelablePromise<any> {
119
+ return __request(OpenAPI, {
120
+ method: "POST",
121
+ url: "/v1/orders/{orderId}/payments/nowpayments/recreate",
122
+ path: {
123
+ orderId: orderId,
124
+ },
125
+ body: requestBody,
126
+ mediaType: "application/json",
127
+ isServerAction: true,
128
+ });
129
+ }
130
+
72
131
  /**
73
132
  * @param requestBody
74
133
  * @returns any
@@ -6,6 +6,30 @@ import type { CancelablePromise } from "../core/CancelablePromise";
6
6
  import { OpenAPI } from "../core/OpenAPI";
7
7
  import { request as __request } from "../core/request";
8
8
  export class PaymentsService {
9
+ /**
10
+ * Cached NOWPayments currency list (~1h) for pay-currency picker.
11
+ * @returns any
12
+ * @throws ApiError
13
+ */
14
+ public static paymentsControllerGetNowpaymentsCurrencies(): CancelablePromise<any> {
15
+ return __request(OpenAPI, {
16
+ method: "GET",
17
+ url: "/v1/payments/nowpayments/currencies",
18
+ });
19
+ }
20
+
21
+ /**
22
+ * NOWPayments fiat currencies (JWT-backed, cached ~1h) for pay-currency picker.
23
+ * @returns any
24
+ * @throws ApiError
25
+ */
26
+ public static paymentsControllerGetNowpaymentsFiatCurrencies(): CancelablePromise<any> {
27
+ return __request(OpenAPI, {
28
+ method: "GET",
29
+ url: "/v1/payments/nowpayments/fiat-currencies",
30
+ });
31
+ }
32
+
9
33
  /**
10
34
  * @param shipping
11
35
  * @param upgrade