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:
poduck
2025-12-22 15:35:53 -05:00
parent 28d6cee207
commit f1b1f18bc5
86 changed files with 21364 additions and 19708 deletions

View File

@@ -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>
);