strapi-plugin-magic-mail 1.0.1

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.
Files changed (91) hide show
  1. package/COPYRIGHT_NOTICE.txt +13 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1420 -0
  4. package/admin/jsconfig.json +10 -0
  5. package/admin/src/components/AddAccountModal.jsx +1943 -0
  6. package/admin/src/components/Initializer.jsx +14 -0
  7. package/admin/src/components/LicenseGuard.jsx +475 -0
  8. package/admin/src/components/PluginIcon.jsx +5 -0
  9. package/admin/src/hooks/useAuthRefresh.js +44 -0
  10. package/admin/src/hooks/useLicense.js +158 -0
  11. package/admin/src/index.js +86 -0
  12. package/admin/src/pages/Analytics.jsx +762 -0
  13. package/admin/src/pages/App.jsx +111 -0
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
  16. package/admin/src/pages/HomePage.jsx +1233 -0
  17. package/admin/src/pages/LicensePage.jsx +424 -0
  18. package/admin/src/pages/RoutingRules.jsx +1141 -0
  19. package/admin/src/pages/Settings.jsx +603 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +71 -0
  22. package/admin/src/translations/en.json +70 -0
  23. package/admin/src/translations/es.json +71 -0
  24. package/admin/src/translations/fr.json +71 -0
  25. package/admin/src/translations/pt.json +71 -0
  26. package/admin/src/utils/fetchWithRetry.js +123 -0
  27. package/admin/src/utils/getTranslation.js +5 -0
  28. package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
  29. package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
  30. package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
  31. package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
  32. package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
  33. package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
  34. package/dist/_chunks/de-CN-G9j1S.js +64 -0
  35. package/dist/_chunks/de-DS04rP54.mjs +64 -0
  36. package/dist/_chunks/en-BDc7Jk8u.js +64 -0
  37. package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
  38. package/dist/_chunks/es-BpV1MIdm.js +64 -0
  39. package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
  40. package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
  41. package/dist/_chunks/fr-vpziIpRp.js +64 -0
  42. package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
  43. package/dist/_chunks/pt-ODpAhDNa.js +64 -0
  44. package/dist/admin/index.js +89 -0
  45. package/dist/admin/index.mjs +90 -0
  46. package/dist/server/index.js +6214 -0
  47. package/dist/server/index.mjs +6208 -0
  48. package/package.json +113 -0
  49. package/server/jsconfig.json +10 -0
  50. package/server/src/bootstrap.js +153 -0
  51. package/server/src/config/features.js +260 -0
  52. package/server/src/config/index.js +6 -0
  53. package/server/src/content-types/email-account/schema.json +93 -0
  54. package/server/src/content-types/email-event/index.js +8 -0
  55. package/server/src/content-types/email-event/schema.json +57 -0
  56. package/server/src/content-types/email-link/index.js +8 -0
  57. package/server/src/content-types/email-link/schema.json +49 -0
  58. package/server/src/content-types/email-log/index.js +8 -0
  59. package/server/src/content-types/email-log/schema.json +106 -0
  60. package/server/src/content-types/email-template/schema.json +74 -0
  61. package/server/src/content-types/email-template-version/schema.json +60 -0
  62. package/server/src/content-types/index.js +33 -0
  63. package/server/src/content-types/routing-rule/schema.json +59 -0
  64. package/server/src/controllers/accounts.js +220 -0
  65. package/server/src/controllers/analytics.js +347 -0
  66. package/server/src/controllers/controller.js +26 -0
  67. package/server/src/controllers/email-designer.js +474 -0
  68. package/server/src/controllers/index.js +21 -0
  69. package/server/src/controllers/license.js +267 -0
  70. package/server/src/controllers/oauth.js +474 -0
  71. package/server/src/controllers/routing-rules.js +122 -0
  72. package/server/src/controllers/test.js +383 -0
  73. package/server/src/destroy.js +23 -0
  74. package/server/src/index.js +25 -0
  75. package/server/src/middlewares/index.js +3 -0
  76. package/server/src/policies/index.js +3 -0
  77. package/server/src/register.js +5 -0
  78. package/server/src/routes/admin.js +469 -0
  79. package/server/src/routes/content-api.js +37 -0
  80. package/server/src/routes/index.js +9 -0
  81. package/server/src/services/account-manager.js +277 -0
  82. package/server/src/services/analytics.js +496 -0
  83. package/server/src/services/email-designer.js +870 -0
  84. package/server/src/services/email-router.js +1420 -0
  85. package/server/src/services/index.js +17 -0
  86. package/server/src/services/license-guard.js +418 -0
  87. package/server/src/services/oauth.js +515 -0
  88. package/server/src/services/service.js +7 -0
  89. package/server/src/utils/encryption.js +81 -0
  90. package/strapi-admin.js +4 -0
  91. package/strapi-server.js +4 -0
