Fix Stuffbox popup completion and preflight storage before file selection
Hop-State: A_06FP87Y83XVM00YY2RB0X2G Hop-Proposal: R_06FP87XN1TX5B0CW1ZFDEK8 Hop-Task: T_06FP86NZGXSMEX63FZDAS70 Hop-Attempt: AT_06FP86NZGYZEGPH937G3WK8
This commit is contained in:
+38
-13
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import AutoTextarea from '@/components/AutoTextarea';
|
||||
import { StorageSessionPrompt } from '@/components/StorageSessionPrompt';
|
||||
@@ -8,7 +8,7 @@ import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPro
|
||||
import { useToast } from '@/lib/contexts/ToastContext';
|
||||
import { useAccentColor } from '@/lib/contexts/AccentColorContext';
|
||||
import { refreshStorageSession } from '@/lib/storage/client';
|
||||
import { MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
|
||||
export default function AdminPage() {
|
||||
const { showToast } = useToast();
|
||||
@@ -30,6 +30,7 @@ export default function AdminPage() {
|
||||
});
|
||||
const [savingSettings, setSavingSettings] = useState(false);
|
||||
const [isUploadingBanner, setIsUploadingBanner] = useState(false);
|
||||
const [isCheckingBannerStorage, setIsCheckingBannerStorage] = useState(false);
|
||||
const [bannerUploadError, setBannerUploadError] = useState<string | null>(null);
|
||||
const [showBannerSessionPrompt, setShowBannerSessionPrompt] = useState(false);
|
||||
const [showBannerStorageConfiguration, setShowBannerStorageConfiguration] = useState(false);
|
||||
@@ -41,6 +42,7 @@ export default function AdminPage() {
|
||||
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
||||
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
|
||||
const bannerInputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
fetch('/api/admin/me')
|
||||
.then((res) => res.json())
|
||||
@@ -145,6 +147,23 @@ export default function AdminPage() {
|
||||
await uploadBannerFile(file);
|
||||
};
|
||||
|
||||
const handleChooseBanner = async () => {
|
||||
setBannerUploadError(null);
|
||||
setIsCheckingBannerStorage(true);
|
||||
try {
|
||||
const provider = await getStorageProvider();
|
||||
if (!provider) {
|
||||
setShowBannerStorageConfiguration(true);
|
||||
return;
|
||||
}
|
||||
bannerInputRef.current?.click();
|
||||
} catch (error) {
|
||||
setBannerUploadError(error instanceof Error ? error.message : 'Unable to check media storage');
|
||||
} finally {
|
||||
setIsCheckingBannerStorage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBannerSessionSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -418,16 +437,17 @@ export default function AdminPage() {
|
||||
<div>
|
||||
<label style={{ fontSize: '13px', fontWeight: 500, marginBottom: '4px', display: 'block' }}>Banner image</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<label className="btn btn-ghost btn-sm">
|
||||
{isUploadingBanner ? 'Uploading...' : 'Upload banner'}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleBannerUpload}
|
||||
disabled={isUploadingBanner}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
<button className="btn btn-ghost btn-sm" type="button" onClick={handleChooseBanner} disabled={isUploadingBanner || isCheckingBannerStorage}>
|
||||
{isUploadingBanner ? 'Uploading...' : isCheckingBannerStorage ? 'Checking storage…' : 'Upload banner'}
|
||||
</button>
|
||||
<input
|
||||
ref={bannerInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleBannerUpload}
|
||||
disabled={isUploadingBanner}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{bannerUploadError && (
|
||||
<span style={{ fontSize: '12px', color: 'var(--danger)' }}>{bannerUploadError}</span>
|
||||
)}
|
||||
@@ -608,7 +628,12 @@ export default function AdminPage() {
|
||||
open={showBannerStorageConfiguration}
|
||||
onConfigured={async () => {
|
||||
setShowBannerStorageConfiguration(false);
|
||||
if (pendingBannerFile) await uploadBannerFile(pendingBannerFile, false);
|
||||
if (pendingBannerFile) {
|
||||
await uploadBannerFile(pendingBannerFile, false);
|
||||
return;
|
||||
}
|
||||
showToast('Stuffbox connected. Choose a banner to continue.', 'success');
|
||||
bannerInputRef.current?.click();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowBannerStorageConfiguration(false);
|
||||
|
||||
@@ -3,20 +3,10 @@ import { requireAuth } from '@/lib/auth';
|
||||
import { exchangeAuthorizationCode, StuffboxApiError } from '@/lib/stuffbox/client';
|
||||
import { consumeStuffboxConnectionState } from '@/lib/stuffbox/connection-state';
|
||||
import { saveStuffboxTokens } from '@/lib/stuffbox/tokens';
|
||||
import { renderStuffboxPopupResponse } from '@/lib/stuffbox/popup-response';
|
||||
|
||||
function popupResponse(origin: string, success: boolean, message: string): NextResponse {
|
||||
const safeJson = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c');
|
||||
const payload = safeJson({ type: 'synapsis:stuffbox', success, message });
|
||||
const targetOrigin = safeJson(origin);
|
||||
const fallback = success ? '/settings/storage?stuffbox=connected' : '/settings/storage?stuffbox=error';
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Stuffbox</title></head><body>
|
||||
<p>${success ? 'Stuffbox connected. You can close this window.' : 'Stuffbox could not be connected.'}</p>
|
||||
<script>
|
||||
if (window.opener) { window.opener.postMessage(${payload}, ${targetOrigin}); window.close(); }
|
||||
else { window.location.replace(${safeJson(fallback)}); }
|
||||
</script>
|
||||
</body></html>`;
|
||||
return new NextResponse(html, {
|
||||
return new NextResponse(renderStuffboxPopupResponse(origin, success, message), {
|
||||
status: success ? 200 : 400,
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
||||
});
|
||||
|
||||
+4
-4
@@ -717,6 +717,9 @@ a.btn-primary:visited {
|
||||
color: var(--foreground-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.compose-media-button:hover {
|
||||
@@ -725,10 +728,7 @@ a.btn-primary:visited {
|
||||
}
|
||||
|
||||
.compose-media-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.compose-media-error {
|
||||
|
||||
+47
-14
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import AutoTextarea from '@/components/AutoTextarea';
|
||||
import { Post, Attachment } from '@/lib/types';
|
||||
import { ImageIcon, AlertTriangle, Film } from 'lucide-react';
|
||||
@@ -8,7 +8,7 @@ import { VideoEmbed } from '@/components/VideoEmbed';
|
||||
import { useFormattedHandle } from '@/lib/utils/handle';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt';
|
||||
import { MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
|
||||
interface MediaAttachment extends Attachment {
|
||||
mimeType?: string;
|
||||
@@ -29,7 +29,9 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
const [isPosting, setIsPosting] = useState(false);
|
||||
const [attachments, setAttachments] = useState<MediaAttachment[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isCheckingStorage, setIsCheckingStorage] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [storageNotice, setStorageNotice] = useState<string | null>(null);
|
||||
const [pendingStorageFiles, setPendingStorageFiles] = useState<File[]>([]);
|
||||
const [showStorageConfiguration, setShowStorageConfiguration] = useState(false);
|
||||
const [linkPreview, setLinkPreview] = useState<any>(null);
|
||||
@@ -38,6 +40,7 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
const [isNsfw, setIsNsfw] = useState(false);
|
||||
const [canPostNsfw, setCanPostNsfw] = useState(false);
|
||||
const [isNsfwNode, setIsNsfwNode] = useState(false);
|
||||
const mediaInputRef = useRef<HTMLInputElement>(null);
|
||||
const maxLength = 600;
|
||||
const remaining = maxLength - content.length;
|
||||
const previewMedia = linkPreview?.media?.length
|
||||
@@ -164,6 +167,24 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
await uploadMediaFiles(files);
|
||||
};
|
||||
|
||||
const handleAddMedia = async () => {
|
||||
setUploadError(null);
|
||||
setStorageNotice(null);
|
||||
setIsCheckingStorage(true);
|
||||
try {
|
||||
const provider = await getStorageProvider();
|
||||
if (!provider) {
|
||||
setShowStorageConfiguration(true);
|
||||
return;
|
||||
}
|
||||
mediaInputRef.current?.click();
|
||||
} catch (error) {
|
||||
setUploadError(error instanceof Error ? error.message : 'Unable to check media storage');
|
||||
} finally {
|
||||
setIsCheckingStorage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAttachment = (id: string) => {
|
||||
setAttachments((prev) => prev.filter((item) => item.id !== id));
|
||||
};
|
||||
@@ -176,7 +197,12 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
setShowStorageConfiguration(false);
|
||||
const files = pendingStorageFiles;
|
||||
setPendingStorageFiles([]);
|
||||
await uploadMediaFiles(files);
|
||||
if (files.length > 0) {
|
||||
await uploadMediaFiles(files);
|
||||
return;
|
||||
}
|
||||
setStorageNotice('Stuffbox connected. Choose your media to continue.');
|
||||
mediaInputRef.current?.click();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowStorageConfiguration(false);
|
||||
@@ -274,6 +300,9 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
{uploadError && (
|
||||
<div className="compose-media-error">{uploadError}</div>
|
||||
)}
|
||||
{storageNotice && (
|
||||
<div style={{ color: 'var(--success)', fontSize: '13px', marginTop: '8px' }}>{storageNotice}</div>
|
||||
)}
|
||||
<div className="compose-footer">
|
||||
<div className="compose-footer-left">
|
||||
<span className={`compose-counter ${remaining < 50 ? (remaining < 0 ? 'error' : 'warning') : ''}`}>
|
||||
@@ -292,20 +321,24 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
)}
|
||||
</div>
|
||||
<div className="compose-actions">
|
||||
<label
|
||||
<button
|
||||
type="button"
|
||||
className="compose-media-button"
|
||||
title="Add media"
|
||||
onClick={handleAddMedia}
|
||||
disabled={isUploading || isCheckingStorage || attachments.length >= 4}
|
||||
>
|
||||
{isUploading ? '...' : <ImageIcon size={20} />}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,video/mp4,video/webm,video/quicktime"
|
||||
multiple
|
||||
onChange={handleMediaSelect}
|
||||
disabled={isUploading || attachments.length >= 4}
|
||||
className="compose-media-input"
|
||||
/>
|
||||
</label>
|
||||
{isUploading || isCheckingStorage ? '...' : <ImageIcon size={20} />}
|
||||
</button>
|
||||
<input
|
||||
ref={mediaInputRef}
|
||||
type="file"
|
||||
accept="image/*,video/mp4,video/webm,video/quicktime"
|
||||
multiple
|
||||
onChange={handleMediaSelect}
|
||||
disabled={isUploading || attachments.length >= 4}
|
||||
className="compose-media-input"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -60,21 +60,52 @@ export function StorageConfigurationPrompt({ open, onConfigured, onCancel, varia
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !data.authorizationUrl) throw new Error(data.error || 'Unable to connect Stuffbox');
|
||||
const result = new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let channel: BroadcastChannel | null = null;
|
||||
const timeout = window.setTimeout(() => {
|
||||
window.removeEventListener('message', receive);
|
||||
reject(new Error('Stuffbox connection timed out.'));
|
||||
finish({ type: 'synapsis:stuffbox', success: false, message: 'Stuffbox connection timed out.' });
|
||||
}, 10 * 60_000);
|
||||
const popupClosed = window.setInterval(() => {
|
||||
if (popup.closed) finish({ type: 'synapsis:stuffbox', success: false, message: 'The Stuffbox window was closed before connecting.' });
|
||||
}, 500);
|
||||
|
||||
function cleanup() {
|
||||
window.clearTimeout(timeout);
|
||||
window.clearInterval(popupClosed);
|
||||
window.removeEventListener('message', receive);
|
||||
window.removeEventListener('storage', receiveStorage);
|
||||
channel?.close();
|
||||
}
|
||||
|
||||
function finish(data: { type?: string; success?: boolean; message?: string }) {
|
||||
if (settled || data.type !== 'synapsis:stuffbox') return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
window.focus();
|
||||
if (data.success) resolve();
|
||||
else reject(new Error(data.message || 'Stuffbox could not be connected.'));
|
||||
}
|
||||
|
||||
function receive(event: MessageEvent) {
|
||||
if (event.origin !== window.location.origin || event.data?.type !== 'synapsis:stuffbox') return;
|
||||
window.clearTimeout(timeout);
|
||||
window.removeEventListener('message', receive);
|
||||
if (event.data.success) resolve();
|
||||
else reject(new Error(event.data.message || 'Stuffbox could not be connected.'));
|
||||
finish(event.data);
|
||||
}
|
||||
|
||||
function receiveStorage(event: StorageEvent) {
|
||||
if (event.key !== 'synapsis:stuffbox:result' || !event.newValue) return;
|
||||
try { finish(JSON.parse(event.newValue)); } catch { /* Ignore unrelated storage values. */ }
|
||||
}
|
||||
|
||||
window.addEventListener('message', receive);
|
||||
window.addEventListener('storage', receiveStorage);
|
||||
if ('BroadcastChannel' in window) {
|
||||
channel = new BroadcastChannel('synapsis:stuffbox');
|
||||
channel.onmessage = (event) => finish(event.data);
|
||||
}
|
||||
});
|
||||
popup.location.href = data.authorizationUrl;
|
||||
await result;
|
||||
if (!popup.closed) popup.close();
|
||||
await onConfigured();
|
||||
} catch (connectError) {
|
||||
if (!popup.closed) popup.close();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
|
||||
import { StorageSessionPrompt } from '@/components/StorageSessionPrompt';
|
||||
import { StorageConfigurationPrompt } from '@/components/StorageConfigurationPrompt';
|
||||
import { refreshStorageSession } from '@/lib/storage/client';
|
||||
import { MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
|
||||
|
||||
interface UserStorageImageUploadProps {
|
||||
label: string;
|
||||
@@ -29,6 +29,8 @@ export function UserStorageImageUpload({
|
||||
}: UserStorageImageUploadProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isCheckingStorage, setIsCheckingStorage] = useState(false);
|
||||
const [storageNotice, setStorageNotice] = useState('');
|
||||
const [showSessionPrompt, setShowSessionPrompt] = useState(false);
|
||||
const [showConfigurationPrompt, setShowConfigurationPrompt] = useState(false);
|
||||
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||
@@ -79,6 +81,24 @@ export function UserStorageImageUpload({
|
||||
await uploadFile(file);
|
||||
};
|
||||
|
||||
const handleChooseFile = async () => {
|
||||
setIsCheckingStorage(true);
|
||||
setStorageNotice('');
|
||||
onError?.('');
|
||||
try {
|
||||
const provider = await getStorageProvider();
|
||||
if (!provider) {
|
||||
setShowConfigurationPrompt(true);
|
||||
return;
|
||||
}
|
||||
inputRef.current?.click();
|
||||
} catch (error) {
|
||||
onError?.(error instanceof Error ? error.message : 'Unable to check media storage');
|
||||
} finally {
|
||||
setIsCheckingStorage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePromptSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -120,17 +140,17 @@ export function UserStorageImageUpload({
|
||||
{label}
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<label className="btn btn-ghost btn-sm" style={{ cursor: isUploading ? 'default' : 'pointer' }}>
|
||||
{isUploading ? 'Uploading...' : 'Choose File'}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
<button className="btn btn-ghost btn-sm" type="button" onClick={handleChooseFile} disabled={isUploading || isCheckingStorage}>
|
||||
{isUploading ? 'Uploading...' : isCheckingStorage ? 'Checking storage…' : 'Choose File'}
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{value && (
|
||||
<div
|
||||
@@ -168,6 +188,11 @@ export function UserStorageImageUpload({
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
{storageNotice && (
|
||||
<p style={{ fontSize: '13px', color: 'var(--success)', marginTop: '6px' }}>
|
||||
{storageNotice}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<StorageSessionPrompt
|
||||
@@ -186,7 +211,12 @@ export function UserStorageImageUpload({
|
||||
open={showConfigurationPrompt}
|
||||
onConfigured={async () => {
|
||||
setShowConfigurationPrompt(false);
|
||||
if (pendingFile) await uploadFile(pendingFile, false);
|
||||
if (pendingFile) {
|
||||
await uploadFile(pendingFile, false);
|
||||
return;
|
||||
}
|
||||
setStorageNotice('Stuffbox connected. Choose your file to continue.');
|
||||
inputRef.current?.click();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowConfigurationPrompt(false);
|
||||
|
||||
@@ -28,10 +28,23 @@ interface JsonResponse extends Record<string, unknown> {
|
||||
provider?: string | null;
|
||||
}
|
||||
|
||||
export type StorageProvider = 'stuffbox' | 's3' | null;
|
||||
|
||||
async function json(response: Response): Promise<JsonResponse> {
|
||||
return response.json().catch(() => ({})) as Promise<JsonResponse>;
|
||||
}
|
||||
|
||||
export async function getStorageProvider(): Promise<StorageProvider> {
|
||||
const response = await fetch('/api/storage/configuration', { cache: 'no-store' });
|
||||
const configuration = await json(response);
|
||||
if (!response.ok) {
|
||||
throw new MediaUploadError(configuration.error || 'Unable to load storage configuration', undefined, response.status);
|
||||
}
|
||||
return configuration.provider === 'stuffbox' || configuration.provider === 's3'
|
||||
? configuration.provider
|
||||
: null;
|
||||
}
|
||||
|
||||
function directPut(
|
||||
uploadUrl: string,
|
||||
file: File,
|
||||
@@ -105,13 +118,8 @@ export async function uploadMediaFile(
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<UploadedMedia> {
|
||||
const configurationResponse = await fetch('/api/storage/configuration', { cache: 'no-store' });
|
||||
const configuration = await json(configurationResponse);
|
||||
if (!configurationResponse.ok) {
|
||||
throw new MediaUploadError(configuration.error || 'Unable to load storage configuration', undefined, configurationResponse.status);
|
||||
}
|
||||
|
||||
if (configuration.provider === 'stuffbox') return uploadToStuffbox(file, onProgress);
|
||||
if (configuration.provider === 's3') return uploadToS3(file);
|
||||
const provider = await getStorageProvider();
|
||||
if (provider === 'stuffbox') return uploadToStuffbox(file, onProgress);
|
||||
if (provider === 's3') return uploadToS3(file);
|
||||
throw new MediaUploadError('Connect media storage before uploading.', 'STORAGE_NOT_CONFIGURED', 409);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { renderStuffboxPopupResponse } from './popup-response';
|
||||
|
||||
describe('renderStuffboxPopupResponse', () => {
|
||||
it('notifies the main tab without navigating the popup into Synapsis', () => {
|
||||
const html = renderStuffboxPopupResponse('https://synapsis.example', true, 'Stuffbox connected.');
|
||||
|
||||
expect(html).toContain("new BroadcastChannel('synapsis:stuffbox')");
|
||||
expect(html).toContain("localStorage.setItem('synapsis:stuffbox:result'");
|
||||
expect(html).toContain('window.close()');
|
||||
expect(html).not.toContain('window.location.replace');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
export function renderStuffboxPopupResponse(origin: string, success: boolean, message: string): string {
|
||||
const safeJson = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c');
|
||||
const payload = safeJson({ type: 'synapsis:stuffbox', success, message });
|
||||
const targetOrigin = safeJson(origin);
|
||||
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><title>Stuffbox</title></head><body>
|
||||
<p>${success ? 'Stuffbox connected. Returning to Synapsis…' : 'Stuffbox could not be connected. Returning to Synapsis…'}</p>
|
||||
<script>
|
||||
const payload = ${payload};
|
||||
try {
|
||||
if (window.opener && !window.opener.closed) window.opener.postMessage(payload, ${targetOrigin});
|
||||
} catch {}
|
||||
try {
|
||||
const channel = new BroadcastChannel('synapsis:stuffbox');
|
||||
channel.postMessage(payload);
|
||||
channel.close();
|
||||
} catch {}
|
||||
try {
|
||||
localStorage.setItem('synapsis:stuffbox:result', JSON.stringify({ ...payload, sentAt: Date.now() }));
|
||||
} catch {}
|
||||
window.setTimeout(() => window.close(), 75);
|
||||
</script>
|
||||
</body></html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user