Add Stripe notifications, messaging improvements, and code cleanup
Stripe Notifications: - Add periodic task to check Stripe Connect accounts for requirements - Create in-app notifications for business owners when action needed - Add management command to setup Stripe periodic tasks - Display Stripe notifications with credit card icon in notification bell - Navigate to payments page when Stripe notification clicked Messaging Improvements: - Add "Everyone" option to broadcast message recipients - Allow sending messages to yourself (remove self-exclusion) - Fix broadcast message ID not returned after creation - Add real-time websocket support for broadcast notifications - Show toast when broadcast message received via websocket UI Fixes: - Remove "View all" button from notifications (no page exists) - Add StripeNotificationBanner component for Connect alerts - Connect useUserNotifications hook in TopBar for app-wide websocket Code Cleanup: - Remove legacy automations app and plugin system - Remove safe_scripting module (moved to Activepieces) - Add migration to remove plugin-related models - Various test improvements and coverage additions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* onboarding experience without redirecting users away from the app.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
ConnectComponentsProvider,
|
||||
ConnectAccountOnboarding,
|
||||
@@ -22,6 +22,65 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createAccountSession, refreshConnectStatus, ConnectAccountInfo } from '../api/payments';
|
||||
import { useDarkMode } from '../hooks/useDarkMode';
|
||||
|
||||
// Get appearance config based on dark mode
|
||||
const getAppearance = (isDark: boolean) => ({
|
||||
overlays: 'drawer' as const,
|
||||
variables: {
|
||||
// Brand colors - using your blue theme
|
||||
colorPrimary: '#3b82f6', // brand-500
|
||||
colorBackground: isDark ? '#1f2937' : '#ffffff', // gray-800 / white
|
||||
colorText: isDark ? '#f9fafb' : '#111827', // gray-50 / gray-900
|
||||
colorSecondaryText: isDark ? '#9ca3af' : '#6b7280', // gray-400 / gray-500
|
||||
colorBorder: isDark ? '#374151' : '#e5e7eb', // gray-700 / gray-200
|
||||
colorDanger: '#ef4444', // red-500
|
||||
|
||||
// Typography - matching Inter font
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
fontSizeBase: '14px',
|
||||
fontSizeSm: '12px',
|
||||
fontSizeLg: '16px',
|
||||
fontSizeXl: '18px',
|
||||
fontWeightNormal: '400',
|
||||
fontWeightMedium: '500',
|
||||
fontWeightBold: '600',
|
||||
|
||||
// Spacing & Borders - matching your rounded-lg style
|
||||
spacingUnit: '12px',
|
||||
borderRadius: '8px',
|
||||
|
||||
// Form elements
|
||||
formBackgroundColor: isDark ? '#111827' : '#f9fafb', // gray-900 / gray-50
|
||||
formBorderColor: isDark ? '#374151' : '#d1d5db', // gray-700 / gray-300
|
||||
formHighlightColorBorder: '#3b82f6', // brand-500
|
||||
formAccentColor: '#3b82f6', // brand-500
|
||||
|
||||
// Buttons
|
||||
buttonPrimaryColorBackground: '#3b82f6', // brand-500
|
||||
buttonPrimaryColorText: '#ffffff',
|
||||
buttonSecondaryColorBackground: isDark ? '#374151' : '#f3f4f6', // gray-700 / gray-100
|
||||
buttonSecondaryColorText: isDark ? '#f9fafb' : '#374151', // gray-50 / gray-700
|
||||
buttonSecondaryColorBorder: isDark ? '#4b5563' : '#d1d5db', // gray-600 / gray-300
|
||||
|
||||
// Action colors
|
||||
actionPrimaryColorText: '#3b82f6', // brand-500
|
||||
actionSecondaryColorText: isDark ? '#9ca3af' : '#6b7280', // gray-400 / gray-500
|
||||
|
||||
// Badge colors
|
||||
badgeNeutralColorBackground: isDark ? '#374151' : '#f3f4f6', // gray-700 / gray-100
|
||||
badgeNeutralColorText: isDark ? '#d1d5db' : '#4b5563', // gray-300 / gray-600
|
||||
badgeSuccessColorBackground: isDark ? '#065f46' : '#d1fae5', // green-800 / green-100
|
||||
badgeSuccessColorText: isDark ? '#6ee7b7' : '#065f46', // green-300 / green-800
|
||||
badgeWarningColorBackground: isDark ? '#92400e' : '#fef3c7', // amber-800 / amber-100
|
||||
badgeWarningColorText: isDark ? '#fcd34d' : '#92400e', // amber-300 / amber-800
|
||||
badgeDangerColorBackground: isDark ? '#991b1b' : '#fee2e2', // red-800 / red-100
|
||||
badgeDangerColorText: isDark ? '#fca5a5' : '#991b1b', // red-300 / red-800
|
||||
|
||||
// Offset background (used for layered sections)
|
||||
offsetBackgroundColor: isDark ? '#111827' : '#f9fafb', // gray-900 / gray-50
|
||||
},
|
||||
});
|
||||
|
||||
interface ConnectOnboardingEmbedProps {
|
||||
connectAccount: ConnectAccountInfo | null;
|
||||
@@ -39,13 +98,62 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
onError,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isDark = useDarkMode();
|
||||
const [stripeConnectInstance, setStripeConnectInstance] = useState<StripeConnectInstance | null>(null);
|
||||
const [loadingState, setLoadingState] = useState<LoadingState>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Track the theme that was used when initializing
|
||||
const initializedThemeRef = useRef<boolean | null>(null);
|
||||
// Flag to trigger auto-reinitialize
|
||||
const [needsReinit, setNeedsReinit] = useState(false);
|
||||
|
||||
const isActive = connectAccount?.status === 'active' && connectAccount?.charges_enabled;
|
||||
|
||||
// Initialize Stripe Connect
|
||||
// Detect theme changes when onboarding is already open
|
||||
useEffect(() => {
|
||||
if (loadingState === 'ready' && initializedThemeRef.current !== null && initializedThemeRef.current !== isDark) {
|
||||
// Theme changed while onboarding is open - trigger reinitialize
|
||||
setNeedsReinit(true);
|
||||
}
|
||||
}, [isDark, loadingState]);
|
||||
|
||||
// Handle reinitialization
|
||||
useEffect(() => {
|
||||
if (needsReinit) {
|
||||
setStripeConnectInstance(null);
|
||||
initializedThemeRef.current = null;
|
||||
setNeedsReinit(false);
|
||||
// Re-run initialization
|
||||
(async () => {
|
||||
setLoadingState('loading');
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await createAccountSession();
|
||||
const { client_secret, publishable_key } = response.data;
|
||||
|
||||
const instance = await loadConnectAndInitialize({
|
||||
publishableKey: publishable_key,
|
||||
fetchClientSecret: async () => client_secret,
|
||||
appearance: getAppearance(isDark),
|
||||
});
|
||||
|
||||
setStripeConnectInstance(instance);
|
||||
setLoadingState('ready');
|
||||
initializedThemeRef.current = isDark;
|
||||
} catch (err: any) {
|
||||
console.error('Failed to reinitialize Stripe Connect:', err);
|
||||
const message = err.response?.data?.error || err.message || t('payments.failedToInitializePayment');
|
||||
setErrorMessage(message);
|
||||
setLoadingState('error');
|
||||
onError?.(message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [needsReinit, isDark, t, onError]);
|
||||
|
||||
// Initialize Stripe Connect (user-triggered)
|
||||
const initializeStripeConnect = useCallback(async () => {
|
||||
if (loadingState === 'loading' || loadingState === 'ready') return;
|
||||
|
||||
@@ -57,27 +165,16 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
const response = await createAccountSession();
|
||||
const { client_secret, publishable_key } = response.data;
|
||||
|
||||
// Initialize the Connect instance
|
||||
// Initialize the Connect instance with theme-aware appearance
|
||||
const instance = await loadConnectAndInitialize({
|
||||
publishableKey: publishable_key,
|
||||
fetchClientSecret: async () => client_secret,
|
||||
appearance: {
|
||||
overlays: 'drawer',
|
||||
variables: {
|
||||
colorPrimary: '#635BFF',
|
||||
colorBackground: '#ffffff',
|
||||
colorText: '#1a1a1a',
|
||||
colorDanger: '#df1b41',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontSizeBase: '14px',
|
||||
spacingUnit: '12px',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
appearance: getAppearance(isDark),
|
||||
});
|
||||
|
||||
setStripeConnectInstance(instance);
|
||||
setLoadingState('ready');
|
||||
initializedThemeRef.current = isDark;
|
||||
} catch (err: any) {
|
||||
console.error('Failed to initialize Stripe Connect:', err);
|
||||
const message = err.response?.data?.error || err.message || t('payments.failedToInitializePayment');
|
||||
@@ -85,7 +182,7 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
setLoadingState('error');
|
||||
onError?.(message);
|
||||
}
|
||||
}, [loadingState, onError, t]);
|
||||
}, [loadingState, onError, t, isDark]);
|
||||
|
||||
// Handle onboarding completion
|
||||
const handleOnboardingExit = useCallback(async () => {
|
||||
@@ -242,7 +339,7 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
|
||||
<button
|
||||
onClick={initializeStripeConnect}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium text-white bg-[#635BFF] rounded-lg hover:bg-[#5851ea] transition-colors"
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium text-white bg-brand-500 rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
<CreditCard size={18} />
|
||||
{t('payments.startPaymentSetup')}
|
||||
@@ -255,7 +352,7 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
if (loadingState === 'loading') {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin text-[#635BFF] mb-4" size={40} />
|
||||
<Loader2 className="animate-spin text-brand-500 mb-4" size={40} />
|
||||
<p className="text-gray-600 dark:text-gray-400">{t('payments.initializingPaymentSetup')}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user