|
@@ -0,0 +1,343 @@
|
|
|
|
|
+import { useState, useEffect } from "react";
|
|
|
|
|
+import { useNavigate, useSearchParams } from "react-router-dom";
|
|
|
|
|
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
|
|
|
+import { Store, LogIn, UserPlus, Loader2, CheckCircle, AlertCircle, ArrowRight } from "lucide-react";
|
|
|
|
|
+import { useAuth } from "@/components/context/AuthContext";
|
|
|
|
|
+import { API_URL } from "@/lib/config";
|
|
|
|
|
+import { useToast } from "@/hooks/use-toast";
|
|
|
|
|
+import { useTranslation } from "react-i18next";
|
|
|
|
|
+
|
|
|
|
|
+interface PendingInstallation {
|
|
|
|
|
+ installation_id: string;
|
|
|
|
|
+ shopname: string;
|
|
|
|
|
+ platform: 'shoprenter' | 'shopify' | 'woocommerce';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function IntegrationsRedirect() {
|
|
|
|
|
+ const { t } = useTranslation();
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+ const [searchParams] = useSearchParams();
|
|
|
|
|
+ const { isAuthenticated, loading: authLoading } = useAuth();
|
|
|
|
|
+ const { toast } = useToast();
|
|
|
|
|
+
|
|
|
|
|
+ const [pendingInstall, setPendingInstall] = useState<PendingInstallation | null>(null);
|
|
|
|
|
+ const [showAuthDialog, setShowAuthDialog] = useState(false);
|
|
|
|
|
+ const [showAssignDialog, setShowAssignDialog] = useState(false);
|
|
|
|
|
+ const [completing, setCompleting] = useState(false);
|
|
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ // Detect platform from URL parameters
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const srInstall = searchParams.get('sr_install');
|
|
|
|
|
+ const shopname = searchParams.get('shopname');
|
|
|
|
|
+ const errorParam = searchParams.get('error');
|
|
|
|
|
+
|
|
|
|
|
+ // Handle errors
|
|
|
|
|
+ if (errorParam) {
|
|
|
|
|
+ setError(errorParam === 'token_exchange_failed'
|
|
|
|
|
+ ? t('integrations.oauth.errors.token_exchange_failed', 'Failed to exchange token with ShopRenter. Please try again.')
|
|
|
|
|
+ : t(`integrations.oauth.errors.${errorParam}`, errorParam));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Detect ShopRenter installation
|
|
|
|
|
+ if (srInstall && shopname) {
|
|
|
|
|
+ setPendingInstall({
|
|
|
|
|
+ installation_id: srInstall,
|
|
|
|
|
+ shopname: shopname,
|
|
|
|
|
+ platform: 'shoprenter'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [searchParams, t]);
|
|
|
|
|
+
|
|
|
|
|
+ // Handle auth state changes
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (authLoading) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (pendingInstall) {
|
|
|
|
|
+ if (isAuthenticated) {
|
|
|
|
|
+ // User is logged in, show assign dialog
|
|
|
|
|
+ setShowAssignDialog(true);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // User is not logged in, show auth options
|
|
|
|
|
+ setShowAuthDialog(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [isAuthenticated, authLoading, pendingInstall]);
|
|
|
|
|
+
|
|
|
|
|
+ // Complete the installation
|
|
|
|
|
+ const completeInstallation = async () => {
|
|
|
|
|
+ if (!pendingInstall) return;
|
|
|
|
|
+
|
|
|
|
|
+ setCompleting(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) {
|
|
|
|
|
+ throw new Error('No session data found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/complete-shoprenter-install`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ installation_id: pendingInstall.installation_id
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(data.error || 'Failed to complete installation');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('integrations.oauth.shopConnected', 'Shop Connected'),
|
|
|
|
|
+ description: t('integrations.oauth.shopConnectedDescription', { shopName: pendingInstall.shopname }),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Redirect to webshops page
|
|
|
|
|
+ navigate('/webshops?sr_connected=true&store=' + encodeURIComponent(pendingInstall.shopname));
|
|
|
|
|
+
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Error completing installation:', err);
|
|
|
|
|
+ const errorMessage = err instanceof Error ? err.message : 'Failed to complete installation';
|
|
|
|
|
+ setError(errorMessage);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('integrations.oauth.connectionFailed', 'Connection Failed'),
|
|
|
|
|
+ description: errorMessage,
|
|
|
|
|
+ variant: "destructive",
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setCompleting(false);
|
|
|
|
|
+ setShowAssignDialog(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Handle login redirect
|
|
|
|
|
+ const handleLogin = () => {
|
|
|
|
|
+ // Store pending installation in sessionStorage to retrieve after login
|
|
|
|
|
+ if (pendingInstall) {
|
|
|
|
|
+ sessionStorage.setItem('pending_install', JSON.stringify(pendingInstall));
|
|
|
|
|
+ }
|
|
|
|
|
+ navigate('/login?redirect=/integrations&' + searchParams.toString());
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Handle signup redirect
|
|
|
|
|
+ const handleSignup = () => {
|
|
|
|
|
+ // Store pending installation in sessionStorage to retrieve after signup
|
|
|
|
|
+ if (pendingInstall) {
|
|
|
|
|
+ sessionStorage.setItem('pending_install', JSON.stringify(pendingInstall));
|
|
|
|
|
+ }
|
|
|
|
|
+ navigate('/signup?redirect=/integrations&' + searchParams.toString());
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Handle creating new account for this shop
|
|
|
|
|
+ const handleCreateNewAccount = () => {
|
|
|
|
|
+ if (pendingInstall) {
|
|
|
|
|
+ sessionStorage.setItem('pending_install', JSON.stringify(pendingInstall));
|
|
|
|
|
+ }
|
|
|
|
|
+ // Log out current user and redirect to signup
|
|
|
|
|
+ localStorage.removeItem('session_data');
|
|
|
|
|
+ navigate('/signup?redirect=/integrations&' + searchParams.toString());
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Show error state
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="min-h-screen flex items-center justify-center bg-slate-900 p-4">
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700 max-w-md w-full">
|
|
|
|
|
+ <CardHeader className="text-center">
|
|
|
|
|
+ <AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
|
|
|
+ <CardTitle className="text-white text-2xl">
|
|
|
|
|
+ {t('integrations.oauth.connectionFailed', 'Connection Failed')}
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription className="text-slate-400">
|
|
|
|
|
+ {error}
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent className="space-y-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ onClick={() => navigate('/webshops')}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('integrations.oauth.backToIntegrations', 'Back to Integrations')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Show loading state
|
|
|
|
|
+ if (authLoading || (!pendingInstall && !error)) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="min-h-screen flex items-center justify-center bg-slate-900">
|
|
|
|
|
+ <div className="text-center">
|
|
|
|
|
+ <Loader2 className="w-12 h-12 text-cyan-500 animate-spin mx-auto mb-4" />
|
|
|
|
|
+ <p className="text-slate-400">
|
|
|
|
|
+ {t('integrations.oauth.processing', 'Processing integration...')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Platform info
|
|
|
|
|
+ const platformInfo = {
|
|
|
|
|
+ shoprenter: {
|
|
|
|
|
+ name: 'ShopRenter',
|
|
|
|
|
+ color: 'text-blue-500',
|
|
|
|
|
+ bgColor: 'bg-blue-500'
|
|
|
|
|
+ },
|
|
|
|
|
+ shopify: {
|
|
|
|
|
+ name: 'Shopify',
|
|
|
|
|
+ color: 'text-green-500',
|
|
|
|
|
+ bgColor: 'bg-green-500'
|
|
|
|
|
+ },
|
|
|
|
|
+ woocommerce: {
|
|
|
|
|
+ name: 'WooCommerce',
|
|
|
|
|
+ color: 'text-purple-500',
|
|
|
|
|
+ bgColor: 'bg-purple-500'
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const platform = pendingInstall ? platformInfo[pendingInstall.platform] : null;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="min-h-screen flex items-center justify-center bg-slate-900 p-4">
|
|
|
|
|
+ {/* Auth Dialog for non-authenticated users */}
|
|
|
|
|
+ <Dialog open={showAuthDialog} onOpenChange={setShowAuthDialog}>
|
|
|
|
|
+ <DialogContent className="bg-slate-800 border-slate-700 max-w-md">
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <div className="flex items-center justify-center mb-4">
|
|
|
|
|
+ <Store className={`w-12 h-12 ${platform?.color || 'text-cyan-500'}`} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <DialogTitle className="text-white text-center text-xl">
|
|
|
|
|
+ {t('integrations.oauth.connectShop', 'Connect Your Shop')}
|
|
|
|
|
+ </DialogTitle>
|
|
|
|
|
+ <DialogDescription className="text-slate-400 text-center">
|
|
|
|
|
+ {t('integrations.oauth.authRequired', 'Sign in or create an account to connect')}
|
|
|
|
|
+ <span className="font-semibold text-white"> {pendingInstall?.shopname}</span>
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-3 mt-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ onClick={handleLogin}
|
|
|
|
|
+ >
|
|
|
|
|
+ <LogIn className="w-4 h-4 mr-2" />
|
|
|
|
|
+ {t('integrations.oauth.signIn', 'Sign in to your account')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ className="w-full border-slate-600 text-white hover:bg-slate-700"
|
|
|
|
|
+ onClick={handleSignup}
|
|
|
|
|
+ >
|
|
|
|
|
+ <UserPlus className="w-4 h-4 mr-2" />
|
|
|
|
|
+ {t('integrations.oauth.createAccount', 'Create a new account')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <DialogFooter className="mt-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ className="w-full text-slate-400 hover:text-white"
|
|
|
|
|
+ onClick={() => navigate('/')}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('common.cancel', 'Cancel')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Assign Dialog for authenticated users */}
|
|
|
|
|
+ <Dialog open={showAssignDialog} onOpenChange={setShowAssignDialog}>
|
|
|
|
|
+ <DialogContent className="bg-slate-800 border-slate-700 max-w-md">
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <div className="flex items-center justify-center mb-4">
|
|
|
|
|
+ <CheckCircle className={`w-12 h-12 ${platform?.color || 'text-cyan-500'}`} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <DialogTitle className="text-white text-center text-xl">
|
|
|
|
|
+ {t('integrations.oauth.readyToConnect', 'Ready to Connect')}
|
|
|
|
|
+ </DialogTitle>
|
|
|
|
|
+ <DialogDescription className="text-slate-400 text-center">
|
|
|
|
|
+ {t('integrations.oauth.assignShop', 'Connect')}
|
|
|
|
|
+ <span className="font-semibold text-white"> {pendingInstall?.shopname}</span>
|
|
|
|
|
+ {t('integrations.oauth.toAccount', ' to your account?')}
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-3 mt-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ onClick={completeInstallation}
|
|
|
|
|
+ disabled={completing}
|
|
|
|
|
+ >
|
|
|
|
|
+ {completing ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
+ {t('integrations.oauth.connecting', 'Connecting...')}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <ArrowRight className="w-4 h-4 mr-2" />
|
|
|
|
|
+ {t('integrations.oauth.connectToMyAccount', 'Connect to my account')}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ className="w-full border-slate-600 text-white hover:bg-slate-700"
|
|
|
|
|
+ onClick={handleCreateNewAccount}
|
|
|
|
|
+ disabled={completing}
|
|
|
|
|
+ >
|
|
|
|
|
+ <UserPlus className="w-4 h-4 mr-2" />
|
|
|
|
|
+ {t('integrations.oauth.createNewAccount', 'Create a new account instead')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <DialogFooter className="mt-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ className="w-full text-slate-400 hover:text-white"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setShowAssignDialog(false);
|
|
|
|
|
+ navigate('/webshops');
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabled={completing}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('common.cancel', 'Cancel')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Fallback content when no dialog is shown */}
|
|
|
|
|
+ {!showAuthDialog && !showAssignDialog && pendingInstall && (
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700 max-w-md w-full">
|
|
|
|
|
+ <CardHeader className="text-center">
|
|
|
|
|
+ <Store className={`w-16 h-16 ${platform?.color || 'text-cyan-500'} mx-auto mb-4`} />
|
|
|
|
|
+ <CardTitle className="text-white text-2xl">
|
|
|
|
|
+ {t('integrations.oauth.connectingShop', 'Connecting Shop')}
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription className="text-slate-400">
|
|
|
|
|
+ {pendingInstall.shopname}
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent className="flex justify-center">
|
|
|
|
|
+ <Loader2 className="w-8 h-8 text-cyan-500 animate-spin" />
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|