@t402/react 2.3.0 → 2.4.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/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @t402/react
2
+
3
+ React components and hooks for the t402 Payment Protocol. Build payment-aware UIs with pre-built components and reactive hooks.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @t402/react
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { PaymentProvider, PaymentButton, usePaymentRequired } from '@t402/react'
15
+
16
+ function App() {
17
+ return (
18
+ <PaymentProvider>
19
+ <ProtectedContent />
20
+ </PaymentProvider>
21
+ )
22
+ }
23
+
24
+ function ProtectedContent() {
25
+ const { paymentRequired, requirements, pay, status } = usePaymentRequired('/api/data')
26
+
27
+ if (paymentRequired) {
28
+ return <PaymentButton requirements={requirements} onPay={pay} status={status} />
29
+ }
30
+
31
+ return <div>Premium content loaded!</div>
32
+ }
33
+ ```
34
+
35
+ ## Components
36
+
37
+ ### PaymentButton
38
+
39
+ Renders a payment action button with loading and status states.
40
+
41
+ ```tsx
42
+ import { PaymentButton } from '@t402/react'
43
+ ;<PaymentButton requirements={requirements} onPay={handlePay} status={status} />
44
+ ```
45
+
46
+ ### PaymentDetails
47
+
48
+ Displays payment requirements (amount, network, recipient).
49
+
50
+ ```tsx
51
+ import { PaymentDetails } from '@t402/react'
52
+ ;<PaymentDetails requirements={requirements} />
53
+ ```
54
+
55
+ ### PaymentStatusDisplay
56
+
57
+ Shows the current payment status with appropriate UI feedback.
58
+
59
+ ```tsx
60
+ import { PaymentStatusDisplay } from '@t402/react'
61
+ ;<PaymentStatusDisplay status={status} />
62
+ ```
63
+
64
+ ### AddressDisplay
65
+
66
+ Renders a blockchain address with truncation and copy functionality.
67
+
68
+ ```tsx
69
+ import { AddressDisplay } from '@t402/react'
70
+ ;<AddressDisplay address="0x1234...5678" />
71
+ ```
72
+
73
+ ## Hooks
74
+
75
+ ### usePaymentRequired
76
+
77
+ Detects 402 responses and extracts payment requirements.
78
+
79
+ ```tsx
80
+ const {
81
+ paymentRequired, // boolean - whether payment is needed
82
+ requirements, // PaymentRequirements[] from 402 response
83
+ pay, // () => Promise<void> - trigger payment
84
+ status, // PaymentStatus - current state
85
+ } = usePaymentRequired(url, options)
86
+ ```
87
+
88
+ ### usePaymentStatus
89
+
90
+ Tracks the lifecycle of a payment (idle, pending, confirming, complete, error).
91
+
92
+ ```tsx
93
+ const { status, error, reset } = usePaymentStatus()
94
+ ```
95
+
96
+ ### useAsyncPayment
97
+
98
+ Manages async payment flows with automatic retry and status tracking.
99
+
100
+ ```tsx
101
+ const { execute, status, error } = useAsyncPayment(paymentFn)
102
+ ```
103
+
104
+ ## Provider
105
+
106
+ ### PaymentProvider
107
+
108
+ Wraps your app to provide payment context to all child components.
109
+
110
+ ```tsx
111
+ import { PaymentProvider } from '@t402/react'
112
+ ;<PaymentProvider>
113
+ <App />
114
+ </PaymentProvider>
115
+ ```
116
+
117
+ Access context directly with `usePaymentContext()`.
118
+
119
+ ## Utilities
120
+
121
+ ```typescript
122
+ import {
123
+ isEvmNetwork,
124
+ isSvmNetwork,
125
+ isTonNetwork,
126
+ getNetworkDisplayName,
127
+ truncateAddress,
128
+ formatTokenAmount,
129
+ choosePaymentRequirement,
130
+ } from '@t402/react'
131
+
132
+ // Network detection
133
+ isEvmNetwork('eip155:8453') // true
134
+
135
+ // Display helpers
136
+ getNetworkDisplayName('eip155:8453') // "Base"
137
+ truncateAddress('0x1234567890abcdef') // "0x1234...cdef"
138
+ formatTokenAmount(1500000n, 6) // "1.50"
139
+ ```
140
+
141
+ ## Peer Dependencies
142
+
143
+ - `react` ^18.0.0 || ^19.0.0
144
+ - `react-dom` ^18.0.0 || ^19.0.0
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ pnpm build # Build the package
150
+ pnpm test # Run unit tests
151
+ ```
152
+
153
+ ## Related Packages
154
+
155
+ - `@t402/core` - Core protocol types
156
+ - `@t402/vue` - Vue components & composables
157
+ - `@t402/paywall` - Universal paywall UI
158
+ - `@t402/fetch` - Fetch wrapper with automatic payment
@@ -628,6 +628,262 @@ function useAsyncPayment(options) {
628
628
  reset
629
629
  };
630
630
  }
