mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
redirect payments when http
This commit is contained in:
@@ -5,6 +5,7 @@ import { loadStripe } from '@stripe/stripe-js';
|
||||
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
|
||||
import licenseService, { PlanTierGroup } from '@app/services/licenseService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
|
||||
|
||||
// Validate Stripe key (static validation, no dynamic imports)
|
||||
const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
@@ -108,10 +109,17 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
current_license_key: existingLicenseKey,
|
||||
requires_seats: selectedPlan.requiresSeats,
|
||||
seat_count: Math.max(1, Math.min(minimumSeats || 1, 10000)),
|
||||
successUrl: `${window.location.origin}/settings/adminPlan?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancelUrl: `${window.location.origin}/settings/adminPlan`,
|
||||
});
|
||||
|
||||
// Check if we got a redirect URL (hosted checkout for HTTP)
|
||||
if (response.url) {
|
||||
console.log('Redirecting to Stripe hosted checkout:', response.url);
|
||||
// Redirect to Stripe's hosted checkout page
|
||||
window.location.href = response.url;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, use embedded checkout (HTTPS)
|
||||
setState({
|
||||
status: 'ready',
|
||||
clientSecret: response.clientSecret,
|
||||
@@ -129,102 +137,29 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
};
|
||||
|
||||
const pollForLicenseKey = useCallback(async (installId: string) => {
|
||||
// Exponential backoff: 1s → 2s → 4s → 8s → 16s (31 seconds total, 5 requests)
|
||||
const BACKOFF_MS = [1000, 2000, 4000, 8000, 16000];
|
||||
let attemptIndex = 0;
|
||||
// Use shared polling utility
|
||||
const result = await pollLicenseKeyWithBackoff(installId, {
|
||||
isMounted: () => isMountedRef.current,
|
||||
onStatusChange: setPollingStatus,
|
||||
});
|
||||
|
||||
setPollingStatus('polling');
|
||||
console.log(`Starting license key polling for installation: ${installId}`);
|
||||
if (result.success && result.licenseKey) {
|
||||
setLicenseKey(result.licenseKey);
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
// Check if component is still mounted
|
||||
if (!isMountedRef.current) {
|
||||
console.log('Polling cancelled: component unmounted');
|
||||
return;
|
||||
// Activate the license key
|
||||
const activation = await activateLicenseKey(result.licenseKey, {
|
||||
isMounted: () => isMountedRef.current,
|
||||
onActivated: onLicenseActivated,
|
||||
});
|
||||
|
||||
if (!activation.success) {
|
||||
console.error('Failed to activate license key:', activation.error);
|
||||
}
|
||||
|
||||
const attemptNumber = attemptIndex + 1;
|
||||
console.log(`Polling attempt ${attemptNumber}/${BACKOFF_MS.length}`);
|
||||
|
||||
try {
|
||||
const response = await licenseService.checkLicenseKey(installId);
|
||||
|
||||
// Check mounted after async operation
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (response.status === 'ready' && response.license_key) {
|
||||
console.log('✅ License key ready!');
|
||||
setLicenseKey(response.license_key);
|
||||
setPollingStatus('ready');
|
||||
|
||||
// Save license key to backend
|
||||
try {
|
||||
const saveResponse = await licenseService.saveLicenseKey(response.license_key);
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (saveResponse.success) {
|
||||
console.log(`License key activated on backend: ${saveResponse.licenseType}`);
|
||||
|
||||
// Fetch and pass license info to parent
|
||||
try {
|
||||
const licenseInfo = await licenseService.getLicenseInfo();
|
||||
if (!isMountedRef.current) return;
|
||||
onLicenseActivated?.(licenseInfo);
|
||||
} catch (infoError) {
|
||||
console.error('Error fetching license info:', infoError);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to save license key to backend:', saveResponse.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving license key to backend:', error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// License not ready yet, continue polling
|
||||
attemptIndex++;
|
||||
|
||||
if (attemptIndex >= BACKOFF_MS.length) {
|
||||
console.warn('⏱️ License polling timeout after all attempts');
|
||||
if (!isMountedRef.current) return;
|
||||
setPollingStatus('timeout');
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule next poll with exponential backoff
|
||||
const nextDelay = BACKOFF_MS[attemptIndex];
|
||||
console.log(`Retrying in ${nextDelay}ms...`);
|
||||
|
||||
pollingTimeoutRef.current = setTimeout(() => {
|
||||
poll();
|
||||
}, nextDelay);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Polling attempt ${attemptNumber} failed:`, error);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
attemptIndex++;
|
||||
|
||||
if (attemptIndex >= BACKOFF_MS.length) {
|
||||
console.error('Polling failed after all attempts');
|
||||
setPollingStatus('timeout');
|
||||
return;
|
||||
}
|
||||
|
||||
// Retry with exponential backoff even on error
|
||||
const nextDelay = BACKOFF_MS[attemptIndex];
|
||||
console.log(`Retrying after error in ${nextDelay}ms...`);
|
||||
|
||||
pollingTimeoutRef.current = setTimeout(() => {
|
||||
poll();
|
||||
}, nextDelay);
|
||||
}
|
||||
};
|
||||
|
||||
await poll();
|
||||
} else if (result.timedOut) {
|
||||
console.warn('License key polling timed out');
|
||||
} else if (result.error) {
|
||||
console.error('License key polling failed:', result.error);
|
||||
}
|
||||
}, [onLicenseActivated]);
|
||||
|
||||
const handlePaymentComplete = async () => {
|
||||
@@ -237,26 +172,16 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
console.log('Upgrade detected - syncing existing license key');
|
||||
setPollingStatus('polling');
|
||||
|
||||
try {
|
||||
const saveResponse = await licenseService.saveLicenseKey(currentLicenseKey);
|
||||
const activation = await activateLicenseKey(currentLicenseKey, {
|
||||
isMounted: () => true, // Modal is open, no need to check
|
||||
onActivated: onLicenseActivated,
|
||||
});
|
||||
|
||||
if (saveResponse.success) {
|
||||
console.log(`License upgraded successfully: ${saveResponse.licenseType}`);
|
||||
setPollingStatus('ready');
|
||||
|
||||
// Fetch and pass updated license info to parent
|
||||
try {
|
||||
const licenseInfo = await licenseService.getLicenseInfo();
|
||||
onLicenseActivated?.(licenseInfo);
|
||||
} catch (infoError) {
|
||||
console.error('Error fetching updated license info:', infoError);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to sync upgraded license:', saveResponse.error);
|
||||
setPollingStatus('timeout');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing upgraded license:', error);
|
||||
if (activation.success) {
|
||||
console.log(`License upgraded successfully: ${activation.licenseType}`);
|
||||
setPollingStatus('ready');
|
||||
} else {
|
||||
console.error('Failed to sync upgraded license:', activation.error);
|
||||
setPollingStatus('timeout');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { ManageBillingButton } from '@app/components/shared/ManageBillingButton';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
|
||||
|
||||
interface PremiumSettingsData {
|
||||
key?: string;
|
||||
@@ -61,6 +62,107 @@ const AdminPlanSection: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle return from hosted Stripe checkout
|
||||
const handleCheckoutReturn = async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paymentStatus = urlParams.get('payment_status');
|
||||
const sessionId = urlParams.get('session_id');
|
||||
|
||||
if (paymentStatus === 'success' && sessionId) {
|
||||
console.log('Payment successful via hosted checkout:', sessionId);
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
// Check if this is an upgrade or new subscription
|
||||
if (currentLicenseInfo?.licenseKey) {
|
||||
// UPGRADE: Sync existing license key
|
||||
console.log('Upgrade detected - syncing existing license');
|
||||
|
||||
const activation = await activateLicenseKey(currentLicenseInfo.licenseKey, {
|
||||
onActivated: fetchLicenseInfo,
|
||||
});
|
||||
|
||||
if (activation.success) {
|
||||
alert({
|
||||
message: t('payment.upgradeSuccess', 'Your subscription has been upgraded successfully!'),
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to sync license after upgrade:', activation.error);
|
||||
alert({
|
||||
message: t('payment.syncError', 'Payment successful but license sync failed. Please contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// NEW SUBSCRIPTION: Poll for license key
|
||||
console.log('New subscription - polling for license key');
|
||||
alert({
|
||||
message: t('payment.paymentSuccess', 'Payment successful! Retrieving your license key...'),
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
try {
|
||||
const installationId = await licenseService.getInstallationId();
|
||||
console.log('Polling for license key with installation ID:', installationId);
|
||||
|
||||
// Use shared polling utility
|
||||
const result = await pollLicenseKeyWithBackoff(installationId);
|
||||
|
||||
if (result.success && result.licenseKey) {
|
||||
// Activate the license key
|
||||
const activation = await activateLicenseKey(result.licenseKey, {
|
||||
onActivated: fetchLicenseInfo,
|
||||
});
|
||||
|
||||
if (activation.success) {
|
||||
console.log(`License key activated: ${activation.licenseType}`);
|
||||
alert({
|
||||
message: t('payment.licenseActivated', 'License key activated successfully!'),
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to save license key:', activation.error);
|
||||
alert({
|
||||
message: t('payment.licenseSaveError', 'Failed to save license key. Please contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} else if (result.timedOut) {
|
||||
console.warn('License key polling timed out');
|
||||
alert({
|
||||
message: t('payment.licenseDelayed', 'License key is being generated. Please check back shortly or contact support.'),
|
||||
color: 'yellow',
|
||||
});
|
||||
} else {
|
||||
console.error('License key polling failed:', result.error);
|
||||
alert({
|
||||
message: t('payment.licensePollingError', 'Failed to retrieve license key. Please check your email or contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to poll for license key:', error);
|
||||
alert({
|
||||
message: t('payment.licenseRetrievalError', 'Failed to retrieve license key. Please check your email or contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (paymentStatus === 'canceled') {
|
||||
console.log('Payment canceled by user');
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
alert({
|
||||
message: t('payment.paymentCanceled', 'Payment was canceled.'),
|
||||
color: 'yellow',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Check if Stripe is configured
|
||||
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
if (!stripeKey || error) {
|
||||
@@ -68,6 +170,9 @@ const AdminPlanSection: React.FC = () => {
|
||||
}
|
||||
fetchLicenseInfo();
|
||||
|
||||
// Handle checkout return after license info is loaded
|
||||
handleCheckoutReturn();
|
||||
|
||||
// Fetch premium settings
|
||||
fetchPremiumSettings();
|
||||
}, [error, config]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { supabase } from '@app/services/supabaseClient';
|
||||
import { getCheckoutMode } from '@app/utils/protocolDetection';
|
||||
|
||||
export interface PlanFeature {
|
||||
name: string;
|
||||
@@ -48,6 +49,7 @@ export interface CheckoutSessionRequest {
|
||||
export interface CheckoutSessionResponse {
|
||||
clientSecret: string;
|
||||
sessionId: string;
|
||||
url?: string; // URL for hosted checkout (when not using HTTPS)
|
||||
}
|
||||
|
||||
export interface BillingPortalResponse {
|
||||
@@ -356,6 +358,11 @@ const licenseService = {
|
||||
* Create a Stripe checkout session for upgrading
|
||||
*/
|
||||
async createCheckoutSession(request: CheckoutSessionRequest): Promise<CheckoutSessionResponse> {
|
||||
// Detect if HTTPS is available to determine checkout mode
|
||||
const checkoutMode = getCheckoutMode();
|
||||
const baseUrl = window.location.origin;
|
||||
const settingsUrl = `${baseUrl}/settings/adminPlan`;
|
||||
|
||||
const { data, error } = await supabase.functions.invoke('create-checkout', {
|
||||
body: {
|
||||
self_hosted: true,
|
||||
@@ -364,7 +371,15 @@ const licenseService = {
|
||||
current_license_key: request.current_license_key,
|
||||
requires_seats: request.requires_seats,
|
||||
seat_count: request.seat_count || 1,
|
||||
callback_base_url: window.location.origin,
|
||||
callback_base_url: baseUrl,
|
||||
ui_mode: checkoutMode,
|
||||
// For hosted checkout, provide success/cancel URLs
|
||||
success_url: checkoutMode === 'hosted'
|
||||
? `${settingsUrl}?session_id={CHECKOUT_SESSION_ID}&payment_status=success`
|
||||
: undefined,
|
||||
cancel_url: checkoutMode === 'hosted'
|
||||
? `${settingsUrl}?payment_status=canceled`
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
199
frontend/src/proprietary/utils/licenseCheckoutUtils.ts
Normal file
199
frontend/src/proprietary/utils/licenseCheckoutUtils.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Shared utilities for license checkout completion
|
||||
* Used by both embedded and hosted checkout flows
|
||||
*/
|
||||
|
||||
import licenseService, { LicenseInfo } from '@app/services/licenseService';
|
||||
|
||||
/**
|
||||
* Result of license key polling
|
||||
*/
|
||||
export interface LicenseKeyPollResult {
|
||||
success: boolean;
|
||||
licenseKey?: string;
|
||||
error?: string;
|
||||
timedOut?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for license key polling
|
||||
*/
|
||||
export interface PollConfig {
|
||||
/** Check if component is still mounted (prevents state updates after unmount) */
|
||||
isMounted?: () => boolean;
|
||||
/** Callback for status changes during polling */
|
||||
onStatusChange?: (status: 'polling' | 'ready' | 'timeout') => void;
|
||||
/** Custom backoff intervals in milliseconds (default: [1000, 2000, 4000, 8000, 16000]) */
|
||||
backoffMs?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for license key with exponential backoff
|
||||
* Consolidates polling logic used by both embedded and hosted checkout
|
||||
*/
|
||||
export async function pollLicenseKeyWithBackoff(
|
||||
installationId: string,
|
||||
config: PollConfig = {}
|
||||
): Promise<LicenseKeyPollResult> {
|
||||
const {
|
||||
isMounted = () => true,
|
||||
onStatusChange,
|
||||
backoffMs = [1000, 2000, 4000, 8000, 16000],
|
||||
} = config;
|
||||
|
||||
let attemptIndex = 0;
|
||||
|
||||
onStatusChange?.('polling');
|
||||
console.log(`Starting license key polling for installation: ${installationId}`);
|
||||
|
||||
const poll = async (): Promise<LicenseKeyPollResult> => {
|
||||
// Check if component is still mounted
|
||||
if (!isMounted()) {
|
||||
console.log('Polling cancelled: component unmounted');
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
const attemptNumber = attemptIndex + 1;
|
||||
console.log(`Polling attempt ${attemptNumber}/${backoffMs.length}`);
|
||||
|
||||
try {
|
||||
const response = await licenseService.checkLicenseKey(installationId);
|
||||
|
||||
// Check mounted after async operation
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
if (response.status === 'ready' && response.license_key) {
|
||||
console.log('✅ License key ready!');
|
||||
onStatusChange?.('ready');
|
||||
return {
|
||||
success: true,
|
||||
licenseKey: response.license_key,
|
||||
};
|
||||
}
|
||||
|
||||
// License not ready yet, continue polling
|
||||
attemptIndex++;
|
||||
|
||||
if (attemptIndex >= backoffMs.length) {
|
||||
console.warn('⏱️ License polling timeout after all attempts');
|
||||
onStatusChange?.('timeout');
|
||||
return {
|
||||
success: false,
|
||||
timedOut: true,
|
||||
error: 'Polling timeout - license key not ready',
|
||||
};
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
const nextDelay = backoffMs[attemptIndex];
|
||||
console.log(`Retrying in ${nextDelay}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, nextDelay));
|
||||
|
||||
return poll();
|
||||
} catch (error) {
|
||||
console.error(`Polling attempt ${attemptNumber} failed:`, error);
|
||||
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
attemptIndex++;
|
||||
|
||||
if (attemptIndex >= backoffMs.length) {
|
||||
console.error('Polling failed after all attempts');
|
||||
onStatusChange?.('timeout');
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Polling failed',
|
||||
};
|
||||
}
|
||||
|
||||
// Retry with exponential backoff even on error
|
||||
const nextDelay = backoffMs[attemptIndex];
|
||||
console.log(`Retrying after error in ${nextDelay}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, nextDelay));
|
||||
|
||||
return poll();
|
||||
}
|
||||
};
|
||||
|
||||
return poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of license key activation
|
||||
*/
|
||||
export interface LicenseActivationResult {
|
||||
success: boolean;
|
||||
licenseType?: string;
|
||||
licenseInfo?: LicenseInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a license key by saving it to the backend and fetching updated info
|
||||
* Consolidates activation logic used by both embedded and hosted checkout
|
||||
*/
|
||||
export async function activateLicenseKey(
|
||||
licenseKey: string,
|
||||
options: {
|
||||
/** Check if component is still mounted */
|
||||
isMounted?: () => boolean;
|
||||
/** Callback when license is activated with updated info */
|
||||
onActivated?: (licenseInfo: LicenseInfo) => void;
|
||||
} = {}
|
||||
): Promise<LicenseActivationResult> {
|
||||
const { isMounted = () => true, onActivated } = options;
|
||||
|
||||
try {
|
||||
console.log('Activating license key...');
|
||||
const saveResponse = await licenseService.saveLicenseKey(licenseKey);
|
||||
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
if (saveResponse.success) {
|
||||
console.log(`License key activated: ${saveResponse.licenseType}`);
|
||||
|
||||
// Fetch updated license info
|
||||
try {
|
||||
const licenseInfo = await licenseService.getLicenseInfo();
|
||||
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
onActivated?.(licenseInfo);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
licenseType: saveResponse.licenseType,
|
||||
licenseInfo,
|
||||
};
|
||||
} catch (infoError) {
|
||||
console.error('Error fetching license info after activation:', infoError);
|
||||
// Still return success since save succeeded
|
||||
return {
|
||||
success: true,
|
||||
licenseType: saveResponse.licenseType,
|
||||
error: 'Failed to fetch updated license info',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to save license key:', saveResponse.error);
|
||||
return {
|
||||
success: false,
|
||||
error: saveResponse.error || 'Failed to save license key',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating license key:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Activation failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
43
frontend/src/proprietary/utils/protocolDetection.ts
Normal file
43
frontend/src/proprietary/utils/protocolDetection.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Protocol detection utility for determining secure context
|
||||
* Used to decide between Embedded Checkout (HTTPS) and Hosted Checkout (HTTP)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the current context is secure (HTTPS or localhost)
|
||||
* @returns true if HTTPS or localhost, false if HTTP
|
||||
*/
|
||||
export function isSecureContext(): boolean {
|
||||
// Allow localhost for development (works with both HTTP and HTTPS)
|
||||
if (typeof window !== 'undefined') {
|
||||
const hostname = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
// Localhost is considered secure for development
|
||||
// if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// Check if HTTPS
|
||||
return protocol === 'https:';
|
||||
}
|
||||
|
||||
// Default to false if window is not available (SSR context)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate Stripe checkout UI mode based on current context
|
||||
* @returns 'embedded' for HTTPS/localhost, 'hosted' for HTTP
|
||||
*/
|
||||
export function getCheckoutMode(): 'embedded' | 'hosted' {
|
||||
return isSecureContext() ? 'embedded' : 'hosted';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Embedded Checkout can be used in current context
|
||||
* @returns true if secure context (HTTPS/localhost)
|
||||
*/
|
||||
export function canUseEmbeddedCheckout(): boolean {
|
||||
return isSecureContext();
|
||||
}
|
||||
Reference in New Issue
Block a user