@@ -0,0 +1,14 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import pluginId from '../pluginId';
3
+
4
+ const Initializer = ({ setPlugin }) => {
5
+ const ref = useRef(setPlugin);
6
+
7
+ useEffect(() => {
8
+ ref.current(pluginId);
9
+ }, []);
10
+
11
+ return null;
12
+ };
13
+
14
+ export default Initializer;
@@ -0,0 +1,475 @@
1
+ import { useState, useEffect } from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+ import {
4
+ Typography,
5
+ Box,
6
+ Flex,
7
+ Button,
8
+ TextInput,
9
+ Loader,
10
+ } from '@strapi/design-system';
11
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
12
+ import { CheckIcon, KeyIcon, XMarkIcon } from '@heroicons/react/24/outline';
13
+ import { useNavigate } from 'react-router-dom';
14
+ import { useAuthRefresh } from '../hooks/useAuthRefresh';
15
+
16
+ // Animations
17
+ const fadeIn = keyframes`
18
+ from { opacity: 0; }
19
+ to { opacity: 1; }
20
+ `;
21
+
22
+ const slideUp = keyframes`
23
+ from {
24
+ opacity: 0;
25
+ transform: translateY(30px);
26
+ }
27
+ to {
28
+ opacity: 1;
29
+ transform: translateY(0);
30
+ }
31
+ `;
32
+
33
+ // Styled Components
34
+ const ModalOverlay = styled.div`
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ right: 0;
39
+ bottom: 0;
40
+ background: rgba(4, 28, 47, 0.85);
41
+ backdrop-filter: blur(8px);
42
+ z-index: 9999;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ animation: ${fadeIn} 0.3s ease-out;
47
+ padding: 20px;
48
+ `;
49
+
50
+ const ModalContent = styled(Box)`
51
+ background: white;
52
+ border-radius: 16px;
53
+ width: 100%;
54
+ max-width: 580px;
55
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
56
+ animation: ${slideUp} 0.4s cubic-bezier(0.4, 0, 0.2, 1);
57
+ overflow: hidden;
58
+ `;
59
+
60
+ const GradientHeader = styled(Box)`
61
+ background: linear-gradient(135deg, #0EA5E9 0%, #A855F7 100%);
62
+ padding: 32px 40px;
63
+ position: relative;
64
+ overflow: hidden;
65
+
66
+ &::before {
67
+ content: '';
68
+ position: absolute;
69
+ top: -50%;
70
+ right: -50%;
71
+ width: 200%;
72
+ height: 200%;
73
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
74
+ }
75
+ `;
76
+
77
+ const IconWrapper = styled.div`
78
+ width: 72px;
79
+ height: 72px;
80
+ border-radius: 50%;
81
+ background: rgba(255, 255, 255, 0.2);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ margin: 0 auto 16px;
86
+ backdrop-filter: blur(10px);
87
+ border: 2px solid rgba(255, 255, 255, 0.3);
88
+
89
+ svg {
90
+ width: 36px;
91
+ height: 36px;
92
+ color: white;
93
+ }
94
+ `;
95
+
96
+ const CloseButton = styled.button`
97
+ position: absolute;
98
+ top: 16px;
99
+ right: 16px;
100
+ background: rgba(255, 255, 255, 0.2);
101
+ border: 2px solid rgba(255, 255, 255, 0.3);
102
+ border-radius: 50%;
103
+ width: 36px;
104
+ height: 36px;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ cursor: pointer;
109
+ transition: all 0.2s;
110
+ z-index: 10;
111
+
112
+ svg {
113
+ width: 20px;
114
+ height: 20px;
115
+ color: white;
116
+ }
117
+
118
+ &:hover {
119
+ background: rgba(255, 255, 255, 0.3);
120
+ transform: scale(1.1);
121
+ }
122
+
123
+ &:active {
124
+ transform: scale(0.95);
125
+ }
126
+ `;
127
+
128
+ const ToggleButton = styled.button`
129
+ background: none;
130
+ border: none;
131
+ color: #0EA5E9;
132
+ font-size: 13px;
133
+ font-weight: 600;
134
+ cursor: pointer;
135
+ padding: 8px 0;
136
+ text-decoration: underline;
137
+ transition: color 0.2s;
138
+
139
+ &:hover {
140
+ color: #A855F7;
141
+ }
142
+
143
+ &:disabled {
144
+ opacity: 0.5;
145
+ cursor: not-allowed;
146
+ }
147
+ `;
148
+
149
+ const LicenseGuard = ({ children }) => {
150
+ const { get, post } = useFetchClient();
151
+ const { toggleNotification } = useNotification();
152
+ const navigate = useNavigate();
153
+ useAuthRefresh(); // Initialize token auto-refresh
154
+
155
+ const [isChecking, setIsChecking] = useState(true);
156
+ const [needsLicense, setNeedsLicense] = useState(false);
157
+ const [isCreating, setIsCreating] = useState(false);
158
+ const [useExistingKey, setUseExistingKey] = useState(false);
159
+ const [useAutoCreate, setUseAutoCreate] = useState(true);
160
+ const [existingLicenseKey, setExistingLicenseKey] = useState('');
161
+ const [existingEmail, setExistingEmail] = useState('');
162
+ const [adminUser, setAdminUser] = useState(null);
163
+
164
+ useEffect(() => {
165
+ checkLicenseStatus();
166
+ fetchAdminUser();
167
+ }, []);
168
+
169
+ const fetchAdminUser = async () => {
170
+ try {
171
+ const response = await get('/admin/users/me');
172
+ const userData = response.data?.data || response.data;
173
+ if (userData) {
174
+ setAdminUser(userData);
175
+ }
176
+ } catch (error) {
177
+ console.debug('[MagicMail] Could not fetch admin user');
178
+ }
179
+ };
180
+
181
+ const checkLicenseStatus = async () => {
182
+ setIsChecking(true);
183
+ try {
184
+ const response = await get('/magic-mail/license/status');
185
+
186
+ if (response.data.valid) {
187
+ setNeedsLicense(false);
188
+ } else {
189
+ setNeedsLicense(true);
190
+ }
191
+ } catch (error) {
192
+ console.error('[MagicMail] License check error:', error);
193
+ setNeedsLicense(true);
194
+ } finally {
195
+ setIsChecking(false);
196
+ }
197
+ };
198
+
199
+ const handleAutoCreateLicense = async (e) => {
200
+ e.preventDefault();
201
+ setIsCreating(true);
202
+
203
+ try {
204
+ const response = await post('/magic-mail/license/auto-create', {});
205
+
206
+ if (response.data && response.data.success) {
207
+ toggleNotification({
208
+ type: 'success',
209
+ message: '✅ License created! Reloading...',
210
+ });
211
+
212
+ setNeedsLicense(false);
213
+
214
+ setTimeout(() => {
215
+ window.location.reload();
216
+ }, 500);
217
+ } else {
218
+ throw new Error('Failed to auto-create license');
219
+ }
220
+ } catch (error) {
221
+ console.error('[MagicMail] Error:', error);
222
+ toggleNotification({
223
+ type: 'danger',
224
+ message: 'Failed to create license. Try manual entry.',
225
+ });
226
+ setIsCreating(false);
227
+ setUseAutoCreate(false);
228
+ }
229
+ };
230
+
231
+ const handleValidateExistingKey = async (e) => {
232
+ e.preventDefault();
233
+
234
+ if (!existingLicenseKey.trim() || !existingEmail.trim()) {
235
+ toggleNotification({
236
+ type: 'warning',
237
+ message: 'Please enter both license key and email address',
238
+ });
239
+ return;
240
+ }
241
+
242
+ setIsCreating(true);
243
+
244
+ try {
245
+ const response = await post('/magic-mail/license/store-key', {
246
+ licenseKey: existingLicenseKey.trim(),
247
+ email: existingEmail.trim(),
248
+ });
249
+
250
+ if (response.data && response.data.success) {
251
+ toggleNotification({
252
+ type: 'success',
253
+ message: '✅ License activated! Reloading...',
254
+ });
255
+
256
+ setNeedsLicense(false);
257
+
258
+ setTimeout(() => {
259
+ window.location.reload();
260
+ }, 500);
261
+ } else {
262
+ throw new Error('Invalid license');
263
+ }
264
+ } catch (error) {
265
+ toggleNotification({
266
+ type: 'danger',
267
+ message: 'Invalid license key or email address',
268
+ });
269
+ setIsCreating(false);
270
+ }
271
+ };
272
+
273
+ const handleClose = () => {
274
+ navigate('/content-manager');
275
+ };
276
+
277
+ if (isChecking) {
278
+ return (
279
+ <Box padding={8} style={{ textAlign: 'center' }}>
280
+ <Loader>Checking license...</Loader>
281
+ </Box>
282
+ );
283
+ }
284
+
285
+ if (needsLicense) {
286
+ return (
287
+ <ModalOverlay>
288
+ <ModalContent>
289
+ <GradientHeader>
290
+ <CloseButton onClick={handleClose} type="button">
291
+ <XMarkIcon />
292
+ </CloseButton>
293
+ <IconWrapper>
294
+ <KeyIcon />
295
+ </IconWrapper>
296
+ <Box style={{ textAlign: 'center', position: 'relative' }}>
297
+ <Typography
298
+ variant="alpha"
299
+ style={{
300
+ color: 'white',
301
+ fontSize: '24px',
302
+ fontWeight: '700',
303
+ marginBottom: '12px',
304
+ display: 'block',
305
+ }}
306
+ >
307
+ 🔐 Activate MagicMail
308
+ </Typography>
309
+ <Typography
310
+ variant="epsilon"
311
+ style={{
312
+ color: 'rgba(255, 255, 255, 0.9)',
313
+ fontSize: '14px',
314
+ display: 'block',
315
+ }}
316
+ >
317
+ {useExistingKey ? 'Enter your existing license key' : 'Create a license to start using the plugin'}
318
+ </Typography>
319
+ </Box>
320
+ </GradientHeader>
321
+
322
+ <form onSubmit={useExistingKey ? handleValidateExistingKey : handleAutoCreateLicense}>
323
+ <Box padding={6} paddingLeft={8} paddingRight={8}>
324
+ <Flex direction="column" gap={5} style={{ width: '100%' }}>
325
+ <Box style={{ textAlign: 'center', width: '100%' }}>
326
+ <ToggleButton
327
+ type="button"
328
+ onClick={() => setUseExistingKey(!useExistingKey)}
329
+ disabled={isCreating}
330
+ >
331
+ {useExistingKey ? '← Create new license' : 'Have a license key? →'}
332
+ </ToggleButton>
333
+ </Box>
334
+
335
+ <Box
336
+ background="primary100"
337
+ padding={4}
338
+ style={{
339
+ borderRadius: '8px',
340
+ border: '2px solid #BAE6FD',
341
+ width: '100%',
342
+ }}
343
+ >
344
+ <Typography variant="omega" style={{ fontSize: '13px', lineHeight: '1.6' }}>
345
+ {useExistingKey
346
+ ? '🔑 Enter your email and license key to activate.'
347
+ : adminUser && adminUser.email
348
+ ? `✨ Click "Activate" to auto-create a license with your account (${adminUser.email})`
349
+ : '✨ Click "Activate" to auto-create a license with your admin account'
350
+ }
351
+ </Typography>
352
+ </Box>
353
+
354
+ {useExistingKey ? (
355
+ // Existing License Key Input
356
+ <>
357
+ <Box style={{ width: '100%' }}>
358
+ <Typography
359
+ variant="pi"
360
+ fontWeight="bold"
361
+ style={{ marginBottom: '8px', display: 'block' }}
362
+ >
363
+ Email Address *
364
+ </Typography>
365
+ <TextInput
366
+ placeholder="admin@example.com"
367
+ type="email"
368
+ value={existingEmail}
369
+ onChange={(e) => setExistingEmail(e.target.value)}
370
+ required
371
+ disabled={isCreating}
372
+ />
373
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
374
+ Enter the email address associated with this license
375
+ </Typography>
376
+ </Box>
377
+
378
+ <Box style={{ width: '100%' }}>
379
+ <Typography
380
+ variant="pi"
381
+ fontWeight="bold"
382
+ style={{ marginBottom: '8px', display: 'block' }}
383
+ >
384
+ License Key *
385
+ </Typography>
386
+ <TextInput
387
+ placeholder="MAGIC-MAIL-XXXX-XXXX-XXXX"
388
+ value={existingLicenseKey}
389
+ onChange={(e) => setExistingLicenseKey(e.target.value)}
390
+ required
391
+ disabled={isCreating}
392
+ />
393
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
394
+ Enter the license key
395
+ </Typography>
396
+ </Box>
397
+ </>
398
+ ) : adminUser ? (
399
+ // Auto-create mode - Show user info
400
+ <Box
401
+ background="success100"
402
+ padding={5}
403
+ style={{
404
+ borderRadius: '8px',
405
+ border: '2px solid #DCFCE7',
406
+ textAlign: 'center',
407
+ }}
408
+ >
409
+ <Typography variant="omega" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
410
+ Ready to activate with your account:
411
+ </Typography>
412
+ <Typography variant="pi" style={{ marginBottom: '4px', display: 'block' }}>
413
+ 👤 {adminUser.firstname || 'Admin'} {adminUser.lastname || 'User'}
414
+ </Typography>
415
+ <Typography variant="pi" textColor="neutral600">
416
+ 📧 {adminUser.email || 'Loading...'}
417
+ </Typography>
418
+ </Box>
419
+ ) : (
420
+ <Box padding={4} background="neutral100" hasRadius style={{ textAlign: 'center' }}>
421
+ <Loader small />
422
+ <Typography variant="pi" marginTop={2}>Loading admin user data...</Typography>
423
+ </Box>
424
+ )}
425
+
426
+ <Flex gap={3} justifyContent="center" style={{ marginTop: '16px' }}>
427
+ {useExistingKey ? (
428
+ <Button
429
+ type="submit"
430
+ size="L"
431
+ startIcon={<CheckIcon style={{ width: 20, height: 20 }} />}
432
+ loading={isCreating}
433
+ disabled={isCreating || !existingLicenseKey.trim() || !existingEmail.trim()}
434
+ style={{
435
+ background: 'linear-gradient(135deg, #0EA5E9 0%, #A855F7 100%)',
436
+ color: 'white',
437
+ fontWeight: '600',
438
+ border: 'none',
439
+ boxShadow: '0 4px 12px rgba(14, 165, 233, 0.4)',
440
+ }}
441
+ >
442
+ Validate License
443
+ </Button>
444
+ ) : (
445
+ <Button
446
+ type="submit"
447
+ size="L"
448
+ startIcon={<CheckIcon style={{ width: 20, height: 20 }} />}
449
+ loading={isCreating}
450
+ disabled={isCreating || !adminUser}
451
+ style={{
452
+ background: 'linear-gradient(135deg, #0EA5E9 0%, #A855F7 100%)',
453
+ color: 'white',
454
+ fontWeight: '600',
455
+ border: 'none',
456
+ boxShadow: '0 4px 12px rgba(14, 165, 233, 0.4)',
457
+ }}
458
+ >
459
+ Activate License
460
+ </Button>
461
+ )}
462
+ </Flex>
463
+ </Flex>
464
+ </Box>
465
+ </form>
466
+ </ModalContent>
467
+ </ModalOverlay>
468
+ );
469
+ }
470
+
471
+ return <>{children}</>;
472
+ };
473
+
474
+ export default LicenseGuard;
475
+
@@ -0,0 +1,5 @@
1
+ import { EnvelopeIcon } from '@heroicons/react/24/outline';
2
+
3
+ const PluginIcon = () => <EnvelopeIcon style={{ width: 24, height: 24 }} />;
4
+
5
+ export default PluginIcon;
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useFetchClient } from '@strapi/strapi/admin';
3
+
4
+ /**
5
+ * Hook to handle JWT token refresh for admin panel
6
+ * Automatically refreshes token before expiration (every 4 minutes)
7
+ * Also handles 401 responses with automatic token refresh
8
+ */
9
+ export const useAuthRefresh = () => {
10
+ const { get } = useFetchClient();
11
+ const intervalRef = useRef(null);
12
+
13
+ useEffect(() => {
14
+ // Set up auto-refresh every 4 minutes (before 5 min expiration)
15
+ intervalRef.current = setInterval(async () => {
16
+ try {
17
+ // Refresh token by hitting a protected endpoint
18
+ await get('/admin/users/me');
19
+ console.debug('[Auth Refresh] Token refreshed successfully');
20
+ } catch (error) {
21
+ // If refresh fails, log but don't break
22
+ console.debug('[Auth Refresh] Token refresh attempt failed');
23
+ }
24
+ }, 4 * 60 * 1000); // 4 minutes
25
+
26
+ return () => {
27
+ if (intervalRef.current) {
28
+ clearInterval(intervalRef.current);
29
+ }
30
+ };
31
+ }, [get]);
32
+
33
+ return {
34
+ refreshToken: async () => {
35
+ try {
36
+ await get('/admin/users/me');
37
+ return true;
38
+ } catch (error) {
39
+ return false;
40
+ }
41
+ },
42
+ };
43
+ };
44
+
@@ -0,0 +1,158 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useFetchClient } from '@strapi/strapi/admin';
3
+
4
+ /**
5
+ * Hook to check license status for MagicMail
6
+ * Returns: { isPremium, loading, error, licenseData, refetch }
7
+ */
8
+ export const useLicense = () => {
9
+ const { get } = useFetchClient();
10
+ const [isPremium, setIsPremium] = useState(false);
11
+ const [isAdvanced, setIsAdvanced] = useState(false);
12
+ const [isEnterprise, setIsEnterprise] = useState(false);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+ const [licenseData, setLicenseData] = useState(null);
16
+
17
+ useEffect(() => {
18
+ let mounted = true;
19
+
20
+ const fetchLicense = async () => {
21
+ if (mounted) {
22
+ await checkLicense();
23
+ }
24
+ };
25
+
26
+ fetchLicense();
27
+
28
+ // Auto-refresh every 1 hour
29
+ const interval = setInterval(() => {
30
+ if (mounted) {
31
+ checkLicense(true); // Silent refresh
32
+ }
33
+ }, 60 * 60 * 1000);
34
+
35
+ return () => {
36
+ mounted = false;
37
+ clearInterval(interval);
38
+ };
39
+ }, []);
40
+
41
+ const checkLicense = async (silent = false) => {
42
+ if (!silent) {
43
+ setLoading(true);
44
+ }
45
+
46
+ try {
47
+ const response = await get('/magic-mail/license/status');
48
+
49
+ const isValid = response.data?.valid || false;
50
+ const hasPremiumFeature = response.data?.data?.features?.premium || false;
51
+ const hasAdvancedFeature = response.data?.data?.features?.advanced || false;
52
+ const hasEnterpriseFeature = response.data?.data?.features?.enterprise || false;
53
+
54
+ setIsPremium(isValid && hasPremiumFeature);
55
+ setIsAdvanced(isValid && hasAdvancedFeature);
56
+ setIsEnterprise(isValid && hasEnterpriseFeature);
57
+ setLicenseData(response.data?.data || null);
58
+ setError(null);
59
+ } catch (err) {
60
+ // Ignore AbortError (happens on unmount)
61
+ if (err.name === 'AbortError') {
62
+ return;
63
+ }
64
+
65
+ if (!silent) {
66
+ console.error('[MagicMail] License check error:', err);
67
+ }
68
+ setIsPremium(false);
69
+ setIsAdvanced(false);
70
+ setIsEnterprise(false);
71
+ setLicenseData(null);
72
+ setError(err);
73
+ } finally {
74
+ if (!silent) {
75
+ setLoading(false);
76
+ }
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Check if a specific feature is available
82
+ * @param {string} featureName - Name of the feature to check
83
+ * @returns {boolean}
84
+ */
85
+ const hasFeature = (featureName) => {
86
+ if (!featureName) return false;
87
+
88
+ // Free tier features (always available)
89
+ const freeFeatures = [
90
+ 'basic-smtp',
91
+ 'oauth-gmail',
92
+ 'oauth-microsoft',
93
+ 'oauth-yahoo',
94
+ 'basic-routing',
95
+ 'email-logging',
96
+ 'account-testing',
97
+ 'strapi-service-override',
98
+ 'email-designer-basic',
99
+ ];
100
+
101
+ if (freeFeatures.includes(featureName)) return true;
102
+
103
+ // Premium+ features
104
+ const premiumFeatures = [
105
+ 'email-designer-templates',
106
+ ];
107
+
108
+ if (premiumFeatures.includes(featureName) && isPremium) return true;
109
+
110
+ // Advanced+ features
111
+ const advancedFeatures = [
112
+ 'sendgrid',
113
+ 'mailgun',
114
+ 'dkim-signing',
115
+ 'priority-headers',
116
+ 'list-unsubscribe',
117
+ 'security-validation',
118
+ 'analytics-dashboard',
119
+ 'advanced-routing',
120
+ 'email-designer-versioning',
121
+ 'email-designer-import-export',
122
+ ];
123
+
124
+ if (advancedFeatures.includes(featureName) && isAdvanced) return true;
125
+
126
+ // Enterprise features
127
+ const enterpriseFeatures = [
128
+ 'multi-tenant',
129
+ 'compliance-reports',
130
+ 'custom-security-rules',
131
+ 'priority-support',
132
+ 'email-designer-custom-blocks',
133
+ 'email-designer-team-library',
134
+ 'email-designer-a-b-testing',
135
+ ];
136
+
137
+ if (enterpriseFeatures.includes(featureName) && isEnterprise) return true;
138
+
139
+ return false;
140
+ };
141
+
142
+ return {
143
+ isPremium,
144
+ isAdvanced,
145
+ isEnterprise,
146
+ loading,
147
+ error,
148
+ licenseData,
149
+ features: {
150
+ premium: isPremium,
151
+ advanced: isAdvanced,
152
+ enterprise: isEnterprise,
153
+ },
154
+ hasFeature,
155
+ refetch: checkLicense
156
+ };
157
+ };
158
+