@riligar/payments-react 1.0.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/package.json +38 -0
- package/src/components/FeatureControl.jsx +16 -0
- package/src/components/PortalButton.jsx +53 -0
- package/src/components/Pricing.jsx +141 -0
- package/src/components/Toast.jsx +77 -0
- package/src/context/PaymentsProvider.jsx +78 -0
- package/src/hooks/usePayments.js +73 -0
- package/src/index.js +6 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@riligar/payments-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React SDK for Riligar Payments Hub",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"module": "./src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "echo 'No build step required for pure ESM source' && exit 0",
|
|
10
|
+
"release": "semantic-release",
|
|
11
|
+
"release:dry-run": "semantic-release --dry-run"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/riligar-applications/payments.git",
|
|
22
|
+
"directory": "packages/payments-react"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@tabler/icons-react": "^3.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
32
|
+
"@semantic-release/git": "^10.0.1",
|
|
33
|
+
"@semantic-release/github": "^10.0.0",
|
|
34
|
+
"@semantic-release/npm": "^12.0.0",
|
|
35
|
+
"@semantic-release/release-notes-generator": "^14.0.0",
|
|
36
|
+
"semantic-release": "^24.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useHasFeature } from '../hooks/usePayments';
|
|
3
|
+
|
|
4
|
+
export const FeatureControl = ({
|
|
5
|
+
featureId,
|
|
6
|
+
children,
|
|
7
|
+
fallback = null
|
|
8
|
+
}) => {
|
|
9
|
+
const hasAccess = useHasFeature(featureId);
|
|
10
|
+
|
|
11
|
+
if (!hasAccess) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return children;
|
|
16
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconExternalLink } from '@tabler/icons-react';
|
|
3
|
+
import { usePaymentsContext } from '../context/PaymentsProvider';
|
|
4
|
+
import { usePortal } from '../hooks/usePayments';
|
|
5
|
+
|
|
6
|
+
export const PortalButton = ({
|
|
7
|
+
returnUrl,
|
|
8
|
+
label = 'Gerenciar Assinatura',
|
|
9
|
+
customStyles = {}
|
|
10
|
+
}) => {
|
|
11
|
+
const { userId } = usePaymentsContext();
|
|
12
|
+
const { openPortal } = usePortal();
|
|
13
|
+
|
|
14
|
+
const handleClick = async () => {
|
|
15
|
+
try {
|
|
16
|
+
await openPortal({ userId, returnUrl });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
// Notification handled by hook
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
onClick={handleClick}
|
|
25
|
+
style={{
|
|
26
|
+
display: 'inline-flex',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
padding: '0.625rem 1.25rem',
|
|
29
|
+
backgroundColor: '#fff',
|
|
30
|
+
color: '#000',
|
|
31
|
+
border: '1px solid #eee',
|
|
32
|
+
borderRadius: '8px',
|
|
33
|
+
fontSize: '0.875rem',
|
|
34
|
+
fontWeight: 600,
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
transition: 'all 0.2s ease',
|
|
37
|
+
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
38
|
+
...customStyles
|
|
39
|
+
}}
|
|
40
|
+
onMouseOver={(e) => {
|
|
41
|
+
e.target.style.backgroundColor = '#f9f9f9';
|
|
42
|
+
e.target.style.borderColor = '#ddd';
|
|
43
|
+
}}
|
|
44
|
+
onMouseOut={(e) => {
|
|
45
|
+
e.target.style.backgroundColor = '#fff';
|
|
46
|
+
e.target.style.borderColor = '#eee';
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<IconExternalLink size={16} style={{ marginRight: '0.5rem' }} />
|
|
50
|
+
{label}
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconCheck, IconPackage } from '@tabler/icons-react';
|
|
3
|
+
import { usePaymentsContext } from '../context/PaymentsProvider';
|
|
4
|
+
import { useCheckout } from '../hooks/usePayments';
|
|
5
|
+
|
|
6
|
+
export const Pricing = ({
|
|
7
|
+
successUrl,
|
|
8
|
+
cancelUrl,
|
|
9
|
+
onCheckoutStart,
|
|
10
|
+
customStyles = {}
|
|
11
|
+
}) => {
|
|
12
|
+
const { plans, loading, userId } = usePaymentsContext();
|
|
13
|
+
const { createCheckout } = useCheckout();
|
|
14
|
+
|
|
15
|
+
if (loading) return <div className="p-8 text-center" style={{ fontFamily: "'Montserrat', sans-serif", color: '#666', fontSize: '0.9rem' }}>Carregando planos...</div>;
|
|
16
|
+
|
|
17
|
+
if (!plans || plans.length === 0) {
|
|
18
|
+
return (
|
|
19
|
+
<div style={{
|
|
20
|
+
padding: '2rem 1.5rem',
|
|
21
|
+
textAlign: 'center',
|
|
22
|
+
backgroundColor: '#fff',
|
|
23
|
+
border: '1px solid #eee',
|
|
24
|
+
borderRadius: '12px',
|
|
25
|
+
fontFamily: "'Montserrat', sans-serif",
|
|
26
|
+
maxWidth: '400px',
|
|
27
|
+
margin: '2rem auto',
|
|
28
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.02)'
|
|
29
|
+
}}>
|
|
30
|
+
<div style={{ marginBottom: '1rem', color: '#000' }}>
|
|
31
|
+
<IconPackage size={20} stroke={2} />
|
|
32
|
+
</div>
|
|
33
|
+
<h3 style={{ fontFamily: "'Montserrat', sans-serif", color: '#000', fontSize: '0.95rem', fontWeight: 700, marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
|
34
|
+
Configuração Pendente
|
|
35
|
+
</h3>
|
|
36
|
+
<p style={{ fontSize: '0.8rem', lineHeight: 1.5, margin: 0, color: '#666' }}>
|
|
37
|
+
Não identificamos planos vinculados a este projeto. Configure as ofertas no dashboard para habilitar o checkout.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleSubscribe = async (plan) => {
|
|
44
|
+
if (onCheckoutStart) onCheckoutStart(plan);
|
|
45
|
+
try {
|
|
46
|
+
await createCheckout({
|
|
47
|
+
planId: plan.id,
|
|
48
|
+
userId,
|
|
49
|
+
successUrl,
|
|
50
|
+
cancelUrl
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Notification handled by hook
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="pricing-grid" style={{
|
|
59
|
+
display: 'grid',
|
|
60
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
|
61
|
+
gap: '1.5rem',
|
|
62
|
+
padding: '1.5rem',
|
|
63
|
+
fontFamily: "'Montserrat', sans-serif",
|
|
64
|
+
...customStyles.grid
|
|
65
|
+
}}>
|
|
66
|
+
{plans.map((plan) => (
|
|
67
|
+
<div key={plan.id} className="pricing-card" style={{
|
|
68
|
+
border: '1px solid #eee',
|
|
69
|
+
borderRadius: '12px',
|
|
70
|
+
padding: '2rem',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
flexDirection: 'column',
|
|
73
|
+
backgroundColor: '#fff',
|
|
74
|
+
transition: 'all 0.2s ease',
|
|
75
|
+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.02)',
|
|
76
|
+
...customStyles.card
|
|
77
|
+
}}>
|
|
78
|
+
<h3 style={{
|
|
79
|
+
fontFamily: "'Montserrat', sans-serif",
|
|
80
|
+
fontSize: '0.9rem',
|
|
81
|
+
fontWeight: 800,
|
|
82
|
+
marginBottom: '0.5rem',
|
|
83
|
+
color: '#000',
|
|
84
|
+
textTransform: 'uppercase',
|
|
85
|
+
letterSpacing: '1px'
|
|
86
|
+
}}>
|
|
87
|
+
{plan.name}
|
|
88
|
+
</h3>
|
|
89
|
+
<div style={{ marginBottom: '2rem', display: 'flex', alignItems: 'baseline' }}>
|
|
90
|
+
<span style={{
|
|
91
|
+
fontFamily: "'Montserrat', sans-serif",
|
|
92
|
+
fontSize: '2.5rem',
|
|
93
|
+
fontWeight: 900,
|
|
94
|
+
color: '#000',
|
|
95
|
+
letterSpacing: '-1.5px',
|
|
96
|
+
lineHeight: 1
|
|
97
|
+
}}>
|
|
98
|
+
{(plan.price / 100).toLocaleString('pt-BR', { style: 'currency', currency: plan.currency || 'BRL' })}
|
|
99
|
+
</span>
|
|
100
|
+
<span style={{ color: '#999', marginLeft: '0.4rem', fontSize: '0.75rem', fontWeight: 600, textTransform: 'uppercase' }}>/mês</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 2rem 0', flex: 1 }}>
|
|
104
|
+
<li style={{ display: 'flex', alignItems: 'center', marginBottom: '1rem', color: '#555', fontSize: '0.85rem', fontWeight: 500 }}>
|
|
105
|
+
<IconCheck size={16} style={{ marginRight: '0.75rem', color: '#000' }} stroke={2.5} />
|
|
106
|
+
Acesso total aos recursos
|
|
107
|
+
</li>
|
|
108
|
+
<li style={{ display: 'flex', alignItems: 'center', marginBottom: '1rem', color: '#555', fontSize: '0.85rem', fontWeight: 500 }}>
|
|
109
|
+
<IconCheck size={16} style={{ marginRight: '0.75rem', color: '#000' }} stroke={2.5} />
|
|
110
|
+
Suporte prioritário exclusivo
|
|
111
|
+
</li>
|
|
112
|
+
</ul>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => handleSubscribe(plan)}
|
|
116
|
+
style={{
|
|
117
|
+
backgroundColor: '#000',
|
|
118
|
+
color: '#fff',
|
|
119
|
+
border: 'none',
|
|
120
|
+
borderRadius: '8px',
|
|
121
|
+
padding: '1rem',
|
|
122
|
+
fontSize: '0.8rem',
|
|
123
|
+
fontWeight: 800,
|
|
124
|
+
cursor: 'pointer',
|
|
125
|
+
transition: 'all 0.15s ease',
|
|
126
|
+
fontFamily: "'Montserrat', sans-serif",
|
|
127
|
+
textTransform: 'uppercase',
|
|
128
|
+
letterSpacing: '1px',
|
|
129
|
+
width: '100%',
|
|
130
|
+
...customStyles.button
|
|
131
|
+
}}
|
|
132
|
+
onMouseDown={(e) => e.target.style.transform = 'scale(0.97)'}
|
|
133
|
+
onMouseUp={(e) => e.target.style.transform = 'scale(1)'}
|
|
134
|
+
>
|
|
135
|
+
Assinar Agora
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { IconCircleCheck, IconCircleX, IconInfoCircle, IconX } from '@tabler/icons-react';
|
|
3
|
+
|
|
4
|
+
export const Toast = ({
|
|
5
|
+
message,
|
|
6
|
+
type = 'info',
|
|
7
|
+
onClose,
|
|
8
|
+
duration = 5000
|
|
9
|
+
}) => {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const timer = setTimeout(onClose, duration);
|
|
12
|
+
return () => clearTimeout(timer);
|
|
13
|
+
}, [onClose, duration]);
|
|
14
|
+
|
|
15
|
+
const icons = {
|
|
16
|
+
success: <IconCircleCheck size={20} color="#000" />,
|
|
17
|
+
error: <IconCircleX size={20} color="#000" />,
|
|
18
|
+
info: <IconInfoCircle size={20} color="#000" />
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const bgColors = {
|
|
22
|
+
success: '#fff',
|
|
23
|
+
error: '#fff',
|
|
24
|
+
info: '#fff'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const borderColors = {
|
|
28
|
+
success: '#ddd',
|
|
29
|
+
error: '#ddd',
|
|
30
|
+
info: '#ddd'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div style={{
|
|
35
|
+
position: 'fixed',
|
|
36
|
+
bottom: '24px',
|
|
37
|
+
right: '24px',
|
|
38
|
+
zIndex: 9999,
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
padding: '16px 20px',
|
|
42
|
+
backgroundColor: bgColors[type],
|
|
43
|
+
border: `1px solid ${borderColors[type]}`,
|
|
44
|
+
borderRadius: '8px',
|
|
45
|
+
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
|
46
|
+
minWidth: '320px',
|
|
47
|
+
animation: 'slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
48
|
+
fontFamily: "'Montserrat', sans-serif"
|
|
49
|
+
}}>
|
|
50
|
+
<style>{`
|
|
51
|
+
@keyframes slideIn {
|
|
52
|
+
from { transform: translateY(20px); opacity: 0; }
|
|
53
|
+
to { transform: translateY(0); opacity: 1; }
|
|
54
|
+
}
|
|
55
|
+
`}</style>
|
|
56
|
+
<div style={{ marginRight: '12px', display: 'flex' }}>
|
|
57
|
+
{icons[type]}
|
|
58
|
+
</div>
|
|
59
|
+
<div style={{ flex: 1, color: '#111', fontSize: '14px', fontWeight: 500 }}>
|
|
60
|
+
{message}
|
|
61
|
+
</div>
|
|
62
|
+
<button
|
|
63
|
+
onClick={onClose}
|
|
64
|
+
style={{
|
|
65
|
+
background: 'none',
|
|
66
|
+
border: 'none',
|
|
67
|
+
padding: '4px',
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
color: '#666',
|
|
70
|
+
display: 'flex'
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<IconX size={16} />
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { Toast } from '../components/Toast';
|
|
3
|
+
|
|
4
|
+
const PaymentsContext = createContext(null);
|
|
5
|
+
|
|
6
|
+
export const PaymentsProvider = ({
|
|
7
|
+
children,
|
|
8
|
+
publicKey,
|
|
9
|
+
userId,
|
|
10
|
+
apiBase = 'https://payments-api.riligar.com'
|
|
11
|
+
}) => {
|
|
12
|
+
const [plans, setPlans] = useState([]);
|
|
13
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const [toast, setToast] = useState(null);
|
|
17
|
+
|
|
18
|
+
const showToast = (message, type = 'info') => {
|
|
19
|
+
setToast({ message, type });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const fetchPlans = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${apiBase}/sdk/v1/plans?publicKey=${publicKey}`);
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
setPlans(data.data || []);
|
|
28
|
+
} else {
|
|
29
|
+
showToast(data.message || 'Erro ao carregar planos', 'error');
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
showToast('Falha na conexão com o servidor', 'error');
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (publicKey) {
|
|
40
|
+
fetchPlans();
|
|
41
|
+
} else {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [publicKey, userId]);
|
|
45
|
+
|
|
46
|
+
const value = {
|
|
47
|
+
publicKey,
|
|
48
|
+
userId,
|
|
49
|
+
apiBase,
|
|
50
|
+
plans,
|
|
51
|
+
subscriptions,
|
|
52
|
+
loading,
|
|
53
|
+
error,
|
|
54
|
+
refreshPlans: fetchPlans,
|
|
55
|
+
showToast
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<PaymentsContext.Provider value={value}>
|
|
60
|
+
{children}
|
|
61
|
+
{toast && (
|
|
62
|
+
<Toast
|
|
63
|
+
message={toast.message}
|
|
64
|
+
type={toast.type}
|
|
65
|
+
onClose={() => setToast(null)}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</PaymentsContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const usePaymentsContext = () => {
|
|
73
|
+
const context = useContext(PaymentsContext);
|
|
74
|
+
if (!context) {
|
|
75
|
+
throw new Error('usePaymentsContext must be used within a PaymentsProvider');
|
|
76
|
+
}
|
|
77
|
+
return context;
|
|
78
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { usePaymentsContext } from '../context/PaymentsProvider';
|
|
2
|
+
|
|
3
|
+
export const useCheckout = () => {
|
|
4
|
+
const { apiBase, showToast } = usePaymentsContext();
|
|
5
|
+
|
|
6
|
+
const createCheckout = async ({ planId, userId, successUrl, cancelUrl }) => {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(`${apiBase}/checkout`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
planId,
|
|
13
|
+
userId,
|
|
14
|
+
success_url: successUrl,
|
|
15
|
+
cancel_url: cancelUrl
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
const checkoutUrl = data.data?.url || data.url;
|
|
21
|
+
|
|
22
|
+
if (checkoutUrl) {
|
|
23
|
+
window.location.href = checkoutUrl;
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error(data.message || 'Failed to create checkout session');
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
showToast(err.message, 'error');
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return { createCheckout };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const usePortal = () => {
|
|
37
|
+
const { apiBase, publicKey, showToast } = usePaymentsContext();
|
|
38
|
+
|
|
39
|
+
const openPortal = async ({ userId, returnUrl }) => {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${apiBase}/account`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
userId,
|
|
46
|
+
return_url: returnUrl,
|
|
47
|
+
publicKey
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
const portalUrl = data.data?.url || data.url;
|
|
53
|
+
|
|
54
|
+
if (portalUrl) {
|
|
55
|
+
window.location.href = portalUrl;
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(data.message || 'Failed to open customer portal');
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
showToast(err.message, 'error');
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return { openPortal };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const useHasFeature = (featureId) => {
|
|
69
|
+
const { subscriptions } = usePaymentsContext();
|
|
70
|
+
// Simple implementation: check if any active subscription matches the plan info
|
|
71
|
+
// This can be expanded based on how features are mapped to plans.
|
|
72
|
+
return subscriptions.some(sub => sub.status === 'active' && sub.planName === featureId);
|
|
73
|
+
};
|
package/src/index.js
ADDED