@coin-voyage/paykit 2.4.4 → 2.4.5-beta.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.
@@ -1,4 +1,4 @@
1
- import type { ChainId, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderCreationErrorEvent, PayOrderMetadata, PayOrderRefundedEvent } from "@coin-voyage/shared/types";
1
+ import type { ChainId, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderCreationErrorEvent, PayOrderExecutingEvent, PayOrderMetadata, PayOrderRefundedEvent, PayOrderStartedEvent } from "@coin-voyage/shared/types";
2
2
  import type { CustomTheme, Mode, PayModalOptions, Theme } from "../../types";
3
3
  type DepositPayButtonParams = {
4
4
  /**
@@ -34,8 +34,17 @@ type PayButtonCommonProps = PayButtonPaymentProps & {
34
34
  intent?: string;
35
35
  /** Called when invalid properties are used in order to create a deposit payOrder */
36
36
  onPaymentCreationError?: (event: PayOrderCreationErrorEvent) => void;
37
- /** Called when user sends payment and transaction is seen on chain */
37
+ /** Called when payment details are available and the order is awaiting payment. */
38
+ onAwaitingPayment?: (event: PayOrderStartedEvent) => void;
39
+ /** Called when the payment is detected and awaiting confirmation. */
40
+ onConfirmingPayment?: (event: PayOrderConfirmingEvent) => void;
41
+ /**
42
+ * Called when the payment is detected and awaiting confirmation.
43
+ * @deprecated Use `onConfirmingPayment` instead.
44
+ */
38
45
  onPaymentStarted?: (event: PayOrderConfirmingEvent) => void;
46
+ /** Called when the payment is confirmed and the order is executing. */
47
+ onExecutingPayment?: (event: PayOrderExecutingEvent) => void;
39
48
  /** Called when destination transfer or call completes successfully */
40
49
  onPaymentCompleted?: (event: PayOrderCompletedEvent) => void;
41
50
  /** Called when destination call reverts and funds are refunded */
@@ -23,7 +23,9 @@ function PayButtonCustom(props) {
23
23
  usePayModalCallbacks(props.onOpen, props.onClose);
24
24
  const { order, show, hide } = usePayButtonController(props);
25
25
  usePaymentLifecycle(order, {
26
- onPaymentStarted: props.onPaymentStarted,
26
+ onAwaitingPayment: props.onAwaitingPayment,
27
+ onConfirmingPayment: props.onConfirmingPayment ?? props.onPaymentStarted,
28
+ onExecutingPayment: props.onExecutingPayment,
27
29
  onPaymentCompleted: props.onPaymentCompleted,
28
30
  onPaymentBounced: props.onPaymentBounced,
29
31
  }, {
@@ -0,0 +1,9 @@
1
+ import { PayOrderEvent } from "@coin-voyage/shared/types";
2
+ interface UseOrderStatusWSProps {
3
+ orderId?: string;
4
+ enabled?: boolean;
5
+ onEvent?: (eventData: PayOrderEvent) => void;
6
+ onError?: (error: unknown) => void;
7
+ }
8
+ export declare function useOrderStatusWS({ orderId, enabled, onEvent, onError, }: UseOrderStatusWSProps): void;
9
+ export {};
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useBackendApi } from "../components/contexts/api";
3
+ export function useOrderStatusWS({ orderId, enabled = true, onEvent, onError, }) {
4
+ const api = useBackendApi();
5
+ const socketRef = useRef(null);
6
+ const onEventRef = useRef(null);
7
+ const onErrorRef = useRef(null);
8
+ useEffect(() => {
9
+ onEventRef.current = onEvent;
10
+ }, [onEvent]);
11
+ useEffect(() => {
12
+ onErrorRef.current = onError;
13
+ }, [onError]);
14
+ useEffect(() => {
15
+ if (!enabled)
16
+ return;
17
+ if (socketRef.current)
18
+ return;
19
+ const socket = api.subscribeOrderStatus();
20
+ socketRef.current = socket;
21
+ socket.onOpen(() => {
22
+ if (orderId) {
23
+ socket.subscribe(orderId);
24
+ }
25
+ else {
26
+ socket.subscribeOrg();
27
+ }
28
+ });
29
+ socket.onMessage((msg) => {
30
+ if (msg.type === "event") {
31
+ onEventRef.current?.(msg.data);
32
+ }
33
+ });
34
+ socket.onError((e) => {
35
+ onErrorRef.current?.(e);
36
+ });
37
+ socket.onClose(() => {
38
+ if (socketRef.current === socket) {
39
+ socketRef.current = null;
40
+ }
41
+ });
42
+ return () => {
43
+ if (socketRef.current === socket) {
44
+ socketRef.current = null;
45
+ }
46
+ socket.close();
47
+ };
48
+ }, [api, enabled, orderId]);
49
+ }
@@ -1,6 +1,8 @@
1
- import { PayOrder, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderRefundedEvent } from "@coin-voyage/shared/types";
1
+ import { PayOrder, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderExecutingEvent, PayOrderRefundedEvent, PayOrderStartedEvent } from "@coin-voyage/shared/types";
2
2
  type PaymentLifecycleHandlers = {
3
- onPaymentStarted: ((event: PayOrderConfirmingEvent) => void) | undefined;
3
+ onAwaitingPayment: ((event: PayOrderStartedEvent) => void) | undefined;
4
+ onConfirmingPayment: ((event: PayOrderConfirmingEvent) => void) | undefined;
5
+ onExecutingPayment: ((event: PayOrderExecutingEvent) => void) | undefined;
4
6
  onPaymentCompleted: ((event: PayOrderCompletedEvent) => void) | undefined;
5
7
  onPaymentBounced: ((event: PayOrderRefundedEvent) => void) | undefined;
6
8
  };
@@ -2,11 +2,13 @@ import { PayOrderMode, PayOrderStatus, } from "@coin-voyage/shared/types";
2
2
  import { useEffect, useRef } from "react";
3
3
  const COMPLETED_STATES = [PayOrderStatus.COMPLETED, PayOrderStatus.REFUNDED];
4
4
  const STARTED_STATES = [
5
- ...COMPLETED_STATES,
5
+ PayOrderStatus.AWAITING_PAYMENT,
6
6
  PayOrderStatus.PARTIAL_PAYMENT,
7
7
  PayOrderStatus.AWAITING_CONFIRMATION,
8
8
  PayOrderStatus.OPTIMISTIC_CONFIRMED,
9
9
  PayOrderStatus.EXECUTING_ORDER,
10
+ PayOrderStatus.COMPLETED,
11
+ PayOrderStatus.REFUNDED,
10
12
  ];
11
13
  /**
12
14
  * Handles payment lifecycle events of an order, such as started, completed, and bounced.
@@ -16,58 +18,92 @@ const STARTED_STATES = [
16
18
  * @returns
17
19
  */
18
20
  export function usePaymentLifecycle(order, handlers, options = {}) {
19
- const sentStart = useRef(false);
21
+ const sentAwaitingPayment = useRef(false);
22
+ const sentConfirmingPayment = useRef(false);
23
+ const sentExecutingPayment = useRef(false);
20
24
  const sentComplete = useRef(false);
25
+ const sentBounced = useRef(false);
21
26
  const currentOrderId = useRef(undefined);
22
- const { onPaymentStarted, onPaymentCompleted, onPaymentBounced } = handlers;
27
+ const { onAwaitingPayment, onConfirmingPayment, onExecutingPayment, onPaymentCompleted, onPaymentBounced } = handlers;
23
28
  const { optimisticConfirmation = true } = options;
24
29
  const orderId = order?.id;
25
30
  const orderStatus = order?.status;
26
31
  const orderMetadata = order?.metadata;
27
32
  const payment = order?.payment;
28
- const allowOptimisticCompletion = optimisticConfirmation && order?.mode === PayOrderMode.SALE;
33
+ const allowOptimisticFinalized = optimisticConfirmation && order?.mode === PayOrderMode.SALE;
29
34
  const isStarted = !!orderStatus && STARTED_STATES.includes(orderStatus);
30
- const isFinalized = !!orderStatus &&
31
- (COMPLETED_STATES.includes(orderStatus) ||
32
- (allowOptimisticCompletion &&
33
- (orderStatus === PayOrderStatus.OPTIMISTIC_CONFIRMED || orderStatus === PayOrderStatus.EXECUTING_ORDER)));
35
+ const isFinalized = (!!orderStatus && COMPLETED_STATES.includes(orderStatus)) ||
36
+ (allowOptimisticFinalized &&
37
+ (orderStatus === PayOrderStatus.OPTIMISTIC_CONFIRMED || orderStatus === PayOrderStatus.EXECUTING_ORDER));
34
38
  useEffect(() => {
35
39
  if (!orderId)
36
40
  return;
37
41
  if (currentOrderId.current === orderId)
38
42
  return;
39
43
  currentOrderId.current = orderId;
40
- sentStart.current = false;
44
+ sentAwaitingPayment.current = false;
45
+ sentConfirmingPayment.current = false;
46
+ sentExecutingPayment.current = false;
41
47
  sentComplete.current = false;
48
+ sentBounced.current = false;
42
49
  }, [orderId]);
43
50
  useEffect(() => {
44
- if (sentStart.current || !orderId || !payment || !orderStatus || !isStarted)
51
+ if (sentAwaitingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.AWAITING_PAYMENT)
45
52
  return;
46
- sentStart.current = true;
47
- onPaymentStarted?.({
53
+ sentAwaitingPayment.current = true;
54
+ onAwaitingPayment?.({
55
+ type: "payorder_started",
56
+ payorder_id: orderId,
57
+ status: orderStatus,
58
+ metadata: orderMetadata,
59
+ payment_data: payment,
60
+ });
61
+ }, [onAwaitingPayment, orderId, orderMetadata, orderStatus, payment]);
62
+ useEffect(() => {
63
+ if (sentConfirmingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.AWAITING_CONFIRMATION) {
64
+ return;
65
+ }
66
+ sentConfirmingPayment.current = true;
67
+ onConfirmingPayment?.({
48
68
  type: "payorder_confirming",
49
69
  payorder_id: orderId,
50
70
  status: orderStatus,
51
71
  metadata: orderMetadata,
52
72
  payment_data: payment,
73
+ source_tx_hash: payment.source_tx_hash ?? "",
53
74
  });
54
- }, [isStarted, onPaymentStarted, orderId, orderMetadata, orderStatus, payment]);
75
+ }, [onConfirmingPayment, orderId, orderMetadata, orderStatus, payment]);
55
76
  useEffect(() => {
56
- if (sentComplete.current || !orderId || !payment || !orderStatus || !isFinalized)
77
+ if (sentExecutingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.EXECUTING_ORDER)
57
78
  return;
58
- sentComplete.current = true;
59
- if (orderStatus === PayOrderStatus.REFUNDED) {
60
- onPaymentBounced?.({
61
- type: "payorder_refunded",
62
- payorder_id: orderId,
63
- status: orderStatus,
64
- metadata: orderMetadata,
65
- payment_data: payment,
66
- refund_address: payment.refund_address ?? "",
67
- refund_tx_hash: payment.refund_tx_hash ?? "",
68
- });
79
+ sentExecutingPayment.current = true;
80
+ onExecutingPayment?.({
81
+ type: "payorder_executing",
82
+ payorder_id: orderId,
83
+ status: orderStatus,
84
+ metadata: orderMetadata,
85
+ payment_data: payment,
86
+ source_tx_hash: payment.source_tx_hash ?? "",
87
+ });
88
+ }, [onExecutingPayment, orderId, orderMetadata, orderStatus, payment]);
89
+ useEffect(() => {
90
+ if (sentBounced.current || !orderId || !payment || orderStatus !== PayOrderStatus.REFUNDED)
69
91
  return;
70
- }
92
+ sentBounced.current = true;
93
+ onPaymentBounced?.({
94
+ type: "payorder_refunded",
95
+ payorder_id: orderId,
96
+ status: orderStatus,
97
+ metadata: orderMetadata,
98
+ payment_data: payment,
99
+ refund_address: payment.refund_address ?? "",
100
+ refund_tx_hash: payment.refund_tx_hash ?? "",
101
+ });
102
+ }, [onPaymentBounced, orderId, orderMetadata, orderStatus, payment]);
103
+ useEffect(() => {
104
+ if (sentComplete.current || !orderId || !payment || orderStatus !== PayOrderStatus.COMPLETED)
105
+ return;
106
+ sentComplete.current = true;
71
107
  onPaymentCompleted?.({
72
108
  type: "payorder_completed",
73
109
  payorder_id: orderId,
@@ -77,6 +113,6 @@ export function usePaymentLifecycle(order, handlers, options = {}) {
77
113
  source_tx_hash: payment.source_tx_hash ?? "",
78
114
  destination_tx_hash: payment.destination_tx_hash ?? "",
79
115
  });
80
- }, [isFinalized, onPaymentBounced, onPaymentCompleted, orderId, orderMetadata, orderStatus, payment]);
116
+ }, [onPaymentCompleted, orderId, orderMetadata, orderStatus, payment]);
81
117
  return { isStarted, isFinalized, order };
82
118
  }
@@ -1,5 +1,5 @@
1
1
  import { ApiClient as ApiClientInternal, APIEnvironment } from "@coin-voyage/shared/api";
2
- export declare function ApiClient({ apiKey, environment, }: {
2
+ export declare function ApiClient({ apiKey, environment }: {
3
3
  apiKey: string;
4
4
  environment?: APIEnvironment;
5
5
  }): ApiClientInternal;
@@ -1,7 +1,7 @@
1
1
  import { ApiClient as ApiClientInternal } from "@coin-voyage/shared/api";
2
2
  import { paykitVersion } from "../../utils/version";
3
3
  import { v4 as uuidv4 } from "uuid";
4
- export function ApiClient({ apiKey, environment = "production", }) {
4
+ export function ApiClient({ apiKey, environment = "production" }) {
5
5
  return new ApiClientInternal({
6
6
  apiKey,
7
7
  version: paykitVersion,
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useInWagmiContext } from "@coin-voyage/crypto/evm";
3
3
  import { useAccount, useConnectCallback } from "@coin-voyage/crypto/hooks";
4
4
  import { getDepositAddress, getPaymentStep } from "@coin-voyage/shared/payment";
5
- import { PaymentMethod, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
5
+ import { PaymentMethod, PayOrderStatus } from "@coin-voyage/shared/types";
6
6
  import { Buffer } from "buffer";
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { ThemeProvider as StyledThemeProvider } from "styled-components";
9
9
  import { PayContext } from "../components/contexts/pay/index";
10
10
  import { PayModal } from "../components/pay-modal/index";
11
11
  import { useThemeFont } from "../hooks/useGoogleFont";
12
+ import { useOrderStatusWS } from "../hooks/useOrderStatusWS";
12
13
  import { usePaymentState } from "../hooks/usePaymentState";
13
14
  import defaultTheme from "../styles/defaultTheme";
14
15
  import { ROUTE } from "../types/routes";
@@ -167,13 +168,17 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
167
168
  removeInitStateParam();
168
169
  }
169
170
  }, [setOpen, setPayId, setConnectorChainType]);
170
- useEffect(() => {
171
- const intervalMs = getPollingIntervalMs(payOrder?.status, payOrder?.mode);
172
- if (!intervalMs)
171
+ const onOrderStatusEvent = useCallback((event) => {
172
+ if (!("payorder_id" in event) || event.payorder_id !== payOrder?.id)
173
173
  return;
174
- const timeoutId = setTimeout(refreshOrder, intervalMs);
175
- return () => clearTimeout(timeoutId);
176
- }, [payOrder?.mode, payOrder?.status, refreshOrder]);
174
+ void refreshOrder();
175
+ }, [payOrder?.id, refreshOrder]);
176
+ useOrderStatusWS({
177
+ orderId: payOrder?.id,
178
+ enabled: shouldSubscribeOrderStatus(payOrder?.status),
179
+ onEvent: onOrderStatusEvent,
180
+ onError: log,
181
+ });
177
182
  useEffect(() => {
178
183
  if (isFinalPayOrderStatus(payOrder?.status)) {
179
184
  setRoute(ROUTE.CONFIRMATION);
@@ -259,17 +264,11 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
259
264
  // === Helper functions ===
260
265
  const NON_FINAL_PAY_ORDER_STATUSES = [PayOrderStatus.PENDING, PayOrderStatus.AWAITING_PAYMENT, PayOrderStatus.EXPIRED];
261
266
  const isFinalPayOrderStatus = (status) => !!status && !NON_FINAL_PAY_ORDER_STATUSES.includes(status);
262
- const getPollingIntervalMs = (status, mode) => {
263
- if (!status)
264
- return null;
265
- if (status === PayOrderStatus.AWAITING_PAYMENT) {
266
- return 5000;
267
- }
268
- if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(status)) {
269
- return 2500;
270
- }
271
- if (status === PayOrderStatus.EXECUTING_ORDER) {
272
- return mode === PayOrderMode.DEPOSIT ? 1000 : 2500;
273
- }
274
- return null;
275
- };
267
+ const ORDER_STATUS_SUBSCRIPTION_STATUSES = [
268
+ PayOrderStatus.PENDING,
269
+ PayOrderStatus.AWAITING_PAYMENT,
270
+ PayOrderStatus.AWAITING_CONFIRMATION,
271
+ PayOrderStatus.OPTIMISTIC_CONFIRMED,
272
+ PayOrderStatus.EXECUTING_ORDER,
273
+ ];
274
+ const shouldSubscribeOrderStatus = (status) => !!status && ORDER_STATUS_SUBSCRIPTION_STATUSES.includes(status);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coin-voyage/paykit",
3
3
  "description": "Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.",
4
- "version": "2.4.4",
4
+ "version": "2.4.5-beta.0",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -61,7 +61,7 @@
61
61
  "styled-components": "^5.3.11",
62
62
  "uuid": "13.0.0",
63
63
  "@coin-voyage/crypto": "2.4.4",
64
- "@coin-voyage/shared": "2.4.4"
64
+ "@coin-voyage/shared": "2.4.5-beta.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/qrcode": "1.5.5",