strapi-plugin-magic-mail 2.2.3 → 2.2.5

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 (70) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/AddAccountModal.jsx +0 -1943
  6. package/admin/src/components/Initializer.jsx +0 -14
  7. package/admin/src/components/LicenseGuard.jsx +0 -475
  8. package/admin/src/components/PluginIcon.jsx +0 -5
  9. package/admin/src/hooks/useAuthRefresh.js +0 -44
  10. package/admin/src/hooks/useLicense.js +0 -158
  11. package/admin/src/index.js +0 -87
  12. package/admin/src/pages/Analytics.jsx +0 -762
  13. package/admin/src/pages/App.jsx +0 -111
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  16. package/admin/src/pages/HomePage.jsx +0 -1170
  17. package/admin/src/pages/LicensePage.jsx +0 -430
  18. package/admin/src/pages/RoutingRules.jsx +0 -1141
  19. package/admin/src/pages/Settings.jsx +0 -603
  20. package/admin/src/pluginId.js +0 -3
  21. package/admin/src/translations/de.json +0 -71
  22. package/admin/src/translations/en.json +0 -70
  23. package/admin/src/translations/es.json +0 -71
  24. package/admin/src/translations/fr.json +0 -71
  25. package/admin/src/translations/pt.json +0 -71
  26. package/admin/src/utils/fetchWithRetry.js +0 -123
  27. package/admin/src/utils/getTranslation.js +0 -5
  28. package/admin/src/utils/theme.js +0 -85
  29. package/server/jsconfig.json +0 -10
  30. package/server/src/bootstrap.js +0 -157
  31. package/server/src/config/features.js +0 -260
  32. package/server/src/config/index.js +0 -9
  33. package/server/src/content-types/email-account/schema.json +0 -93
  34. package/server/src/content-types/email-event/index.js +0 -8
  35. package/server/src/content-types/email-event/schema.json +0 -57
  36. package/server/src/content-types/email-link/index.js +0 -8
  37. package/server/src/content-types/email-link/schema.json +0 -49
  38. package/server/src/content-types/email-log/index.js +0 -8
  39. package/server/src/content-types/email-log/schema.json +0 -106
  40. package/server/src/content-types/email-template/schema.json +0 -74
  41. package/server/src/content-types/email-template-version/schema.json +0 -60
  42. package/server/src/content-types/index.js +0 -33
  43. package/server/src/content-types/routing-rule/schema.json +0 -59
  44. package/server/src/controllers/accounts.js +0 -229
  45. package/server/src/controllers/analytics.js +0 -361
  46. package/server/src/controllers/controller.js +0 -26
  47. package/server/src/controllers/email-designer.js +0 -474
  48. package/server/src/controllers/index.js +0 -21
  49. package/server/src/controllers/license.js +0 -269
  50. package/server/src/controllers/oauth.js +0 -474
  51. package/server/src/controllers/routing-rules.js +0 -129
  52. package/server/src/controllers/test.js +0 -301
  53. package/server/src/destroy.js +0 -27
  54. package/server/src/index.js +0 -25
  55. package/server/src/middlewares/index.js +0 -3
  56. package/server/src/policies/index.js +0 -3
  57. package/server/src/register.js +0 -5
  58. package/server/src/routes/admin.js +0 -469
  59. package/server/src/routes/content-api.js +0 -37
  60. package/server/src/routes/index.js +0 -9
  61. package/server/src/services/account-manager.js +0 -329
  62. package/server/src/services/analytics.js +0 -512
  63. package/server/src/services/email-designer.js +0 -717
  64. package/server/src/services/email-router.js +0 -1446
  65. package/server/src/services/index.js +0 -17
  66. package/server/src/services/license-guard.js +0 -423
  67. package/server/src/services/oauth.js +0 -515
  68. package/server/src/services/service.js +0 -7
  69. package/server/src/utils/encryption.js +0 -81
  70. package/server/src/utils/logger.js +0 -84
