@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 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
@@ -0,0 +1,6 @@
1
+ export * from './context/PaymentsProvider';
2
+ export * from './hooks/usePayments';
3
+ export * from './components/Pricing';
4
+ export * from './components/PortalButton';
5
+ export * from './components/FeatureControl';
6
+ export * from './components/Toast';