631
+ function useGaslessPayment(options) {
632
+ const { payFn, onSuccess, onError, autoWait = true } = options;
633
+ const [status, setStatus] = react.useState("idle");
634
+ const [userOpHash, setUserOpHash] = react.useState(null);
635
+ const [txHash, setTxHash] = react.useState(null);
636
+ const [sponsored, setSponsored] = react.useState(null);
637
+ const [error, setError] = react.useState(null);
638
+ const isMountedRef = react.useRef(true);
639
+ const pay = react.useCallback(
640
+ async (params) => {
641
+ setStatus("loading");
642
+ setError(null);
643
+ setUserOpHash(null);
644
+ setTxHash(null);
645
+ setSponsored(null);
646
+ try {
647
+ const result = await payFn(params);
648
+ if (isMountedRef.current) {
649
+ setUserOpHash(result.userOpHash);
650
+ setSponsored(result.sponsored);
651
+ }
652
+ if (autoWait) {
653
+ const receipt = await result.wait();
654
+ if (isMountedRef.current) {
655
+ setTxHash(receipt.txHash);
656
+ setStatus("success");
657
+ onSuccess?.(receipt);
658
+ }
659
+ } else {
660
+ if (isMountedRef.current) {
661
+ setStatus("success");
662
+ onSuccess?.({ txHash: result.userOpHash, success: true });
663
+ }
664
+ }
665
+ } catch (err) {
666
+ const errorMessage = err instanceof Error ? err.message : "Gasless payment failed";
667
+ if (isMountedRef.current) {
668
+ setError(errorMessage);
669
+ setStatus("error");
670
+ }
671
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
672
+ }
673
+ },
674
+ [payFn, onSuccess, onError, autoWait]
675
+ );
676
+ const reset = react.useCallback(() => {
677
+ setStatus("idle");
678
+ setUserOpHash(null);
679
+ setTxHash(null);
680
+ setSponsored(null);
681
+ setError(null);
682
+ }, []);
683
+ return {
684
+ pay,
685
+ status,
686
+ userOpHash,
687
+ txHash,
688
+ sponsored,
689
+ error,
690
+ isLoading: status === "loading",
691
+ isSuccess: status === "success",
692
+ isError: status === "error",
693
+ reset
694
+ };
695
+ }
696
+ function useBridgePayment(options) {
697
+ const { bridgeFn, autoBridgeFn, onSuccess, onError, autoWaitForDelivery = false } = options;
698
+ const [status, setStatus] = react.useState("idle");
699
+ const [txHash, setTxHash] = react.useState(null);
700
+ const [messageGuid, setMessageGuid] = react.useState(null);
701
+ const [dstTxHash, setDstTxHash] = react.useState(null);
702
+ const [error, setError] = react.useState(null);
703
+ const isMountedRef = react.useRef(true);
704
+ const executeBridge = react.useCallback(
705
+ async (bridgeCall) => {
706
+ setStatus("bridging");
707
+ setError(null);
708
+ setTxHash(null);
709
+ setMessageGuid(null);
710
+ setDstTxHash(null);
711
+ try {
712
+ const result = await bridgeCall();
713
+ if (isMountedRef.current) {
714
+ setTxHash(result.txHash);
715
+ setMessageGuid(result.messageGuid);
716
+ }
717
+ if (autoWaitForDelivery) {
718
+ if (isMountedRef.current) setStatus("waiting");
719
+ const delivery = await result.waitForDelivery();
720
+ if (isMountedRef.current) {
721
+ if (delivery.dstTxHash) setDstTxHash(delivery.dstTxHash);
722
+ setStatus("success");
723
+ onSuccess?.({
724
+ txHash: result.txHash,
725
+ dstTxHash: delivery.dstTxHash,
726
+ fromChain: result.fromChain,
727
+ toChain: result.toChain
728
+ });
729
+ }
730
+ } else {
731
+ if (isMountedRef.current) {
732
+ setStatus("success");
733
+ onSuccess?.({
734
+ txHash: result.txHash,
735
+ fromChain: result.fromChain,
736
+ toChain: result.toChain
737
+ });
738
+ }
739
+ }
740
+ } catch (err) {
741
+ const errorMessage = err instanceof Error ? err.message : "Bridge operation failed";
742
+ if (isMountedRef.current) {
743
+ setError(errorMessage);
744
+ setStatus("error");
745
+ }
746
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
747
+ }
748
+ },
749
+ [onSuccess, onError, autoWaitForDelivery]
750
+ );
751
+ const bridge = react.useCallback(
752
+ async (params) => {
753
+ await executeBridge(() => bridgeFn(params));
754
+ },
755
+ [bridgeFn, executeBridge]
756
+ );
757
+ const autoBridge = react.useCallback(
758
+ async (params) => {
759
+ const fn = autoBridgeFn ?? (() => {
760
+ throw new Error("autoBridgeFn not provided");
761
+ });
762
+ await executeBridge(() => fn(params));
763
+ },
764
+ [autoBridgeFn, executeBridge]
765
+ );
766
+ const reset = react.useCallback(() => {
767
+ setStatus("idle");
768
+ setTxHash(null);
769
+ setMessageGuid(null);
770
+ setDstTxHash(null);
771
+ setError(null);
772
+ }, []);
773
+ return {
774
+ bridge,
775
+ autoBridge,
776
+ status,
777
+ txHash,
778
+ messageGuid,
779
+ dstTxHash,
780
+ error,
781
+ isLoading: status === "bridging" || status === "quoting" || status === "waiting",
782
+ isSuccess: status === "success",
783
+ isError: status === "error",
784
+ reset
785
+ };
786
+ }
787
+ function useMultiSigPayment(options) {
788
+ const { initiateFn, submitFn, onSuccess, onError, autoWait = true } = options;
789
+ const [status, setStatus] = react.useState("idle");
790
+ const [requestId, setRequestId] = react.useState(null);
791
+ const [userOpHash, setUserOpHash] = react.useState(null);
792
+ const [txHash, setTxHash] = react.useState(null);
793
+ const [threshold, setThreshold] = react.useState(0);
794
+ const [collectedCount, setCollectedCount] = react.useState(0);
795
+ const [isReady, setIsReady] = react.useState(false);
796
+ const [error, setError] = react.useState(null);
797
+ const isMountedRef = react.useRef(true);
798
+ const initiate = react.useCallback(
799
+ async (params) => {
800
+ setStatus("initiating");
801
+ setError(null);
802
+ setRequestId(null);
803
+ setUserOpHash(null);
804
+ setTxHash(null);
805
+ try {
806
+ const result = await initiateFn(params);
807
+ if (isMountedRef.current) {
808
+ setRequestId(result.requestId);
809
+ setUserOpHash(result.userOpHash);
810
+ setThreshold(result.threshold);
811
+ setCollectedCount(result.collectedCount);
812
+ setIsReady(result.isReady);
813
+ setStatus("collecting");
814
+ }
815
+ } catch (err) {
816
+ const errorMessage = err instanceof Error ? err.message : "Failed to initiate multi-sig payment";
817
+ if (isMountedRef.current) {
818
+ setError(errorMessage);
819
+ setStatus("error");
820
+ }
821
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
822
+ }
823
+ },
824
+ [initiateFn, onError]
825
+ );
826
+ const submit = react.useCallback(async () => {
827
+ if (!requestId) {
828
+ const err = new Error("No active request to submit");
829
+ setError(err.message);
830
+ setStatus("error");
831
+ onError?.(err);
832
+ return;
833
+ }
834
+ setStatus("submitting");
835
+ setError(null);
836
+ try {
837
+ const result = await submitFn(requestId);
838
+ if (autoWait) {
839
+ const receipt = await result.wait();
840
+ if (isMountedRef.current) {
841
+ setTxHash(receipt.txHash);
842
+ setStatus("success");
843
+ onSuccess?.(receipt);
844
+ }
845
+ } else {
846
+ if (isMountedRef.current) {
847
+ setStatus("success");
848
+ onSuccess?.({ txHash: result.userOpHash, success: true });
849
+ }
850
+ }
851
+ } catch (err) {
852
+ const errorMessage = err instanceof Error ? err.message : "Failed to submit multi-sig transaction";
853
+ if (isMountedRef.current) {
854
+ setError(errorMessage);
855
+ setStatus("error");
856
+ }
857
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
858
+ }
859
+ }, [requestId, submitFn, onSuccess, onError, autoWait]);
860
+ const reset = react.useCallback(() => {
861
+ setStatus("idle");
862
+ setRequestId(null);
863
+ setUserOpHash(null);
864
+ setTxHash(null);
865
+ setThreshold(0);
866
+ setCollectedCount(0);
867
+ setIsReady(false);
868
+ setError(null);
869
+ }, []);
870
+ return {
871
+ initiate,
872
+ submit,
873
+ status,
874
+ requestId,
875
+ userOpHash,
876
+ txHash,
877
+ threshold,
878
+ collectedCount,
879
+ isReady,
880
+ error,
881
+ isLoading: status === "initiating" || status === "submitting",
882
+ isSuccess: status === "success",
883
+ isError: status === "error",
884
+ reset
885
+ };
886
+ }
631
887
  var initialState = {
632
888
  status: "idle",
633
889
  error: null,
@@ -738,6 +994,9 @@ exports.isTronNetwork = isTronNetwork;
738
994
  exports.normalizePaymentRequirements = normalizePaymentRequirements;
739
995
  exports.truncateAddress = truncateAddress;
740
996
  exports.useAsyncPayment = useAsyncPayment;
997
+ exports.useBridgePayment = useBridgePayment;
998
+ exports.useGaslessPayment = useGaslessPayment;
999
+ exports.useMultiSigPayment = useMultiSigPayment;
741
1000
  exports.usePaymentContext = usePaymentContext;
742
1001
  exports.usePaymentRequired = usePaymentRequired;
743
1002
  exports.usePaymentStatus = usePaymentStatus;