Fix tenant event form package selector so it no longer renders empty-value options, handles loading/empty
states, and pulls data from the authenticated /api/v1/tenant/packages endpoint.
(resources/js/admin/pages/EventFormPage.tsx, resources/js/admin/api.ts)
- Harden tenant-admin auth flow: prevent PKCE state loss, scope out StrictMode double-processing, add SPA
routes for /event-admin/login and /event-admin/logout, and tighten token/session clearing semantics (resources/js/admin/auth/{context,tokens}.tsx, resources/js/admin/pages/{AuthCallbackPage,LogoutPage}.tsx,
resources/js/admin/router.tsx, routes/web.php)
This commit is contained in:
@@ -666,7 +666,7 @@ export type Package = {
|
||||
};
|
||||
|
||||
export async function getPackages(type: 'endcustomer' | 'reseller' = 'endcustomer'): Promise<Package[]> {
|
||||
const response = await authorizedFetch(`/api/v1/packages?type=${type}`);
|
||||
const response = await authorizedFetch(`/api/v1/tenant/packages?type=${type}`);
|
||||
const data = await jsonOrThrow<{ data: Package[] }>(response, 'Failed to load packages');
|
||||
return data.data ?? [];
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
clearOAuthSession,
|
||||
clearTokens,
|
||||
completeOAuthCallback,
|
||||
isAuthError,
|
||||
loadTokens,
|
||||
registerAuthFailureHandler,
|
||||
startOAuthFlow,
|
||||
} from './tokens';
|
||||
import { ADMIN_LOGIN_PATH } from '../constants';
|
||||
|
||||
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
|
||||
|
||||
@@ -58,18 +58,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setUser(profile);
|
||||
setStatus('authenticated');
|
||||
} catch (error) {
|
||||
if (isAuthError(error)) {
|
||||
handleAuthFailure();
|
||||
} else {
|
||||
console.error('[Auth] Failed to refresh profile', error);
|
||||
}
|
||||
console.error('[Auth] Failed to refresh profile', error);
|
||||
handleAuthFailure();
|
||||
throw error;
|
||||
}
|
||||
}, [handleAuthFailure]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has('reset-auth') || window.location.pathname === ADMIN_LOGIN_PATH) {
|
||||
clearTokens();
|
||||
clearOAuthSession();
|
||||
setUser(null);
|
||||
setStatus('unauthenticated');
|
||||
}
|
||||
|
||||
const tokens = loadTokens();
|
||||
if (!tokens) {
|
||||
setUser(null);
|
||||
setStatus('unauthenticated');
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +83,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
refreshProfile().catch(() => {
|
||||
// refreshProfile already handled failures.
|
||||
});
|
||||
}, [refreshProfile]);
|
||||
}, [handleAuthFailure, refreshProfile]);
|
||||
|
||||
const login = React.useCallback((redirectPath?: string) => {
|
||||
const target = redirectPath ?? window.location.pathname + window.location.search;
|
||||
|
||||
@@ -166,8 +166,15 @@ export async function startOAuthFlow(redirectPath?: string): Promise<void> {
|
||||
|
||||
sessionStorage.setItem(CODE_VERIFIER_KEY, verifier);
|
||||
sessionStorage.setItem(STATE_KEY, state);
|
||||
localStorage.setItem(CODE_VERIFIER_KEY, verifier);
|
||||
localStorage.setItem(STATE_KEY, state);
|
||||
if (redirectPath) {
|
||||
sessionStorage.setItem(REDIRECT_KEY, redirectPath);
|
||||
localStorage.setItem(REDIRECT_KEY, redirectPath);
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.debug('[Auth] PKCE store', { state, verifier, redirectPath });
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -190,16 +197,23 @@ export async function completeOAuthCallback(params: URLSearchParams): Promise<st
|
||||
|
||||
const code = params.get('code');
|
||||
const returnedState = params.get('state');
|
||||
const verifier = sessionStorage.getItem(CODE_VERIFIER_KEY);
|
||||
const expectedState = sessionStorage.getItem(STATE_KEY);
|
||||
const verifier = sessionStorage.getItem(CODE_VERIFIER_KEY) ?? localStorage.getItem(CODE_VERIFIER_KEY);
|
||||
const expectedState = sessionStorage.getItem(STATE_KEY) ?? localStorage.getItem(STATE_KEY);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.debug('[Auth] PKCE debug', { returnedState, expectedState, hasVerifier: !!verifier, params: params.toString() });
|
||||
}
|
||||
|
||||
if (!code || !verifier || !returnedState || !expectedState || returnedState !== expectedState) {
|
||||
clearOAuthSession();
|
||||
notifyAuthFailure();
|
||||
throw new AuthError('invalid_state', 'PKCE state mismatch');
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(CODE_VERIFIER_KEY);
|
||||
sessionStorage.removeItem(STATE_KEY);
|
||||
localStorage.removeItem(CODE_VERIFIER_KEY);
|
||||
localStorage.removeItem(STATE_KEY);
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
@@ -216,6 +230,7 @@ export async function completeOAuthCallback(params: URLSearchParams): Promise<st
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearOAuthSession();
|
||||
console.error('[Auth] Authorization code exchange failed', response.status);
|
||||
notifyAuthFailure();
|
||||
throw new AuthError('token_exchange_failed', 'Failed to exchange authorization code');
|
||||
@@ -227,6 +242,9 @@ export async function completeOAuthCallback(params: URLSearchParams): Promise<st
|
||||
const redirectTarget = sessionStorage.getItem(REDIRECT_KEY);
|
||||
if (redirectTarget) {
|
||||
sessionStorage.removeItem(REDIRECT_KEY);
|
||||
localStorage.removeItem(REDIRECT_KEY);
|
||||
} else {
|
||||
localStorage.removeItem(REDIRECT_KEY);
|
||||
}
|
||||
|
||||
return redirectTarget;
|
||||
@@ -236,4 +254,7 @@ export function clearOAuthSession(): void {
|
||||
sessionStorage.removeItem(CODE_VERIFIER_KEY);
|
||||
sessionStorage.removeItem(STATE_KEY);
|
||||
sessionStorage.removeItem(REDIRECT_KEY);
|
||||
localStorage.removeItem(CODE_VERIFIER_KEY);
|
||||
localStorage.removeItem(STATE_KEY);
|
||||
localStorage.removeItem(REDIRECT_KEY);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
|
||||
const stripePublishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? "";
|
||||
const paypalClientId = import.meta.env.VITE_PAYPAL_CLIENT_ID ?? "";
|
||||
const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null;
|
||||
|
||||
type StripeCheckoutProps = {
|
||||
clientSecret: string;
|
||||
@@ -268,6 +267,10 @@ export default function WelcomeOrderSummaryPage() {
|
||||
const { t, i18n } = useTranslation("onboarding");
|
||||
const locale = i18n.language?.startsWith("en") ? "en-GB" : "de-DE";
|
||||
const { currencyFormatter, dateFormatter } = useLocaleFormats(locale);
|
||||
const stripePromise = React.useMemo(
|
||||
() => (stripePublishableKey ? loadStripe(stripePublishableKey) : null),
|
||||
[stripePublishableKey]
|
||||
);
|
||||
|
||||
const packageIdFromState = typeof location.state === "object" ? (location.state as any)?.packageId : undefined;
|
||||
const selectedPackageId = progress.selectedPackage?.id ?? packageIdFromState ?? null;
|
||||
@@ -335,7 +338,7 @@ export default function WelcomeOrderSummaryPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [requiresPayment, packageDetails, t]);
|
||||
}, [requiresPayment, packageDetails, stripePromise, t]);
|
||||
|
||||
const priceText =
|
||||
progress.selectedPackage?.priceText ??
|
||||
|
||||
@@ -8,8 +8,14 @@ export default function AuthCallbackPage() {
|
||||
const { completeLogin } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const hasHandledRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasHandledRef.current) {
|
||||
return;
|
||||
}
|
||||
hasHandledRef.current = true;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
completeLogin(params)
|
||||
.then((redirectTo) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Loader2, Save, Sparkles, Package as PackageIcon } from 'lucide-react';
|
||||
import { ArrowLeft, Loader2, Save, Sparkles } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
@@ -220,22 +220,30 @@ export default function EventFormPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="package_id">Package</Label>
|
||||
<Select value={form.package_id.toString()} onValueChange={(value) => setForm((prev) => ({ ...prev, package_id: parseInt(value) }))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Waehlen Sie ein Package" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{packagesLoading ? (
|
||||
<SelectItem value="">Laden...</SelectItem>
|
||||
) : (
|
||||
packages?.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id.toString()}>
|
||||
{pkg.name} - {pkg.price} EUR ({pkg.max_photos} Fotos)
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={form.package_id.toString()}
|
||||
onValueChange={(value) => setForm((prev) => ({ ...prev, package_id: parseInt(value, 10) }))}
|
||||
disabled={packagesLoading || !packages?.length}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={packagesLoading ? 'Pakete werden geladen...' : 'Waehlen Sie ein Package'} />
|
||||
</SelectTrigger>
|
||||
{packages?.length ? (
|
||||
<SelectContent>
|
||||
{packages.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id.toString()}>
|
||||
{pkg.name} - {pkg.price} EUR ({pkg.max_photos} Fotos)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
) : null}
|
||||
</Select>
|
||||
{packagesLoading ? (
|
||||
<p className="text-xs text-slate-500">Pakete werden geladen...</p>
|
||||
) : null}
|
||||
{!packagesLoading && (!packages || packages.length === 0) ? (
|
||||
<p className="text-xs text-red-500">Keine Pakete verfuegbar. Bitte pruefen Sie Ihre Einstellungen.</p>
|
||||
) : null}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">Package-Details</Button>
|
||||
|
||||
19
resources/js/admin/pages/LogoutPage.tsx
Normal file
19
resources/js/admin/pages/LogoutPage.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { ADMIN_PUBLIC_LANDING_PATH } from '../constants';
|
||||
|
||||
export default function LogoutPage() {
|
||||
const { logout } = useAuth();
|
||||
|
||||
React.useEffect(() => {
|
||||
logout({ redirect: ADMIN_PUBLIC_LANDING_PATH });
|
||||
}, [logout]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-rose-50 via-white to-slate-50 text-sm text-slate-600">
|
||||
<div className="rounded-2xl border border-rose-100 bg-white/90 px-6 py-4 shadow-sm shadow-rose-100/60">
|
||||
Abmeldung wird vorbereitet ...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import TaskCollectionsPage from './pages/TaskCollectionsPage';
|
||||
import EmotionsPage from './pages/EmotionsPage';
|
||||
import AuthCallbackPage from './pages/AuthCallbackPage';
|
||||
import WelcomeTeaserPage from './pages/WelcomeTeaserPage';
|
||||
import LogoutPage from './pages/LogoutPage';
|
||||
import { useAuth } from './auth/context';
|
||||
import {
|
||||
ADMIN_BASE_PATH,
|
||||
@@ -71,6 +72,7 @@ export const router = createBrowserRouter([
|
||||
children: [
|
||||
{ index: true, element: <LandingGate /> },
|
||||
{ path: 'login', element: <LoginPage /> },
|
||||
{ path: 'logout', element: <LogoutPage /> },
|
||||
{ path: 'auth/callback', element: <AuthCallbackPage /> },
|
||||
{
|
||||
element: <RequireAuth />,
|
||||
@@ -92,7 +94,6 @@ export const router = createBrowserRouter([
|
||||
{ path: 'welcome/packages', element: <WelcomePackagesPage /> },
|
||||
{ path: 'welcome/summary', element: <WelcomeOrderSummaryPage /> },
|
||||
{ path: 'welcome/event', element: <WelcomeEventSetupPage /> },
|
||||
{ path: '', element: <Navigate to="dashboard" replace /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -102,4 +103,3 @@ export const router = createBrowserRouter([
|
||||
element: <Navigate to={ADMIN_PUBLIC_LANDING_PATH} replace />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -8,15 +8,10 @@ import AppLayout from './layouts/app/AppLayout';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { ConsentProvider } from './contexts/consent';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
|
||||
// Initialize Stripe
|
||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '');
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => title ? `${title} - ${appName}` : appName,
|
||||
resolve: (name) => resolvePageComponent(
|
||||
@@ -42,14 +37,12 @@ createInertiaApp({
|
||||
}
|
||||
|
||||
root.render(
|
||||
<Elements stripe={stripePromise}>
|
||||
<ConsentProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<App {...props} />
|
||||
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||
</I18nextProvider>
|
||||
</ConsentProvider>
|
||||
</Elements>
|
||||
<ConsentProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<App {...props} />
|
||||
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||
</I18nextProvider>
|
||||
</ConsentProvider>
|
||||
);
|
||||
},
|
||||
progress: {
|
||||
|
||||
@@ -21,6 +21,7 @@ interface MatomoTrackerProps {
|
||||
const MatomoTracker: React.FC<MatomoTrackerProps> = ({ config }) => {
|
||||
const page = usePage();
|
||||
const { hasConsent } = useConsent();
|
||||
const scriptNonce = (page.props.security as { csp?: { scriptNonce?: string } } | undefined)?.csp?.scriptNonce;
|
||||
const analyticsConsent = hasConsent('analytics');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,6 +56,19 @@ const MatomoTracker: React.FC<MatomoTrackerProps> = ({ config }) => {
|
||||
script.async = true;
|
||||
script.src = `${base}/matomo.js`;
|
||||
script.dataset.matomo = base;
|
||||
if (scriptNonce) {
|
||||
script.setAttribute('nonce', scriptNonce);
|
||||
} else if (typeof window !== 'undefined' && (window as any).__CSP_NONCE) {
|
||||
script.setAttribute('nonce', (window as any).__CSP_NONCE);
|
||||
} else {
|
||||
const metaNonce = document
|
||||
.querySelector('meta[name="csp-nonce"]')
|
||||
?.getAttribute('content');
|
||||
|
||||
if (metaNonce) {
|
||||
script.setAttribute('nonce', metaNonce);
|
||||
}
|
||||
}
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,16 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
description: 'Es gab zu viele Eingaben in kurzer Zeit. Warte kurz und versuche es erneut.',
|
||||
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten moeglich.',
|
||||
},
|
||||
access_rate_limited: {
|
||||
title: 'Zu viele Aufrufe',
|
||||
description: 'Es gab sehr viele Aufrufe in kurzer Zeit. Warte kurz und versuche es erneut.',
|
||||
hint: 'Tipp: Du kannst es gleich noch einmal versuchen.',
|
||||
},
|
||||
gallery_expired: {
|
||||
title: 'Galerie nicht mehr verfuegbar',
|
||||
description: 'Die Galerie zu diesem Event ist nicht mehr zugaenglich.',
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
},
|
||||
event_not_public: {
|
||||
title: 'Event nicht oeffentlich',
|
||||
description: 'Dieses Event ist aktuell nicht oeffentlich zugaenglich.',
|
||||
@@ -404,6 +414,16 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
description: 'There were too many attempts in a short time. Wait a bit and try again.',
|
||||
hint: 'Tip: You can retry in a few minutes.',
|
||||
},
|
||||
access_rate_limited: {
|
||||
title: 'Too many requests',
|
||||
description: 'There were too many requests in a short time. Please wait a moment and try again.',
|
||||
hint: 'Tip: You can retry shortly.',
|
||||
},
|
||||
gallery_expired: {
|
||||
title: 'Gallery unavailable',
|
||||
description: 'The gallery for this event is no longer accessible.',
|
||||
ctaLabel: 'Request new code',
|
||||
},
|
||||
event_not_public: {
|
||||
title: 'Event not public',
|
||||
description: 'This event is not publicly accessible right now.',
|
||||
|
||||
@@ -197,8 +197,12 @@ function getErrorContent(
|
||||
return build('token_expired', { ctaHref: '/event' });
|
||||
case 'token_rate_limited':
|
||||
return build('token_rate_limited');
|
||||
case 'access_rate_limited':
|
||||
return build('access_rate_limited');
|
||||
case 'event_not_public':
|
||||
return build('event_not_public');
|
||||
case 'gallery_expired':
|
||||
return build('gallery_expired', { ctaHref: '/event' });
|
||||
case 'network_error':
|
||||
return build('network_error');
|
||||
case 'server_error':
|
||||
@@ -219,4 +223,3 @@ function SimpleLayout({ title, children }: { title: string; children: React.Reac
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ const API_ERROR_CODES: FetchEventErrorCode[] = [
|
||||
'token_expired',
|
||||
'token_revoked',
|
||||
'token_rate_limited',
|
||||
'access_rate_limited',
|
||||
'gallery_expired',
|
||||
'event_not_public',
|
||||
];
|
||||
|
||||
@@ -78,9 +80,9 @@ function resolveErrorCode(rawCode: unknown, status: number): FetchEventErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 429) return 'token_rate_limited';
|
||||
if (status === 429) return rawCode === 'access_rate_limited' ? 'access_rate_limited' : 'token_rate_limited';
|
||||
if (status === 404) return 'event_not_public';
|
||||
if (status === 410) return 'token_expired';
|
||||
if (status === 410) return rawCode === 'gallery_expired' ? 'gallery_expired' : 'token_expired';
|
||||
if (status === 401) return 'invalid_token';
|
||||
if (status === 403) return 'token_revoked';
|
||||
if (status >= 500) return 'server_error';
|
||||
@@ -98,6 +100,10 @@ function defaultMessageForCode(code: FetchEventErrorCode): string {
|
||||
return 'Dieser Zugriffscode ist abgelaufen.';
|
||||
case 'token_rate_limited':
|
||||
return 'Zu viele Versuche in kurzer Zeit. Bitte warte einen Moment und versuche es erneut.';
|
||||
case 'access_rate_limited':
|
||||
return 'Zu viele Aufrufe in kurzer Zeit. Bitte warte einen Moment und versuche es erneut.';
|
||||
case 'gallery_expired':
|
||||
return 'Die Galerie ist nicht mehr verfügbar.';
|
||||
case 'event_not_public':
|
||||
return 'Dieses Event ist nicht öffentlich verfügbar.';
|
||||
case 'network_error':
|
||||
|
||||
@@ -243,7 +243,10 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({ stripePublishableKey,
|
||||
const [intentRefreshKey, setIntentRefreshKey] = useState(0);
|
||||
const [processingProvider, setProcessingProvider] = useState<Provider | null>(null);
|
||||
|
||||
const stripePromise = useMemo(() => loadStripe(stripePublishableKey), [stripePublishableKey]);
|
||||
const stripePromise = useMemo(
|
||||
() => (stripePublishableKey ? loadStripe(stripePublishableKey) : null),
|
||||
[stripePublishableKey]
|
||||
);
|
||||
const isFree = useMemo(() => (selectedPackage ? selectedPackage.price <= 0 : false), [selectedPackage]);
|
||||
const isReseller = selectedPackage?.type === 'reseller';
|
||||
|
||||
@@ -299,6 +302,12 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({ stripePublishableKey,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stripePromise) {
|
||||
setStatus('error');
|
||||
setStatusDetail(t('checkout.payment_step.stripe_not_loaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authUser) {
|
||||
setStatus('error');
|
||||
setStatusDetail(t('checkout.payment_step.auth_required'));
|
||||
@@ -351,7 +360,7 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({ stripePublishableKey,
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [authUser, intentRefreshKey, isFree, paymentMethod, paypalDisabled, resetPaymentState, selectedPackage, t]);
|
||||
}, [authUser, intentRefreshKey, isFree, paymentMethod, paypalDisabled, resetPaymentState, selectedPackage, stripePromise, t]);
|
||||
|
||||
const providerLabel = useCallback((provider: Provider) => {
|
||||
switch (provider) {
|
||||
@@ -457,7 +466,7 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({ stripePublishableKey,
|
||||
|
||||
{renderStatusAlert()}
|
||||
|
||||
{paymentMethod === 'stripe' && clientSecret && (
|
||||
{paymentMethod === 'stripe' && clientSecret && stripePromise && (
|
||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||
<StripePaymentForm
|
||||
selectedPackage={selectedPackage}
|
||||
|
||||
6
resources/js/types/index.d.ts
vendored
6
resources/js/types/index.d.ts
vendored
@@ -28,6 +28,12 @@ export interface SharedData {
|
||||
auth: Auth;
|
||||
sidebarOpen: boolean;
|
||||
supportedLocales?: string[];
|
||||
security?: {
|
||||
csp?: {
|
||||
scriptNonce?: string;
|
||||
styleNonce?: string;
|
||||
};
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user