@@ -1,1943 +0,0 @@
1
- import React, { useState } from 'react';
2
- import styled, { keyframes, css } from 'styled-components';
3
- import {
4
- Modal,
5
- Typography,
6
- Box,
7
- Flex,
8
- Button,
9
- Field,
10
- TextInput,
11
- Textarea,
12
- NumberInput,
13
- Toggle,
14
- Alert,
15
- Divider,
16
- Badge,
17
- } from '@strapi/design-system';
18
- import { useFetchClient, useNotification } from '@strapi/strapi/admin';
19
- import { Mail, Server, Key, ArrowRight, ArrowLeft, Check, Lock, Cloud, Cog, Star } from '@strapi/icons';
20
-
21
- // ============= ANIMATIONS =============
22
- const fadeIn = keyframes`
23
- from {
24
- opacity: 0;
25
- transform: translateY(10px);
26
- }
27
- to {
28
- opacity: 1;
29
- transform: translateY(0);
30
- }
31
- `;
32
-
33
- const pulse = keyframes`
34
- 0%, 100% {
35
- transform: scale(1);
36
- }
37
- 50% {
38
- transform: scale(1.05);
39
- }
40
- `;
41
-
42
- const slideIn = keyframes`
43
- from {
44
- transform: translateX(-20px);
45
- opacity: 0;
46
- }
47
- to {
48
- transform: translateX(0);
49
- opacity: 1;
50
- }
51
- `;
52
-
53
- // ============= COLORS =============
54
- const colors = {
55
- primary: '#4945ff', // Strapi Primary Blue
56
- primaryLight: '#f0f0ff', // Light Blue Background
57
- success: '#5cb176', // Green for completed
58
- successLight: '#eafaf1', // Light green background
59
- neutral: '#8e8ea9', // Gray
60
- neutralLight: '#f6f6f9', // Light gray
61
- white: '#ffffff',
62
- border: '#dcdce4',
63
- text: '#32324d',
64
- textLight: '#666687',
65
- };
66
-
67
- // ============= STYLED COMPONENTS =============
68
- const StepHeader = styled(Box)`
69
- padding-bottom: 24px;
70
- margin-bottom: 32px;
71
- position: relative;
72
- animation: ${fadeIn} 0.4s ease;
73
-
74
- &::after {
75
- content: '';
76
- position: absolute;
77
- bottom: 0;
78
- left: -24px;
79
- right: -24px;
80
- height: 1px;
81
- background: linear-gradient(90deg, transparent, ${colors.border}, transparent);
82
- }
83
- `;
84
-
85
- const StepTitle = styled(Typography)`
86
- color: ${colors.text};
87
- font-size: 24px;
88
- font-weight: 600;
89
- margin-bottom: 8px;
90
- display: flex;
91
- align-items: center;
92
- gap: 12px;
93
- `;
94
-
95
- const StepSubtitle = styled(Typography)`
96
- color: ${colors.textLight};
97
- font-size: 14px;
98
- line-height: 1.5;
99
- `;
100
-
101
- const StepperContainer = styled(Box)`
102
- display: flex;
103
- align-items: flex-start;
104
- justify-content: center;
105
- gap: 0;
106
- margin-bottom: 48px;
107
- margin-top: 8px;
108
- position: relative;
109
- padding: 0 40px;
110
- `;
111
-
112
- const StepWrapper = styled.div`
113
- flex: 1;
114
- display: flex;
115
- flex-direction: column;
116
- align-items: center;
117
- position: relative;
118
-
119
- &:not(:last-child)::after {
120
- content: '';
121
- position: absolute;
122
- top: 28px;
123
- left: 50%;
124
- width: 100%;
125
- height: 3px;
126
- background: ${props => props.$completed ? colors.success : colors.neutralLight};
127
- transition: all 0.4s ease;
128
- z-index: 0;
129
- }
130
- `;
131
-
132
- const StepDot = styled.div`
133
- width: 56px;
134
- height: 56px;
135
- border-radius: 50%;
136
- background: ${props =>
137
- props.$active ? colors.primary :
138
- props.$completed ? colors.success :
139
- colors.white
140
- };
141
- color: ${props =>
142
- props.$active || props.$completed ? colors.white : colors.textLight
143
- };
144
- border: 4px solid ${props =>
145
- props.$active ? colors.primary :
146
- props.$completed ? colors.success :
147
- colors.border
148
- };
149
- display: flex;
150
- align-items: center;
151
- justify-content: center;
152
- font-weight: 700;
153
- font-size: 18px;
154
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
155
- position: relative;
156
- z-index: 1;
157
- cursor: ${props => props.$completed ? 'pointer' : 'default'};
158
- box-shadow: ${props =>
159
- props.$active ? `0 4px 16px ${colors.primary}40, 0 0 0 8px ${colors.primaryLight}` :
160
- props.$completed ? `0 4px 12px ${colors.success}30` :
161
- '0 2px 8px rgba(0,0,0,0.08)'
162
- };
163
-
164
- ${props => props.$active && css`
165
- animation: ${pulse} 2s infinite;
166
- `}
167
-
168
- &:hover {
169
- transform: ${props => props.$completed ? 'scale(1.1)' : props.$active ? 'scale(1.05)' : 'scale(1)'};
170
- }
171
- `;
172
-
173
- const StepLabel = styled(Typography)`
174
- margin-top: 12px;
175
- font-size: 13px;
176
- color: ${props => props.$active ? colors.primary : props.$completed ? colors.success : colors.textLight};
177
- white-space: nowrap;
178
- font-weight: ${props => props.$active ? 600 : 500};
179
- text-align: center;
180
- transition: all 0.3s ease;
181
- `;
182
-
183
- const ProvidersGrid = styled(Box)`
184
- display: grid;
185
- grid-template-columns: repeat(2, 1fr);
186
- gap: 20px;
187
- margin-bottom: 24px;
188
- animation: ${slideIn} 0.5s ease;
189
- max-width: 800px;
190
- margin-left: auto;
191
- margin-right: auto;
192
- `;
193
-
194
- const ProviderCard = styled(Box)`
195
- background: ${props => props.$selected ? colors.successLight : colors.white};
196
- border: 2px solid ${props => props.$selected ? colors.success : colors.border};
197
- border-radius: 12px;
198
- padding: 24px;
199
- cursor: pointer;
200
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
201
- text-align: center;
202
- aspect-ratio: 1;
203
- min-height: 180px;
204
- display: flex;
205
- flex-direction: column;
206
- align-items: center;
207
- justify-content: center;
208
- gap: 12px;
209
- position: relative;
210
- overflow: hidden;
211
-
212
- &::before {
213
- content: '';
214
- position: absolute;
215
- top: 0;
216
- left: 0;
217
- right: 0;
218
- bottom: 0;
219
- background: linear-gradient(135deg, transparent, rgba(73, 69, 255, 0.05));
220
- opacity: 0;
221
- transition: opacity 0.3s;
222
- }
223
-
224
- &:hover {
225
- transform: translateY(-4px);
226
- box-shadow: 0 8px 24px rgba(73, 69, 255, 0.12);
227
- border-color: ${props => props.$selected ? colors.success : colors.primary};
228
-
229
- &::before {
230
- opacity: 1;
231
- }
232
- }
233
-
234
- ${props => props.$selected && `
235
- &::after {
236
- content: '✓';
237
- position: absolute;
238
- top: 8px;
239
- right: 8px;
240
- width: 24px;
241
- height: 24px;
242
- background: ${colors.success};
243
- color: white;
244
- border-radius: 50%;
245
- display: flex;
246
- align-items: center;
247
- justify-content: center;
248
- font-size: 14px;
249
- font-weight: bold;
250
- }
251
- `}
252
- `;
253
-
254
- const ProviderIcon = styled.div`
255
- width: 56px;
256
- height: 56px;
257
- border-radius: ${props => props.$round ? '50%' : '12px'};
258
- background: ${props => props.$bgColor || colors.primaryLight};
259
- display: flex;
260
- align-items: center;
261
- justify-content: center;
262
- font-size: ${props => props.$fontSize || '24px'};
263
- font-weight: bold;
264
- color: ${props => props.$color || colors.primary};
265
- box-shadow: 0 4px 12px ${props => props.$shadowColor || 'rgba(73, 69, 255, 0.15)'};
266
- `;
267
-
268
- const ProviderName = styled(Typography)`
269
- font-weight: 600;
270
- font-size: 15px;
271
- color: ${colors.text};
272
- margin: 0;
273
- `;
274
-
275
- const ProviderTagline = styled(Typography)`
276
- font-size: 12px;
277
- color: ${colors.textLight};
278
- margin: 0;
279
- `;
280
-
281
- const InfoAlert = styled(Alert)`
282
- background: ${colors.primaryLight};
283
- border: 1px solid ${colors.primary}33;
284
- animation: ${fadeIn} 0.4s ease;
285
-
286
- svg {
287
- color: ${colors.primary};
288
- }
289
- `;
290
-
291
- const FormSection = styled(Box)`
292
- animation: ${slideIn} 0.4s ease;
293
- width: 100%;
294
- max-width: 100%;
295
- `;
296
-
297
- const FullWidthField = styled(Box)`
298
- width: 100%;
299
-
300
- & > * {
301
- width: 100%;
302
- }
303
-
304
- input, textarea {
305
- width: 100% !important;
306
- }
307
- `;
308
-
309
- const SectionTitle = styled(Typography)`
310
- color: ${colors.text};
311
- font-weight: 600;
312
- font-size: 16px;
313
- margin-bottom: 16px;
314
- display: flex;
315
- align-items: center;
316
- gap: 8px;
317
- `;
318
-
319
- const PrimaryToggleBox = styled(Box)`
320
- background: linear-gradient(135deg, ${colors.primaryLight}, ${colors.successLight});
321
- border: 2px solid ${colors.primary}33;
322
- border-radius: 12px;
323
- padding: 20px;
324
- transition: all 0.3s;
325
-
326
- &:hover {
327
- border-color: ${colors.primary}66;
328
- box-shadow: 0 4px 12px rgba(73, 69, 255, 0.1);
329
- }
330
- `;
331
-
332
- // ============= COMPONENT =============
333
- const AddAccountModal = ({ isOpen, onClose, onAccountAdded, editAccount = null }) => {
334
- const { post, get, put } = useFetchClient();
335
- const { toggleNotification } = useNotification();
336
- const [loading, setLoading] = useState(false);
337
- const [currentStep, setCurrentStep] = useState(1);
338
- const [provider, setProvider] = useState('');
339
- const [oauthCode, setOauthCode] = useState(null);
340
- const [oauthState, setOauthState] = useState(null);
341
- const isEditMode = !!editAccount;
342
-
343
- const [formData, setFormData] = useState({
344
- name: '',
345
- description: '',
346
- fromEmail: '',
347
- fromName: '',
348
- replyTo: '',
349
- isActive: true,
350
- isPrimary: false,
351
- priority: 5,
352
- dailyLimit: 500,
353
- hourlyLimit: 50,
354
- host: '',
355
- port: 587,
356
- user: '',
357
- pass: '',
358
- secure: false,
359
- apiKey: '',
360
- oauthClientId: '',
361
- oauthClientSecret: '',
362
- });
363
-
364
- const handleChange = (field, value) => {
365
- setFormData(prev => ({ ...prev, [field]: value }));
366
- };
367
-
368
- // Populate form when editing - fetch decrypted data
369
- React.useEffect(() => {
370
- const loadAccountData = async () => {
371
- if (isEditMode && editAccount && isOpen) {
372
- try {
373
- // Fetch account with decrypted config
374
- const { data } = await get(`/magic-mail/accounts/${editAccount.id}`);
375
- const accountData = data.data;
376
-
377
- setProvider(accountData.provider);
378
- setCurrentStep(2); // Skip provider selection in edit mode
379
- setFormData({
380
- name: accountData.name || '',
381
- description: accountData.description || '',
382
- fromEmail: accountData.fromEmail || '',
383
- fromName: accountData.fromName || '',
384
- replyTo: accountData.replyTo || '',
385
- isActive: accountData.isActive !== undefined ? accountData.isActive : true,
386
- isPrimary: accountData.isPrimary || false,
387
- priority: accountData.priority || 5,
388
- dailyLimit: accountData.dailyLimit || 500,
389
- hourlyLimit: accountData.hourlyLimit || 50,
390
- host: accountData.config?.host || '',
391
- port: accountData.config?.port || 587,
392
- user: accountData.config?.user || '',
393
- pass: accountData.config?.pass || '', // Now populated from decrypted data
394
- secure: accountData.config?.secure || false,
395
- apiKey: accountData.config?.apiKey || '', // Now populated from decrypted data
396
- mailgunDomain: accountData.config?.domain || '',
397
- microsoftTenantId: accountData.config?.tenantId || '',
398
- oauthClientId: accountData.config?.clientId || '',
399
- oauthClientSecret: accountData.config?.clientSecret || '', // Now populated from decrypted data
400
- });
401
- } catch (err) {
402
- console.error('[magic-mail] Error loading account:', err);
403
- toggleNotification({
404
- type: 'danger',
405
- message: 'Failed to load account data',
406
- });
407
- }
408
- } else if (!isEditMode) {
409
- setCurrentStep(1);
410
- setProvider('');
411
- setFormData({
412
- name: '',
413
- description: '',
414
- fromEmail: '',
415
- fromName: '',
416
- replyTo: '',
417
- isActive: true,
418
- isPrimary: false,
419
- priority: 5,
420
- dailyLimit: 500,
421
- hourlyLimit: 50,
422
- host: '',
423
- port: 587,
424
- user: '',
425
- pass: '',
426
- secure: false,
427
- apiKey: '',
428
- mailgunDomain: '',
429
- microsoftTenantId: '',
430
- oauthClientId: '',
431
- oauthClientSecret: '',
432
- });
433
- }
434
- };
435
-
436
- loadAccountData();
437
- }, [isEditMode, editAccount, isOpen]);
438
-
439
- // Check for OAuth callback parameters (from URL or postMessage)
440
- React.useEffect(() => {
441
- // Check URL params (fallback)
442
- const urlParams = new URLSearchParams(window.location.search);
443
- const code = urlParams.get('oauth_code');
444
- const state = urlParams.get('oauth_state');
445
-
446
- if (code && state) {
447
- setOauthCode(code);
448
- setOauthState(state);
449
-
450
- // Clean URL
451
- window.history.replaceState({}, document.title, window.location.pathname);
452
-
453
- toggleNotification({
454
- type: 'success',
455
- message: '✅ Gmail OAuth authorized! Please complete the account setup.',
456
- });
457
- }
458
-
459
- // Listen for postMessage from OAuth popup
460
- const handleMessage = (event) => {
461
- if (event.origin !== window.location.origin) return;
462
-
463
- if (event.data.type === 'gmail-oauth-success') {
464
- setOauthCode(event.data.code);
465
- setOauthState(event.data.state);
466
-
467
- toggleNotification({
468
- type: 'success',
469
- message: '✅ Gmail OAuth authorized! Please complete the account setup.',
470
- });
471
- }
472
-
473
- if (event.data.type === 'microsoft-oauth-success') {
474
- setOauthCode(event.data.code);
475
- setOauthState(event.data.state);
476
-
477
- toggleNotification({
478
- type: 'success',
479
- message: '✅ Microsoft OAuth authorized! Please complete the account setup.',
480
- });
481
- }
482
-
483
- if (event.data.type === 'yahoo-oauth-success') {
484
- setOauthCode(event.data.code);
485
- setOauthState(event.data.state);
486
-
487
- toggleNotification({
488
- type: 'success',
489
- message: '✅ Yahoo Mail OAuth authorized! Please complete the account setup.',
490
- });
491
- }
492
- };
493
-
494
- window.addEventListener('message', handleMessage);
495
-
496
- return () => {
497
- window.removeEventListener('message', handleMessage);
498
- };
499
- }, []);
500
-
501
- const startGmailOAuth = async () => {
502
- if (!formData.oauthClientId) {
503
- toggleNotification({
504
- type: 'warning',
505
- message: 'Please enter your OAuth Client ID first',
506
- });
507
- return;
508
- }
509
-
510
- try {
511
- const { data } = await get(`/magic-mail/oauth/gmail/auth?clientId=${encodeURIComponent(formData.oauthClientId)}`);
512
-
513
- if (data.authUrl) {
514
- // Open OAuth in popup window
515
- const width = 600;
516
- const height = 700;
517
- const left = (window.screen.width - width) / 2;
518
- const top = (window.screen.height - height) / 2;
519
-
520
- const popup = window.open(
521
- data.authUrl,
522
- 'gmail-oauth',
523
- `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
524
- );
525
-
526
- // Listen for OAuth callback
527
- const checkPopup = setInterval(() => {
528
- try {
529
- if (popup.closed) {
530
- clearInterval(checkPopup);
531
-
532
- // Check if we got OAuth code from URL
533
- const urlParams = new URLSearchParams(window.location.search);
534
- const code = urlParams.get('oauth_code');
535
-
536
- if (!code) {
537
- toggleNotification({
538
- type: 'info',
539
- message: 'OAuth window closed. Please try again if not completed.',
540
- });
541
- }
542
- }
543
- } catch (err) {
544
- // Cross-origin error is expected
545
- }
546
- }, 500);
547
-
548
- toggleNotification({
549
- type: 'info',
550
- message: '🔐 Please authorize in the popup window...',
551
- });
552
- }
553
- } catch (err) {
554
- toggleNotification({
555
- type: 'danger',
556
- message: 'Failed to start OAuth flow',
557
- });
558
- }
559
- };
560
-
561
- const startMicrosoftOAuth = async () => {
562
- if (!formData.microsoftTenantId) {
563
- toggleNotification({
564
- type: 'warning',
565
- message: 'Please enter your Tenant (Directory) ID first',
566
- });
567
- return;
568
- }
569
-
570
- if (!formData.oauthClientId) {
571
- toggleNotification({
572
- type: 'warning',
573
- message: 'Please enter your Application (Client) ID',
574
- });
575
- return;
576
- }
577
-
578
- try {
579
- const { data } = await get(`/magic-mail/oauth/microsoft/auth?clientId=${encodeURIComponent(formData.oauthClientId)}&tenantId=${encodeURIComponent(formData.microsoftTenantId)}`);
580
-
581
- if (data.authUrl) {
582
- // Open OAuth in popup window
583
- const width = 600;
584
- const height = 700;
585
- const left = (window.screen.width - width) / 2;
586
- const top = (window.screen.height - height) / 2;
587
-
588
- const popup = window.open(
589
- data.authUrl,
590
- 'microsoft-oauth',
591
- `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
592
- );
593
-
594
- // Listen for OAuth callback
595
- const checkPopup = setInterval(() => {
596
- try {
597
- if (popup.closed) {
598
- clearInterval(checkPopup);
599
-
600
- // Check if we got OAuth code from URL
601
- const urlParams = new URLSearchParams(window.location.search);
602
- const code = urlParams.get('oauth_code');
603
-
604
- if (!code) {
605
- toggleNotification({
606
- type: 'info',
607
- message: 'OAuth window closed. Please try again if not completed.',
608
- });
609
- }
610
- }
611
- } catch (err) {
612
- // Cross-origin error is expected
613
- }
614
- }, 500);
615
-
616
- toggleNotification({
617
- type: 'info',
618
- message: '🔐 Please authorize in the popup window...',
619
- });
620
- }
621
- } catch (err) {
622
- toggleNotification({
623
- type: 'danger',
624
- message: 'Failed to start Microsoft OAuth flow',
625
- });
626
- }
627
- };
628
-
629
- const startYahooOAuth = async () => {
630
- if (!formData.oauthClientId) {
631
- toggleNotification({
632
- type: 'warning',
633
- message: 'Please enter your Yahoo Client ID first',
634
- });
635
- return;
636
- }
637
-
638
- try {
639
- const { data } = await get(`/magic-mail/oauth/yahoo/auth?clientId=${encodeURIComponent(formData.oauthClientId)}`);
640
-
641
- if (data.authUrl) {
642
- // Open OAuth in popup window
643
- const width = 600;
644
- const height = 700;
645
- const left = (window.screen.width - width) / 2;
646
- const top = (window.screen.height - height) / 2;
647
-
648
- const popup = window.open(
649
- data.authUrl,
650
- 'yahoo-oauth',
651
- `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
652
- );
653
-
654
- // Listen for OAuth callback
655
- const checkPopup = setInterval(() => {
656
- try {
657
- if (popup.closed) {
658
- clearInterval(checkPopup);
659
-
660
- // Check if we got OAuth code from URL
661
- const urlParams = new URLSearchParams(window.location.search);
662
- const code = urlParams.get('oauth_code');
663
-
664
- if (!code) {
665
- toggleNotification({
666
- type: 'info',
667
- message: 'OAuth window closed. Please try again if not completed.',
668
- });
669
- }
670
- }
671
- } catch (err) {
672
- // Cross-origin error is expected
673
- }
674
- }, 500);
675
-
676
- toggleNotification({
677
- type: 'info',
678
- message: '🔐 Please authorize in the popup window...',
679
- });
680
- }
681
- } catch (err) {
682
- toggleNotification({
683
- type: 'danger',
684
- message: 'Failed to start Yahoo OAuth flow',
685
- });
686
- }
687
- };
688
-
689
- const canProceed = () => {
690
- if (currentStep === 1) return provider !== '';
691
- if (currentStep === 2) {
692
- // For OAuth providers, fromEmail comes from provider, so only name is required
693
- if (provider === 'gmail-oauth' || provider === 'microsoft-oauth' || provider === 'yahoo-oauth') return formData.name;
694
- return formData.name && formData.fromEmail;
695
- }
696
- if (currentStep === 3) {
697
- if (provider === 'smtp') {
698
- // Password is required (we show the decrypted one in edit mode)
699
- return formData.host && formData.user && formData.pass;
700
- }
701
- if (provider === 'gmail-oauth' || provider === 'yahoo-oauth') {
702
- // In edit mode with existing credentials, they're pre-filled
703
- if (isEditMode && formData.oauthClientId && formData.oauthClientSecret) {
704
- return true;
705
- }
706
- // Only allow proceed if successfully connected (has oauthCode)
707
- return !!oauthCode;
708
- }
709
- if (provider === 'microsoft-oauth') {
710
- // In edit mode with existing credentials, they're pre-filled
711
- if (isEditMode && formData.oauthClientId && formData.oauthClientSecret && formData.microsoftTenantId) {
712
- return true;
713
- }
714
- // Only allow proceed if successfully connected (has oauthCode)
715
- return !!oauthCode;
716
- }
717
- if (provider === 'sendgrid') {
718
- // API key is required
719
- return !!formData.apiKey;
720
- }
721
- if (provider === 'mailgun') {
722
- // API key AND domain are required
723
- return !!formData.apiKey && !!formData.mailgunDomain;
724
- }
725
- }
726
- return true;
727
- };
728
-
729
- const handleSubmit = async () => {
730
- setLoading(true);
731
- try {
732
- let config = {};
733
-
734
- // OAuth providers with full OAuth flow (only for creation, not edit)
735
- if (!isEditMode && (provider === 'gmail-oauth' || provider === 'microsoft-oauth' || provider === 'yahoo-oauth') && oauthCode && oauthState) {
736
- // Use OAuth endpoint to exchange code for tokens and create account
737
- const accountDetails = {
738
- name: formData.name,
739
- description: formData.description,
740
- fromEmail: 'oauth@placeholder.com', // Will be replaced by provider email
741
- fromName: formData.fromName,
742
- replyTo: formData.replyTo,
743
- isPrimary: formData.isPrimary,
744
- priority: formData.priority,
745
- dailyLimit: formData.dailyLimit,
746
- hourlyLimit: formData.hourlyLimit,
747
- config: provider === 'microsoft-oauth' ? {
748
- clientId: formData.oauthClientId,
749
- clientSecret: formData.oauthClientSecret,
750
- tenantId: formData.microsoftTenantId,
751
- } : {
752
- clientId: formData.oauthClientId,
753
- clientSecret: formData.oauthClientSecret,
754
- },
755
- };
756
-
757
- const providerMap = {
758
- 'gmail-oauth': 'gmail',
759
- 'microsoft-oauth': 'microsoft',
760
- 'yahoo-oauth': 'yahoo',
761
- };
762
-
763
- await post('/magic-mail/oauth/create-account', {
764
- provider: providerMap[provider],
765
- code: oauthCode,
766
- state: oauthState,
767
- accountDetails,
768
- });
769
-
770
- const providerNames = {
771
- 'gmail-oauth': 'Gmail',
772
- 'microsoft-oauth': 'Microsoft',
773
- 'yahoo-oauth': 'Yahoo Mail',
774
- };
775
-
776
- toggleNotification({
777
- type: 'success',
778
- message: `✅ ${formData.name} created successfully with ${providerNames[provider]} OAuth!`,
779
- });
780
-
781
- onAccountAdded();
782
- onClose();
783
- setCurrentStep(1);
784
- setOauthCode(null);
785
- setOauthState(null);
786
- return;
787
- }
788
-
789
- // Prepare config based on provider
790
- if (provider === 'smtp') {
791
- config = {
792
- host: formData.host,
793
- port: formData.port,
794
- user: formData.user,
795
- pass: formData.pass, // Now always available (decrypted in edit mode)
796
- secure: formData.secure,
797
- };
798
- } else if (provider === 'sendgrid') {
799
- config = {
800
- apiKey: formData.apiKey // Now always available (decrypted in edit mode)
801
- };
802
- } else if (provider === 'mailgun') {
803
- config = {
804
- apiKey: formData.apiKey, // Now always available (decrypted in edit mode)
805
- domain: formData.mailgunDomain,
806
- };
807
- } else if (provider === 'gmail-oauth' || provider === 'yahoo-oauth') {
808
- config = {
809
- clientId: formData.oauthClientId,
810
- clientSecret: formData.oauthClientSecret, // Now always available (decrypted in edit mode)
811
- };
812
- } else if (provider === 'microsoft-oauth') {
813
- config = {
814
- clientId: formData.oauthClientId,
815
- clientSecret: formData.oauthClientSecret, // Now always available (decrypted in edit mode)
816
- tenantId: formData.microsoftTenantId,
817
- };
818
- }
819
-
820
- const payload = {
821
- name: formData.name,
822
- description: formData.description,
823
- provider,
824
- config,
825
- fromEmail: formData.fromEmail,
826
- fromName: formData.fromName,
827
- replyTo: formData.replyTo,
828
- isActive: formData.isActive,
829
- isPrimary: formData.isPrimary,
830
- priority: formData.priority,
831
- dailyLimit: formData.dailyLimit,
832
- hourlyLimit: formData.hourlyLimit,
833
- };
834
-
835
- if (isEditMode) {
836
- // Update existing account
837
- await put(`/magic-mail/accounts/${editAccount.id}`, payload);
838
- toggleNotification({
839
- type: 'success',
840
- message: `✅ ${formData.name} updated successfully!`,
841
- });
842
- } else {
843
- // Create new account
844
- await post('/magic-mail/accounts', payload);
845
- toggleNotification({
846
- type: 'success',
847
- message: `✅ ${formData.name} created successfully!`,
848
- });
849
- }
850
-
851
- onAccountAdded();
852
- onClose();
853
- setCurrentStep(1);
854
- } catch (err) {
855
- toggleNotification({
856
- type: 'danger',
857
- message: err.response?.data?.error?.message || `Failed to ${isEditMode ? 'update' : 'create'} account`,
858
- });
859
- } finally {
860
- setLoading(false);
861
- }
862
- };
863
-
864
- const getProviderLabel = () => {
865
- switch(provider) {
866
- case 'gmail-oauth': return 'Gmail OAuth';
867
- case 'microsoft-oauth': return 'Microsoft OAuth';
868
- case 'yahoo-oauth': return 'Yahoo Mail OAuth';
869
- case 'smtp': return 'SMTP';
870
- case 'sendgrid': return 'SendGrid';
871
- case 'mailgun': return 'Mailgun';
872
- default: return '';
873
- }
874
- };
875
-
876
- const stepTitles = ['Provider', 'Details', 'Credentials', 'Settings'];
877
-
878
- return (
879
- <Modal.Root open={isOpen} onOpenChange={onClose}>
880
- <Modal.Content size="XL">
881
- <Modal.Header>
882
- <Typography variant="beta">
883
- <Star style={{ marginRight: 8 }} />
884
- {isEditMode ? 'Edit Email Account' : 'Add Email Account'}
885
- </Typography>
886
- </Modal.Header>
887
-
888
- <Modal.Body>
889
- <Flex direction="column" gap={0}>
890
-
891
- {/* Header */}
892
- <StepHeader>
893
- <StepTitle>
894
- {currentStep === 1 && <Mail />}
895
- {currentStep === 2 && <Server />}
896
- {currentStep === 3 && <Lock />}
897
- {currentStep === 4 && <Cog />}
898
- {currentStep === 1 && 'Choose Email Provider'}
899
- {currentStep === 2 && 'Account Details'}
900
- {currentStep === 3 && 'Authentication'}
901
- {currentStep === 4 && 'Configuration'}
902
- </StepTitle>
903
- <StepSubtitle>
904
- {currentStep === 1 && 'Select your preferred email service provider'}
905
- {currentStep === 2 && 'Configure how emails will appear to recipients'}
906
- {currentStep === 3 && `Enter your ${getProviderLabel()} credentials securely`}
907
- {currentStep === 4 && 'Set rate limits and priority for this account'}
908
- </StepSubtitle>
909
- </StepHeader>
910
-
911
- {/* Stepper */}
912
- <StepperContainer>
913
- {[1, 2, 3, 4].map((step) => (
914
- <StepWrapper
915
- key={step}
916
- $completed={currentStep > step}
917
- >
918
- <StepDot
919
- $active={currentStep === step}
920
- $completed={currentStep > step}
921
- onClick={() => currentStep > step && setCurrentStep(step)}
922
- >
923
- {currentStep > step ? <Check /> : step}
924
- </StepDot>
925
- <StepLabel
926
- $active={currentStep === step}
927
- $completed={currentStep > step}
928
- >
929
- {stepTitles[step - 1]}
930
- </StepLabel>
931
- </StepWrapper>
932
- ))}
933
- </StepperContainer>
934
-
935
- {/* Step 1: Choose Provider */}
936
- {currentStep === 1 && (
937
- <Box>
938
- <ProvidersGrid>
939
- <ProviderCard
940
- $selected={provider === 'gmail-oauth'}
941
- onClick={() => setProvider('gmail-oauth')}
942
- >
943
- <ProviderIcon
944
- $round
945
- $bgColor="#4285F433"
946
- $color="#4285F4"
947
- $shadowColor="rgba(66, 133, 244, 0.2)"
948
- >
949
- G
950
- </ProviderIcon>
951
- <ProviderName>Gmail OAuth</ProviderName>
952
- </ProviderCard>
953
-
954
- <ProviderCard
955
- $selected={provider === 'microsoft-oauth'}
956
- onClick={() => setProvider('microsoft-oauth')}
957
- >
958
- <ProviderIcon
959
- $round
960
- $bgColor="#00A4EF33"
961
- $color="#00A4EF"
962
- $shadowColor="rgba(0, 164, 239, 0.2)"
963
- >
964
- M
965
- </ProviderIcon>
966
- <ProviderName>Microsoft OAuth</ProviderName>
967
- </ProviderCard>
968
-
969
- <ProviderCard
970
- $selected={provider === 'yahoo-oauth'}
971
- onClick={() => setProvider('yahoo-oauth')}
972
- >
973
- <ProviderIcon
974
- $round
975
- $bgColor="#6001D233"
976
- $color="#6001D2"
977
- $shadowColor="rgba(96, 1, 210, 0.2)"
978
- >
979
- Y
980
- </ProviderIcon>
981
- <ProviderName>Yahoo Mail OAuth</ProviderName>
982
- </ProviderCard>
983
-
984
- <ProviderCard
985
- $selected={provider === 'smtp'}
986
- onClick={() => setProvider('smtp')}
987
- >
988
- <ProviderIcon>
989
- <Server style={{ width: 28, height: 28 }} />
990
- </ProviderIcon>
991
- <ProviderName>SMTP</ProviderName>
992
- </ProviderCard>
993
-
994
- <ProviderCard
995
- $selected={provider === 'sendgrid'}
996
- onClick={() => setProvider('sendgrid')}
997
- >
998
- <ProviderIcon $bgColor="#1E90FF22" $color="#1E90FF" $shadowColor="rgba(30, 144, 255, 0.2)">
999
- <Cloud style={{ width: 28, height: 28 }} />
1000
- </ProviderIcon>
1001
- <ProviderName>SendGrid</ProviderName>
1002
- </ProviderCard>
1003
-
1004
- <ProviderCard
1005
- $selected={provider === 'mailgun'}
1006
- onClick={() => setProvider('mailgun')}
1007
- >
1008
- <ProviderIcon $bgColor="#FF6B6B22" $color="#FF6B6B" $shadowColor="rgba(255, 107, 107, 0.2)">
1009
- <Mail style={{ width: 28, height: 28 }} />
1010
- </ProviderIcon>
1011
- <ProviderName>Mailgun</ProviderName>
1012
- </ProviderCard>
1013
- </ProvidersGrid>
1014
- </Box>
1015
- )}
1016
-
1017
- {/* Step 2: Name Your Account */}
1018
- {currentStep === 2 && (
1019
- <FormSection>
1020
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1021
- <FullWidthField>
1022
- <Field.Root required>
1023
- <Field.Label>Account Name</Field.Label>
1024
- <TextInput
1025
- placeholder="e.g., Company Gmail, Marketing SendGrid, Transactional Emails"
1026
- value={formData.name}
1027
- onChange={(e) => handleChange('name', e.target.value)}
1028
- />
1029
- <Field.Hint>Give this email account a unique, descriptive name so you can easily identify it later</Field.Hint>
1030
- </Field.Root>
1031
- </FullWidthField>
1032
-
1033
- {provider === 'gmail-oauth' ? (
1034
- <Alert variant="default" title="📧 Email Address">
1035
- <Typography variant="pi">
1036
- Your Gmail address will be automatically retrieved from Google after OAuth authorization.
1037
- You don't need to enter it manually.
1038
- </Typography>
1039
- </Alert>
1040
- ) : (
1041
- <FullWidthField>
1042
- <Field.Root required>
1043
- <Field.Label>From Email Address</Field.Label>
1044
- <TextInput
1045
- placeholder="noreply@company.com"
1046
- type="email"
1047
- value={formData.fromEmail}
1048
- onChange={(e) => handleChange('fromEmail', e.target.value)}
1049
- />
1050
- <Field.Hint>The email address that will appear as the sender. Recipients will see this in their inbox</Field.Hint>
1051
- </Field.Root>
1052
- </FullWidthField>
1053
- )}
1054
-
1055
- <FullWidthField>
1056
- <Field.Root>
1057
- <Field.Label>From Display Name</Field.Label>
1058
- <TextInput
1059
- placeholder="Company Name"
1060
- value={formData.fromName}
1061
- onChange={(e) => handleChange('fromName', e.target.value)}
1062
- />
1063
- <Field.Hint>The friendly name shown next to the email address (e.g., 'ACME Corp' instead of just 'noreply@acme.com')</Field.Hint>
1064
- </Field.Root>
1065
- </FullWidthField>
1066
-
1067
- <FullWidthField>
1068
- <Field.Root>
1069
- <Field.Label>Reply-To Email Address</Field.Label>
1070
- <TextInput
1071
- placeholder="support@company.com"
1072
- type="email"
1073
- value={formData.replyTo}
1074
- onChange={(e) => handleChange('replyTo', e.target.value)}
1075
- />
1076
- <Field.Hint>When recipients hit 'Reply', their response will go to this address. Leave empty to use the From Email</Field.Hint>
1077
- </Field.Root>
1078
- </FullWidthField>
1079
-
1080
- <FullWidthField>
1081
- <Field.Root>
1082
- <Field.Label>Account Description</Field.Label>
1083
- <Textarea
1084
- placeholder="What is this account used for? (e.g., 'Marketing campaigns', 'Order confirmations', 'Password resets')"
1085
- value={formData.description}
1086
- onChange={(e) => handleChange('description', e.target.value)}
1087
- />
1088
- <Field.Hint>Add notes about this account's purpose, usage limits, or any special configuration. Only visible to admins</Field.Hint>
1089
- </Field.Root>
1090
- </FullWidthField>
1091
- </Flex>
1092
- </FormSection>
1093
- )}
1094
-
1095
- {/* Step 3: Credentials */}
1096
- {currentStep === 3 && (
1097
- <FormSection>
1098
- {provider === 'smtp' && (
1099
- <>
1100
- <InfoAlert variant="success" title="🔒 Secure Storage" marginBottom={4}>
1101
- All credentials are encrypted with AES-256-GCM before storage. No plain text passwords in the database.
1102
- </InfoAlert>
1103
-
1104
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1105
- <SectionTitle>
1106
- <Server />
1107
- Server Connection
1108
- </SectionTitle>
1109
-
1110
- <FullWidthField>
1111
- <Field.Root required>
1112
- <Field.Label>SMTP Host Server</Field.Label>
1113
- <TextInput
1114
- placeholder="smtp.gmail.com"
1115
- value={formData.host}
1116
- onChange={(e) => handleChange('host', e.target.value)}
1117
- />
1118
- <Field.Hint>The address of your email server. Common examples: smtp.gmail.com (Gmail), smtp-mail.outlook.com (Outlook), smtp.sendgrid.net (SendGrid)</Field.Hint>
1119
- </Field.Root>
1120
- </FullWidthField>
1121
-
1122
- <FullWidthField>
1123
- <Field.Root required>
1124
- <Field.Label>Port Number</Field.Label>
1125
- <NumberInput
1126
- value={formData.port}
1127
- onValueChange={(value) => handleChange('port', value)}
1128
- />
1129
- <Field.Hint>Standard ports: 587 (recommended - STARTTLS), 465 (SSL/TLS), or 25 (unencrypted - not recommended)</Field.Hint>
1130
- </Field.Root>
1131
- </FullWidthField>
1132
-
1133
- <Divider />
1134
-
1135
- <SectionTitle>
1136
- <Lock />
1137
- Authentication Credentials
1138
- </SectionTitle>
1139
-
1140
- <FullWidthField>
1141
- <Field.Root required>
1142
- <Field.Label>Username / Email</Field.Label>
1143
- <TextInput
1144
- placeholder="your-email@gmail.com"
1145
- value={formData.user}
1146
- onChange={(e) => handleChange('user', e.target.value)}
1147
- />
1148
- <Field.Hint>Usually your full email address. Some providers may use just the username part before the @</Field.Hint>
1149
- </Field.Root>
1150
- </FullWidthField>
1151
-
1152
- <FullWidthField>
1153
- <Field.Root required>
1154
- <Field.Label>Password / App Password</Field.Label>
1155
- <TextInput
1156
- type="password"
1157
- placeholder="Enter your password"
1158
- value={formData.pass}
1159
- onChange={(e) => handleChange('pass', e.target.value)}
1160
- />
1161
- <Field.Hint>For Gmail: Create an App Password in Google Account → Security → 2-Step Verification → App passwords. Regular passwords won't work with 2FA enabled</Field.Hint>
1162
- </Field.Root>
1163
- </FullWidthField>
1164
-
1165
- <Box
1166
- padding={4}
1167
- background={formData.secure ? '#DCFCE7' : '#FEF3C7'}
1168
- hasRadius
1169
- style={{
1170
- border: formData.secure ? '2px solid #22C55E' : '2px solid #F59E0B',
1171
- borderRadius: '8px',
1172
- transition: 'all 0.2s ease',
1173
- }}
1174
- >
1175
- <Flex gap={3} alignItems="center">
1176
- <Toggle
1177
- checked={formData.secure}
1178
- onChange={() => handleChange('secure', !formData.secure)}
1179
- />
1180
- <Flex direction="column" gap={1} style={{ flex: 1 }}>
1181
- <Flex alignItems="center" gap={2}>
1182
- <Typography fontWeight="semiBold" style={{ fontSize: '14px' }}>
1183
- {formData.secure ? '🔒' : '⚠️'} Use SSL/TLS Encryption
1184
- </Typography>
1185
- <Badge
1186
- backgroundColor={formData.secure ? 'success600' : 'warning600'}
1187
- textColor="neutral0"
1188
- size="S"
1189
- >
1190
- {formData.secure ? 'ENABLED' : 'DISABLED'}
1191
- </Badge>
1192
- </Flex>
1193
- <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.5' }}>
1194
- {formData.secure
1195
- ? 'SSL/TLS enabled - Use this for port 465'
1196
- : 'SSL/TLS disabled - Port 587 will use STARTTLS instead'
1197
- }
1198
- </Typography>
1199
- </Flex>
1200
- </Flex>
1201
- </Box>
1202
- </Flex>
1203
- </>
1204
- )}
1205
-
1206
- {provider === 'gmail-oauth' && (
1207
- <>
1208
- <InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
1209
- No passwords stored. Users authenticate directly with Google for maximum security.
1210
- </InfoAlert>
1211
-
1212
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1213
- <SectionTitle>
1214
- <Lock />
1215
- Google OAuth Application
1216
- </SectionTitle>
1217
-
1218
- <FullWidthField>
1219
- <Field.Root required>
1220
- <Field.Label>OAuth Client ID</Field.Label>
1221
- <TextInput
1222
- placeholder="123456789-abc123xyz.apps.googleusercontent.com"
1223
- value={formData.oauthClientId}
1224
- onChange={(e) => handleChange('oauthClientId', e.target.value)}
1225
- />
1226
- <Field.Hint>Found in Google Cloud Console → APIs & Services → Credentials. Looks like a long string ending in .apps.googleusercontent.com</Field.Hint>
1227
- </Field.Root>
1228
- </FullWidthField>
1229
-
1230
- <FullWidthField>
1231
- <Field.Root required>
1232
- <Field.Label>OAuth Client Secret</Field.Label>
1233
- <TextInput
1234
- type="password"
1235
- placeholder="GOCSPX-abcdefghijklmnop"
1236
- value={formData.oauthClientSecret}
1237
- onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
1238
- />
1239
- <Field.Hint>Keep this secret! Found in the same OAuth 2.0 Client ID settings. Never share or commit to git</Field.Hint>
1240
- </Field.Root>
1241
- </FullWidthField>
1242
-
1243
- <Divider />
1244
-
1245
- {oauthCode ? (
1246
- <Alert variant="success" title="✅ OAuth Authorized!">
1247
- <Typography variant="pi">
1248
- You've successfully authorized with Google! Click "Continue" to proceed to settings.
1249
- </Typography>
1250
- </Alert>
1251
- ) : (
1252
- <Box>
1253
- <Typography variant="omega" textColor="neutral600" marginBottom={3}>
1254
- After entering your credentials above, click the button below to connect with Gmail:
1255
- </Typography>
1256
- <Button
1257
- onClick={startGmailOAuth}
1258
- variant="secondary"
1259
- size="L"
1260
- disabled={!formData.oauthClientId}
1261
- style={{
1262
- width: '100%',
1263
- background: '#4285F4',
1264
- color: 'white',
1265
- fontWeight: 600,
1266
- }}
1267
- >
1268
- 🔐 Connect with Google
1269
- </Button>
1270
- </Box>
1271
- )}
1272
-
1273
- <Box
1274
- padding={4}
1275
- background="neutral100"
1276
- hasRadius
1277
- style={{
1278
- border: `1px solid ${colors.border}`,
1279
- borderRadius: '8px'
1280
- }}
1281
- >
1282
- <Typography
1283
- fontWeight="semiBold"
1284
- marginBottom={3}
1285
- style={{ fontSize: '15px' }}
1286
- >
1287
- 📋 Setup Guide
1288
- </Typography>
1289
-
1290
- <Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
1291
- <Box>
1292
- <strong>1.</strong> Go to <a href="https://console.cloud.google.com" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>console.cloud.google.com</a>
1293
- </Box>
1294
-
1295
- <Box>
1296
- <strong>2.</strong> Enable Gmail API (search and click Enable)
1297
- </Box>
1298
-
1299
- <Box>
1300
- <strong>3.</strong> Create Credentials → OAuth Client ID
1301
- </Box>
1302
-
1303
- <Box>
1304
- <strong>4.</strong> Add this redirect URI:
1305
- </Box>
1306
-
1307
- <Flex gap={2} alignItems="center">
1308
- <Box
1309
- padding={2}
1310
- background="neutral0"
1311
- hasRadius
1312
- style={{
1313
- flex: 1,
1314
- fontFamily: 'monospace',
1315
- fontSize: '13px',
1316
- wordBreak: 'break-all',
1317
- border: `1px solid ${colors.border}`
1318
- }}
1319
- >
1320
- {window.location.origin}/magic-mail/oauth/gmail/callback
1321
- </Box>
1322
- <Button
1323
- variant="secondary"
1324
- size="S"
1325
- onClick={() => {
1326
- navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/gmail/callback`);
1327
- toggleNotification({
1328
- type: 'success',
1329
- message: 'Redirect URI copied to clipboard!',
1330
- });
1331
- }}
1332
- >
1333
- Copy
1334
- </Button>
1335
- </Flex>
1336
- </Flex>
1337
- </Box>
1338
- </Flex>
1339
- </>
1340
- )}
1341
-
1342
- {provider === 'microsoft-oauth' && (
1343
- <>
1344
- <InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
1345
- No passwords stored. Users authenticate directly with Microsoft for maximum security.
1346
- </InfoAlert>
1347
-
1348
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1349
- <SectionTitle>
1350
- <Lock />
1351
- Microsoft Azure Application
1352
- </SectionTitle>
1353
-
1354
- <FullWidthField>
1355
- <Field.Root required>
1356
- <Field.Label>Tenant (Directory) ID</Field.Label>
1357
- <TextInput
1358
- placeholder="87654321-4321-4321-4321-987654321abc"
1359
- value={formData.microsoftTenantId}
1360
- onChange={(e) => handleChange('microsoftTenantId', e.target.value)}
1361
- />
1362
- <Field.Hint>Found in Azure Portal → App Registrations → Your App → Overview (next to Application ID). Also a GUID format. Required for OAuth!</Field.Hint>
1363
- </Field.Root>
1364
- </FullWidthField>
1365
-
1366
- <FullWidthField>
1367
- <Field.Root required>
1368
- <Field.Label>Application (Client) ID</Field.Label>
1369
- <TextInput
1370
- placeholder="12345678-1234-1234-1234-123456789abc"
1371
- value={formData.oauthClientId}
1372
- onChange={(e) => handleChange('oauthClientId', e.target.value)}
1373
- />
1374
- <Field.Hint>Found in Azure Portal → App Registrations → Your App → Overview. It's a GUID format.</Field.Hint>
1375
- </Field.Root>
1376
- </FullWidthField>
1377
-
1378
- <FullWidthField>
1379
- <Field.Root required>
1380
- <Field.Label>Client Secret Value</Field.Label>
1381
- <TextInput
1382
- type="password"
1383
- placeholder="abc~123XYZ..."
1384
- value={formData.oauthClientSecret}
1385
- onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
1386
- />
1387
- <Field.Hint>From Azure Portal → Certificates & secrets → Client secrets. Copy the VALUE, not the Secret ID. Keep this secret!</Field.Hint>
1388
- </Field.Root>
1389
- </FullWidthField>
1390
-
1391
- <Divider />
1392
-
1393
- {oauthCode ? (
1394
- <Alert variant="success" title="✅ OAuth Authorized!">
1395
- <Typography variant="pi">
1396
- You've successfully authorized with Microsoft! Click "Continue" to proceed to settings.
1397
- </Typography>
1398
- </Alert>
1399
- ) : (
1400
- <Box>
1401
- <Typography variant="omega" textColor="neutral600" marginBottom={3}>
1402
- After entering your credentials above, click the button below to connect with Microsoft:
1403
- </Typography>
1404
- <Button
1405
- onClick={startMicrosoftOAuth}
1406
- variant="secondary"
1407
- size="L"
1408
- disabled={!formData.oauthClientId}
1409
- style={{
1410
- width: '100%',
1411
- background: '#00A4EF',
1412
- color: 'white',
1413
- fontWeight: 600,
1414
- }}
1415
- >
1416
- 🔐 Connect with Microsoft
1417
- </Button>
1418
- </Box>
1419
- )}
1420
-
1421
- <Box
1422
- padding={4}
1423
- background="neutral100"
1424
- hasRadius
1425
- style={{
1426
- border: `1px solid ${colors.border}`,
1427
- borderRadius: '8px'
1428
- }}
1429
- >
1430
- <Typography
1431
- fontWeight="semiBold"
1432
- marginBottom={3}
1433
- style={{ fontSize: '15px' }}
1434
- >
1435
- 📋 Setup Guide
1436
- </Typography>
1437
-
1438
- <Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
1439
- <Box>
1440
- <strong>1.</strong> Go to <a href="https://portal.azure.com" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>portal.azure.com</a>
1441
- </Box>
1442
-
1443
- <Box>
1444
- <strong>2.</strong> Navigate to Azure Active Directory → App registrations → New registration
1445
- </Box>
1446
-
1447
- <Box>
1448
- <strong>3.</strong> Name your app (e.g., "MagicMail") and select "Accounts in this organizational directory only"
1449
- </Box>
1450
-
1451
- <Box>
1452
- <strong>Important:</strong> Copy both <strong>Application (client) ID</strong> AND <strong>Directory (tenant) ID</strong> from the Overview page!
1453
- </Box>
1454
-
1455
- <Box>
1456
- <strong>4.</strong> Add this redirect URI:
1457
- </Box>
1458
-
1459
- <Flex gap={2} alignItems="center">
1460
- <Box
1461
- padding={2}
1462
- background="neutral0"
1463
- hasRadius
1464
- style={{
1465
- flex: 1,
1466
- fontFamily: 'monospace',
1467
- fontSize: '13px',
1468
- wordBreak: 'break-all',
1469
- color: colors.textSecondary,
1470
- border: `1px solid ${colors.border}`,
1471
- }}
1472
- >
1473
- {`${window.location.origin}/magic-mail/oauth/microsoft/callback`}
1474
- </Box>
1475
- <Button
1476
- variant="secondary"
1477
- size="S"
1478
- onClick={() => {
1479
- navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/microsoft/callback`);
1480
- toggleNotification({
1481
- type: 'success',
1482
- message: 'Redirect URI copied to clipboard!',
1483
- });
1484
- }}
1485
- >
1486
- Copy
1487
- </Button>
1488
- </Flex>
1489
-
1490
- <Box>
1491
- <strong>5.</strong> Under API permissions → Add a permission → Microsoft Graph → Delegated permissions:
1492
- </Box>
1493
-
1494
- <Box style={{ marginLeft: '20px', marginTop: '8px' }}>
1495
- • <code>Mail.Send</code> - Send emails as the signed-in user<br/>
1496
- • <code>User.Read</code> - Read user profile (email address)<br/>
1497
- • <code>offline_access</code> - Maintain access to data (refresh tokens)<br/>
1498
- • <code>openid</code> - Sign users in<br/>
1499
- • <code>email</code> - View users' email address
1500
- </Box>
1501
-
1502
- <Box>
1503
- <strong>6.</strong> Click "Grant admin consent" for your organization (Required!)
1504
- </Box>
1505
-
1506
- <Box>
1507
- <strong>7.</strong> Under Certificates & secrets → Client secrets → New client secret
1508
- </Box>
1509
-
1510
- <Box>
1511
- <strong>8.</strong> Copy the <strong>Value</strong> (not Secret ID) immediately - it won't be shown again
1512
- </Box>
1513
- </Flex>
1514
- </Box>
1515
- </Flex>
1516
- </>
1517
- )}
1518
-
1519
- {provider === 'yahoo-oauth' && (
1520
- <>
1521
- <InfoAlert variant="success" title="🔒 OAuth 2.0 Security" marginBottom={4}>
1522
- No passwords stored. Users authenticate directly with Yahoo for maximum security.
1523
- </InfoAlert>
1524
-
1525
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1526
- <SectionTitle>
1527
- <Lock />
1528
- Yahoo Developer Application
1529
- </SectionTitle>
1530
-
1531
- <FullWidthField>
1532
- <Field.Root required>
1533
- <Field.Label>Yahoo Client ID</Field.Label>
1534
- <TextInput
1535
- placeholder="dj0yJmk9..."
1536
- value={formData.oauthClientId}
1537
- onChange={(e) => handleChange('oauthClientId', e.target.value)}
1538
- />
1539
- <Field.Hint>Found in Yahoo Developer Console → Your App → App Information. Starts with "dj0y..."</Field.Hint>
1540
- </Field.Root>
1541
- </FullWidthField>
1542
-
1543
- <FullWidthField>
1544
- <Field.Root required>
1545
- <Field.Label>Yahoo Client Secret</Field.Label>
1546
- <TextInput
1547
- type="password"
1548
- placeholder="abc123def456..."
1549
- value={formData.oauthClientSecret}
1550
- onChange={(e) => handleChange('oauthClientSecret', e.target.value)}
1551
- />
1552
- <Field.Hint>Keep this secret! Found in the same App Information section. Never share or commit to git.</Field.Hint>
1553
- </Field.Root>
1554
- </FullWidthField>
1555
-
1556
- <Divider />
1557
-
1558
- {oauthCode ? (
1559
- <Alert variant="success" title="✅ OAuth Authorized!">
1560
- <Typography variant="pi">
1561
- You've successfully authorized with Yahoo Mail! Click "Continue" to proceed to settings.
1562
- </Typography>
1563
- </Alert>
1564
- ) : (
1565
- <Box>
1566
- <Typography variant="omega" textColor="neutral600" marginBottom={3}>
1567
- After entering your credentials above, click the button below to connect with Yahoo:
1568
- </Typography>
1569
- <Button
1570
- onClick={startYahooOAuth}
1571
- variant="secondary"
1572
- size="L"
1573
- disabled={!formData.oauthClientId}
1574
- style={{
1575
- width: '100%',
1576
- background: '#6001D2',
1577
- color: 'white',
1578
- fontWeight: 600,
1579
- }}
1580
- >
1581
- 🔐 Connect with Yahoo
1582
- </Button>
1583
- </Box>
1584
- )}
1585
-
1586
- <Box
1587
- padding={4}
1588
- background="neutral100"
1589
- hasRadius
1590
- style={{
1591
- border: `1px solid ${colors.border}`,
1592
- borderRadius: '8px'
1593
- }}
1594
- >
1595
- <Typography
1596
- fontWeight="semiBold"
1597
- marginBottom={3}
1598
- style={{ fontSize: '15px' }}
1599
- >
1600
- 📋 Setup Guide
1601
- </Typography>
1602
-
1603
- <Flex direction="column" gap={2} style={{ fontSize: '14px', lineHeight: '1.6' }}>
1604
- <Box>
1605
- <strong>1.</strong> Go to <a href="https://developer.yahoo.com/apps/" target="_blank" rel="noopener noreferrer" style={{ color: colors.primary, textDecoration: 'underline' }}>developer.yahoo.com/apps</a>
1606
- </Box>
1607
-
1608
- <Box>
1609
- <strong>2.</strong> Click "Create an App"
1610
- </Box>
1611
-
1612
- <Box>
1613
- <strong>3.</strong> Fill in app details (name, description)
1614
- </Box>
1615
-
1616
- <Box>
1617
- <strong>4.</strong> Add this redirect URI:
1618
- </Box>
1619
-
1620
- <Flex gap={2} alignItems="center">
1621
- <Box
1622
- padding={2}
1623
- background="neutral0"
1624
- hasRadius
1625
- style={{
1626
- flex: 1,
1627
- fontFamily: 'monospace',
1628
- fontSize: '13px',
1629
- wordBreak: 'break-all',
1630
- color: colors.textSecondary,
1631
- border: `1px solid ${colors.border}`,
1632
- }}
1633
- >
1634
- {`${window.location.origin}/magic-mail/oauth/yahoo/callback`}
1635
- </Box>
1636
- <Button
1637
- variant="secondary"
1638
- size="S"
1639
- onClick={() => {
1640
- navigator.clipboard.writeText(`${window.location.origin}/magic-mail/oauth/yahoo/callback`);
1641
- toggleNotification({
1642
- type: 'success',
1643
- message: 'Redirect URI copied to clipboard!',
1644
- });
1645
- }}
1646
- >
1647
- Copy
1648
- </Button>
1649
- </Flex>
1650
-
1651
- <Box>
1652
- <strong>5.</strong> Under API Permissions, enable:
1653
- </Box>
1654
-
1655
- <Box style={{ marginLeft: '20px' }}>
1656
- • <code>Mail</code> - Send and manage emails<br/>
1657
- • <code>OpenID Connect</code> - User authentication
1658
- </Box>
1659
-
1660
- <Box>
1661
- <strong>6.</strong> Note your Client ID and Client Secret from the app settings
1662
- </Box>
1663
- </Flex>
1664
- </Box>
1665
- </Flex>
1666
- </>
1667
- )}
1668
-
1669
- {provider === 'sendgrid' && (
1670
- <>
1671
- <InfoAlert variant="success" title="🔒 API Key Security" marginBottom={4}>
1672
- Your API key will be encrypted with AES-256-GCM before storage.
1673
- </InfoAlert>
1674
-
1675
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1676
- <SectionTitle>
1677
- <Key />
1678
- SendGrid API Configuration
1679
- </SectionTitle>
1680
-
1681
- <FullWidthField>
1682
- <Field.Root required>
1683
- <Field.Label>SendGrid API Key</Field.Label>
1684
- <TextInput
1685
- type="password"
1686
- placeholder="SG.xxxxxxxxxxxxxxxxxxxxxx"
1687
- value={formData.apiKey}
1688
- onChange={(e) => handleChange('apiKey', e.target.value)}
1689
- />
1690
- <Field.Hint>
1691
- Found in SendGrid Dashboard → Settings → API Keys. Create a new key with "Mail Send" permission
1692
- </Field.Hint>
1693
- </Field.Root>
1694
- </FullWidthField>
1695
-
1696
- <Alert variant="default" title="📖 SendGrid Resources">
1697
- <Typography variant="pi">
1698
- <strong>Dashboard:</strong> <a href="https://app.sendgrid.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>app.sendgrid.com</a><br/>
1699
- <strong>API Keys:</strong> Settings → API Keys → Create API Key<br/>
1700
- <strong>Required Scope:</strong> Mail Send (Full Access)<br/>
1701
- <strong>Docs:</strong> <a href="https://docs.sendgrid.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>docs.sendgrid.com</a>
1702
- </Typography>
1703
- </Alert>
1704
- </Flex>
1705
- </>
1706
- )}
1707
-
1708
- {provider === 'mailgun' && (
1709
- <>
1710
- <InfoAlert variant="success" title="🔒 API Key Security" marginBottom={4}>
1711
- Your API key will be encrypted with AES-256-GCM before storage.
1712
- </InfoAlert>
1713
-
1714
- <Flex direction="column" gap={4} style={{ width: '100%' }}>
1715
- <SectionTitle>
1716
- <Key />
1717
- Mailgun API Configuration
1718
- </SectionTitle>
1719
-
1720
- <FullWidthField>
1721
- <Field.Root required>
1722
- <Field.Label>Mailgun Domain</Field.Label>
1723
- <TextInput
1724
- placeholder="mg.yourdomain.com or sandbox-xxx.mailgun.org"
1725
- value={formData.mailgunDomain}
1726
- onChange={(e) => handleChange('mailgunDomain', e.target.value)}
1727
- />
1728
- <Field.Hint>
1729
- Your verified Mailgun domain (e.g., mg.yourdomain.com) or sandbox domain for testing
1730
- </Field.Hint>
1731
- </Field.Root>
1732
- </FullWidthField>
1733
-
1734
- <FullWidthField>
1735
- <Field.Root required>
1736
- <Field.Label>Mailgun API Key</Field.Label>
1737
- <TextInput
1738
- type="password"
1739
- placeholder="key-xxxxxxxxxxxxxxxxxxxxxxxx"
1740
- value={formData.apiKey}
1741
- onChange={(e) => handleChange('apiKey', e.target.value)}
1742
- />
1743
- <Field.Hint>
1744
- Found in Mailgun Dashboard → Settings → API Keys. Use your Private API key, not the Public Validation key
1745
- </Field.Hint>
1746
- </Field.Root>
1747
- </FullWidthField>
1748
-
1749
- <Alert variant="default" title="📖 Mailgun Resources">
1750
- <Typography variant="pi">
1751
- <strong>Dashboard:</strong> <a href="https://app.mailgun.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>app.mailgun.com</a><br/>
1752
- <strong>API Keys:</strong> Settings → API Security → Private API Key<br/>
1753
- <strong>Domains:</strong> Sending → Domains (verify your domain or use sandbox)<br/>
1754
- <strong>Docs:</strong> <a href="https://documentation.mailgun.com" target="_blank" rel="noopener noreferrer" style={{color: '#0284c7'}}>documentation.mailgun.com</a>
1755
- </Typography>
1756
- </Alert>
1757
- </Flex>
1758
- </>
1759
- )}
1760
- </FormSection>
1761
- )}
1762
-
1763
- {/* Step 4: Limits & Finalize */}
1764
- {currentStep === 4 && (
1765
- <FormSection>
1766
- <Flex direction="column" gap={5} style={{ width: '100%' }}>
1767
-
1768
- {/* Daily Limit */}
1769
- <Box>
1770
- <Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
1771
- Daily Email Limit
1772
- </Typography>
1773
- <NumberInput
1774
- value={formData.dailyLimit}
1775
- onValueChange={(value) => handleChange('dailyLimit', value)}
1776
- />
1777
- <Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
1778
- Maximum number of emails this account can send per day. Set to 0 for unlimited.
1779
- </Typography>
1780
- </Box>
1781
-
1782
- {/* Hourly Limit */}
1783
- <Box>
1784
- <Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
1785
- Hourly Email Limit
1786
- </Typography>
1787
- <NumberInput
1788
- value={formData.hourlyLimit}
1789
- onValueChange={(value) => handleChange('hourlyLimit', value)}
1790
- />
1791
- <Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
1792
- Maximum number of emails this account can send per hour. Set to 0 for unlimited.
1793
- </Typography>
1794
- </Box>
1795
-
1796
- {/* Priority */}
1797
- <Box>
1798
- <Typography fontWeight="semiBold" marginBottom={2} style={{ fontSize: '15px' }}>
1799
- Account Priority
1800
- </Typography>
1801
- <NumberInput
1802
- value={formData.priority}
1803
- onValueChange={(value) => handleChange('priority', value)}
1804
- min={1}
1805
- max={10}
1806
- />
1807
- <Typography variant="pi" textColor="neutral600" marginTop={2} style={{ fontSize: '13px', lineHeight: '1.5' }}>
1808
- When routing emails, accounts with higher priority (1-10) are preferred. Use 10 for your most reliable account.
1809
- </Typography>
1810
- </Box>
1811
-
1812
- <Divider />
1813
-
1814
- {/* Account Active Toggle */}
1815
- <Box
1816
- padding={4}
1817
- background={formData.isActive ? '#DCFCE7' : '#FEE2E2'}
1818
- hasRadius
1819
- style={{
1820
- border: formData.isActive ? `2px solid #22C55E` : `2px solid #EF4444`,
1821
- borderRadius: '8px',
1822
- transition: 'all 0.2s ease'
1823
- }}
1824
- >
1825
- <Flex gap={3} alignItems="flex-start">
1826
- <Box style={{ paddingTop: '2px' }}>
1827
- <Toggle
1828
- checked={formData.isActive}
1829
- onChange={() => handleChange('isActive', !formData.isActive)}
1830
- />
1831
- </Box>
1832
- <Box style={{ flex: 1 }}>
1833
- <Flex alignItems="center" gap={2} marginBottom={1}>
1834
- <Typography fontWeight="semiBold" style={{ fontSize: '15px' }}>
1835
- {formData.isActive ? '✅' : '❌'} Account Active
1836
- </Typography>
1837
- {formData.isActive ? (
1838
- <Badge backgroundColor="success600" textColor="neutral0" size="S">
1839
- ENABLED
1840
- </Badge>
1841
- ) : (
1842
- <Badge backgroundColor="danger600" textColor="neutral0" size="S">
1843
- DISABLED
1844
- </Badge>
1845
- )}
1846
- </Flex>
1847
- <Typography variant="pi" textColor="neutral600" style={{ lineHeight: '1.6' }}>
1848
- {formData.isActive
1849
- ? 'This account is enabled and can send emails. Disable it to prevent sending without deleting the account.'
1850
- : 'This account is disabled and will not send any emails. Enable it to start sending again.'
1851
- }
1852
- </Typography>
1853
- </Box>
1854
- </Flex>
1855
- </Box>
1856
-
1857
- {/* Primary Account Toggle */}
1858
- <Box
1859
- padding={4}
1860
- background={formData.isPrimary ? '#FEF3C7' : 'neutral100'}
1861
- hasRadius
1862
- style={{
1863
- border: formData.isPrimary ? `2px solid #F59E0B` : `1px solid ${colors.border}`,
1864
- borderRadius: '8px',
1865
- transition: 'all 0.2s ease'
1866
- }}
1867
- >
1868
- <Flex gap={3} alignItems="flex-start">
1869
- <Box style={{ paddingTop: '2px' }}>
1870
- <Toggle
1871
- checked={formData.isPrimary}
1872
- onChange={() => handleChange('isPrimary', !formData.isPrimary)}
1873
- />
1874
- </Box>
1875
- <Box style={{ flex: 1 }}>
1876
- <Flex alignItems="center" gap={2} marginBottom={1}>
1877
- <Typography fontWeight="semiBold" style={{ fontSize: '15px' }}>
1878
- ⭐ Set as Primary Account
1879
- </Typography>
1880
- {formData.isPrimary && (
1881
- <Badge backgroundColor="warning600" textColor="neutral0" size="S">
1882
- PRIMARY
1883
- </Badge>
1884
- )}
1885
- </Flex>
1886
- <Typography variant="pi" textColor="neutral600" style={{ lineHeight: '1.6' }}>
1887
- This account will be used by default when sending emails if no specific account is selected. Only one account can be primary at a time.
1888
- </Typography>
1889
- </Box>
1890
- </Flex>
1891
- </Box>
1892
-
1893
- </Flex>
1894
- </FormSection>
1895
- )}
1896
-
1897
- </Flex>
1898
- </Modal.Body>
1899
-
1900
- <Modal.Footer>
1901
- <Flex justifyContent="space-between" style={{ width: '100%' }}>
1902
- <div>
1903
- {currentStep > 1 && (
1904
- <Button
1905
- variant="tertiary"
1906
- startIcon={<ArrowLeft />}
1907
- onClick={() => setCurrentStep(currentStep - 1)}
1908
- >
1909
- Back
1910
- </Button>
1911
- )}
1912
- </div>
1913
- <Flex gap={2}>
1914
- <Button onClick={onClose} variant="tertiary">
1915
- Cancel
1916
- </Button>
1917
- {currentStep < 4 ? (
1918
- <Button
1919
- endIcon={<ArrowRight />}
1920
- onClick={() => setCurrentStep(currentStep + 1)}
1921
- disabled={!canProceed()}
1922
- >
1923
- Continue
1924
- </Button>
1925
- ) : (
1926
- <Button
1927
- onClick={handleSubmit}
1928
- loading={loading}
1929
- disabled={!canProceed()}
1930
- startIcon={<Check />}
1931
- >
1932
- {isEditMode ? 'Update Account' : 'Create Account'}
1933
- </Button>
1934
- )}
1935
- </Flex>
1936
- </Flex>
1937
- </Modal.Footer>
1938
- </Modal.Content>
1939
- </Modal.Root>
1940
- );
1941
- };
1942
-
1943
- export default AddAccountModal;