smaller fixes

This commit is contained in:
Alexander Medvedev 2026-08-30 11:41:02 +02:00
parent 8e395aa8bb
commit db46670a99
4 changed files with 399 additions and 334 deletions

View File

@ -7,7 +7,8 @@
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
padding: 1.25rem;
overflow-y: auto;
}
.dev-onboarding-embedded {
@ -22,10 +23,27 @@
border-radius: 16px;
width: 100%;
max-width: 580px;
padding: 2.25rem;
max-height: calc(100vh - 2.5rem);
overflow-y: auto;
padding: 1.75rem;
position: relative;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
color: var(--text-color, #e2e8f0);
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
}
.dev-modal-container::-webkit-scrollbar {
width: 6px;
}
.dev-modal-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
.dev-modal-container::-webkit-scrollbar-track {
background: transparent;
}
.dev-modal-close {
@ -372,46 +390,47 @@
.dev-monetization-box {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1.25rem;
border-radius: 10px;
padding: 0.85rem 1rem;
}
.dev-monetization-header {
display: flex;
gap: 0.75rem;
align-items: flex-start;
gap: 0.65rem;
align-items: center;
color: #f97316;
}
.dev-monetization-header h4 {
margin: 0 0 0.2rem 0;
margin: 0 0 0.15rem 0;
color: #fff;
font-size: 1rem;
font-size: 0.95rem;
}
.dev-monetization-header p {
margin: 0;
font-size: 0.85rem;
font-size: 0.8rem;
color: var(--text-muted, #94a3b8);
}
.dev-check-list {
list-style: none;
padding: 0;
margin: 1rem 0 0 0;
margin: 0.5rem 0 0 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.35rem;
}
.dev-check-list li {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
gap: 0.45rem;
font-size: 0.8rem;
color: #e2e8f0;
}
.dev-check-list svg {
color: #22c55e;
flex-shrink: 0;
}

View File

@ -25,7 +25,7 @@ export const DeveloperOnboardingModal: React.FC<DeveloperOnboardingModalProps> =
embedded = false,
}) => {
const { t } = useTranslation();
const { user, login } = useAuth();
const { user, login, refreshUser } = useAuth();
const [step, setStep] = useState<number>(1);
const [entityType, setEntityType] = useState<'individual' | 'organization'>('individual');
const [orgType, setOrgType] = useState<string>('');
@ -582,117 +582,93 @@ export const DeveloperOnboardingModal: React.FC<DeveloperOnboardingModalProps> =
{/* ── STEP 5: Finish & Confirmation ── */}
{step === 5 && (
<div className="dev-step-content">
{!isSellingPaid ? (
<>
<div className="dev-monetization-box">
<div className="dev-monetization-header">
<Gift size={22} color="#10b981" />
<div>
<h4>Ready to Publish Free Plugins!</h4>
<p>Your developer profile is configured for free &amp; open-source uploads.</p>
</div>
</div>
<ul className="dev-check-list">
<li><CheckCircle2 size={16} color="#10b981" /> Publish unlimited free plugins ($0 fee)</li>
<li><CheckCircle2 size={16} color="#10b981" /> Full analytics and review dashboard</li>
<li><CheckCircle2 size={16} color="#10b981" /> Option to add address &amp; sell paid plugins anytime later</li>
</ul>
</div>
{/* ── Security Requirement for Developers ── */}
{!hasSecureAuth && (
<div style={{ marginTop: '1.25rem' }}>
<SecuritySetupCards
title="Developer Security Requirement"
description="To publish plugins and protect developer payouts, you must set up a Passkey or Two-Factor Authentication before completing onboarding."
isRequired={true}
onComplete={() => {}}
/>
</div>
<div className="dev-monetization-box" style={{ marginBottom: '0.85rem' }}>
<div className="dev-monetization-header">
{!isSellingPaid ? (
<Gift size={20} color="#10b981" />
) : (
<CreditCard size={20} color="#f97316" />
)}
{/* ── Mandatory Legal Consent ── */}
<div style={{
marginTop: '1.25rem',
padding: '0.85rem 1rem',
background: 'rgba(255, 183, 77, 0.06)',
border: '1px solid rgba(255, 183, 77, 0.25)',
borderRadius: 8,
}}>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '0.65rem', cursor: 'pointer', fontSize: '0.78rem', color: '#cbd5e1', lineHeight: 1.5 }}>
<input
type="checkbox"
checked={acceptedTerms}
onChange={e => setAcceptedTerms(e.target.checked)}
style={{ marginTop: '0.15rem', accentColor: '#f97316', width: 16, height: 16, flexShrink: 0, cursor: 'pointer' }}
/>
<span>
I agree to the <a href="/developer-terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline', fontWeight: 600 }}>Developer Distribution Agreement</a>, <a href="/terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Terms of Service</a>, and <a href="/guidelines" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Review Guidelines</a>. I grant Pumpkin Marketplace the worldwide license to distribute my plugins, acknowledge platform moderation and takedown authority, and confirm my plugins comply with Mojang's Minecraft EULA.
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.4rem' }}>
<h4 style={{ margin: 0, fontSize: '0.95rem', fontWeight: 600, color: '#fff' }}>
{!isSellingPaid ? 'Free Creator Profile' : 'Commercial Seller Profile'}
</h4>
<span style={{
fontSize: '0.72rem',
fontWeight: 600,
padding: '0.15rem 0.5rem',
borderRadius: 999,
background: !isSellingPaid ? 'rgba(16, 185, 129, 0.15)' : 'rgba(249, 115, 22, 0.15)',
color: !isSellingPaid ? '#34d399' : '#f97316',
border: `1px solid ${!isSellingPaid ? 'rgba(16, 185, 129, 0.3)' : 'rgba(249, 115, 22, 0.3)'}`,
}}>
{!isSellingPaid ? 'Free Uploads' : 'Paid + Stripe'}
</span>
</label>
</div>
<p style={{ margin: '0.2rem 0 0', fontSize: '0.78rem', color: 'var(--text-muted, #94a3b8)' }}>
Display: <strong style={{ color: '#fff' }}>{displayName}</strong> • Email: <strong style={{ color: '#fff' }}>{supportEmail}</strong>
</p>
</div>
</div>
</div>
<div className="dev-btn-row-stacked" style={{ marginTop: '1.25rem' }}>
<button
className="dev-btn dev-btn-primary"
disabled={loading || !acceptedTerms || !hasSecureAuth}
onClick={() => handleCompleteOnboarding(false)}
>
{loading ? 'Completing registration...' : t('developer.onboarding.btn_complete')}
</button>
</div>
</>
{/* ── Security Requirement for Developers ── */}
{hasSecureAuth ? (
<div style={{
padding: '0.65rem 0.85rem',
background: 'rgba(34, 197, 94, 0.08)',
border: '1px solid rgba(34, 197, 94, 0.25)',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
gap: '0.55rem',
color: '#4ade80',
fontSize: '0.82rem',
marginBottom: '0.85rem',
}}>
<CheckCircle2 size={16} style={{ flexShrink: 0 }} />
<span><strong>Security Verified:</strong> 2FA / Passkey protection is active on your account.</span>
</div>
) : (
<>
<div className="dev-monetization-box">
<div className="dev-monetization-header">
<CreditCard size={22} color="#f97316" />
<div>
<h4>{t('developer.onboarding.intent_paid')}</h4>
<p>{t('developer.onboarding.intent_paid_desc')}</p>
</div>
</div>
<ul className="dev-check-list">
<li><CheckCircle2 size={16} color="#10b981" /> Automated payouts directly to your bank account</li>
<li><CheckCircle2 size={16} color="#10b981" /> Set custom pricing, discounts, and coupons</li>
<li><CheckCircle2 size={16} color="#10b981" /> Automated license key generation for buyers</li>
</ul>
</div>
<div style={{ marginBottom: '0.85rem' }}>
<SecuritySetupCards
compact={true}
title="Developer Security Requirement"
description="To publish plugins, set up Passkey or 2FA Authenticator before finishing."
isRequired={true}
onComplete={async () => {
if (refreshUser) await refreshUser();
}}
/>
</div>
)}
{/* ── Security Requirement for Developers ── */}
{!hasSecureAuth && (
<div style={{ marginTop: '1.25rem' }}>
<SecuritySetupCards
title="Developer Security Requirement"
description="To publish plugins and protect developer payouts, you must set up a Passkey or Two-Factor Authentication before completing onboarding."
isRequired={true}
onComplete={() => {}}
/>
</div>
)}
{/* ── Mandatory Legal Consent ── */}
<div style={{
padding: '0.75rem 0.85rem',
background: 'rgba(255, 183, 77, 0.05)',
border: '1px solid rgba(255, 183, 77, 0.2)',
borderRadius: 8,
marginBottom: '1rem',
}}>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '0.55rem', cursor: 'pointer', fontSize: '0.76rem', color: '#cbd5e1', lineHeight: 1.45 }}>
<input
type="checkbox"
checked={acceptedTerms}
onChange={e => setAcceptedTerms(e.target.checked)}
style={{ marginTop: '0.15rem', accentColor: '#f97316', width: 15, height: 15, flexShrink: 0, cursor: 'pointer' }}
/>
<span>
I agree to the <a href="/developer-terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline', fontWeight: 600 }}>Developer Distribution Agreement</a>, <a href="/terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Terms of Service</a>, and <a href="/guidelines" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Review Guidelines</a>. I grant Pumpkin Market license to distribute my plugins and confirm compliance with Mojang's Minecraft EULA.
</span>
</label>
</div>
{/* ── Mandatory Legal Consent ── */}
<div style={{
marginTop: '1.25rem',
padding: '0.85rem 1rem',
background: 'rgba(255, 183, 77, 0.06)',
border: '1px solid rgba(255, 183, 77, 0.25)',
borderRadius: 8,
}}>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '0.65rem', cursor: 'pointer', fontSize: '0.78rem', color: '#cbd5e1', lineHeight: 1.5 }}>
<input
type="checkbox"
checked={acceptedTerms}
onChange={e => setAcceptedTerms(e.target.checked)}
style={{ marginTop: '0.15rem', accentColor: '#f97316', width: 16, height: 16, flexShrink: 0, cursor: 'pointer' }}
/>
<span>
I agree to the <a href="/developer-terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline', fontWeight: 600 }}>Developer Distribution Agreement</a>, <a href="/terms" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Terms of Service</a>, and <a href="/guidelines" target="_blank" rel="noopener noreferrer" style={{ color: '#ffb74d', textDecoration: 'underline' }}>Review Guidelines</a>. I grant Pumpkin Marketplace the worldwide license to distribute my plugins, acknowledge platform moderation and takedown authority, and confirm my plugins comply with Mojang's Minecraft EULA.
</span>
</label>
</div>
<div className="dev-btn-row-stacked" style={{ marginTop: '1.25rem' }}>
{/* Actions */}
<div className="dev-btn-row-stacked">
{isSellingPaid ? (
<>
<button
className="dev-btn dev-btn-stripe"
disabled={loading || !acceptedTerms || !hasSecureAuth}
@ -707,12 +683,22 @@ export const DeveloperOnboardingModal: React.FC<DeveloperOnboardingModalProps> =
>
{t('developer.onboarding.btn_complete')}
</button>
</div>
</>
)}
</>
) : (
<button
className="dev-btn dev-btn-primary"
disabled={loading || !acceptedTerms || !hasSecureAuth}
onClick={() => handleCompleteOnboarding(false)}
>
{loading ? 'Completing registration...' : t('developer.onboarding.btn_complete')}
</button>
)}
</div>
<div className="dev-btn-row" style={{ marginTop: '1rem' }}>
<button className="dev-btn dev-btn-secondary" onClick={prevStep}>{t('developer.onboarding.btn_back')}</button>
<div className="dev-btn-row" style={{ marginTop: '0.85rem' }}>
<button className="dev-btn dev-btn-secondary" onClick={prevStep}>
{t('developer.onboarding.btn_back')}
</button>
</div>
</div>
)}

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useAuth } from '../App';
import api from '../api';
import { startRegistration } from '@simplewebauthn/browser';
import { QRCodeSVG } from 'qrcode.react';
import { RecoveryCodesModal } from './RecoveryCodesModal';
import {
Fingerprint,
@ -24,6 +25,7 @@ interface SecuritySetupCardsProps {
onSkip?: () => void;
canSkip?: boolean;
isRequired?: boolean;
compact?: boolean;
}
export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
@ -33,6 +35,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
onSkip,
canSkip = false,
isRequired = false,
compact = false,
}) => {
const { login, user, refreshUser } = useAuth();
@ -50,7 +53,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
// TOTP State
const [totpLoading, setTotpLoading] = useState(false);
const [totpQr, setTotpQr] = useState<string | null>(null);
const [totpUri, setTotpUri] = useState<string | null>(null);
const [totpSecret, setTotpSecret] = useState<string | null>(null);
const [totpCode, setTotpCode] = useState('');
const [totpVerifying, setTotpVerifying] = useState(false);
@ -63,7 +66,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
setExpandedBox(null);
} else {
setExpandedBox(box);
if (box === 'totp' && !totpQr && !totpSuccess) {
if (box === 'totp' && !totpUri && !totpSuccess) {
loadTotpSetup();
}
}
@ -74,7 +77,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
setTotpLoading(true);
setTotpError(null);
const res = await api.post('/user/2fa/setup');
setTotpQr(res.data.qrCode);
setTotpUri(res.data.uri);
setTotpSecret(res.data.secret);
} catch (err: any) {
setTotpError(err.response?.data?.error || 'Failed to initialize 2FA setup.');
@ -172,7 +175,14 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
const hasSecureAuth = Boolean(user?.totp_enabled || (user?.passkey_count && user.passkey_count > 0) || user?.has_passkey);
return (
<div style={{
<div style={compact ? {
width: '100%',
padding: '0.85rem',
background: 'rgba(255,255,255,0.02)',
borderRadius: 12,
border: '1px solid var(--mp-border, rgba(255,255,255,0.08))',
boxSizing: 'border-box',
} : {
maxWidth: 680,
width: '100%',
margin: '0 auto',
@ -181,67 +191,112 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
borderRadius: 20,
border: '1px solid var(--mp-border, rgba(255,255,255,0.08))',
boxShadow: '0 20px 40px -15px rgba(0,0,0,0.5)',
boxSizing: 'border-box',
}}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{
width: 56,
height: 56,
borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(234, 88, 12, 0.05))',
border: '1px solid rgba(249, 115, 22, 0.3)',
color: 'var(--mp-accent, #f97316)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1rem',
}}>
<Shield size={28} />
{compact ? (
<div style={{ marginBottom: '0.85rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.55rem' }}>
<div style={{
width: 28,
height: 28,
borderRadius: 6,
background: 'rgba(249, 115, 22, 0.15)',
color: 'var(--mp-accent, #f97316)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<Shield size={16} />
</div>
<div>
<h4 style={{ margin: 0, fontSize: '0.9rem', fontWeight: 700, color: 'var(--mp-text, #f8fafc)' }}>
{title}
</h4>
<p style={{ margin: 0, fontSize: '0.76rem', color: 'var(--mp-muted, #94a3b8)', lineHeight: 1.3 }}>
{description}
</p>
</div>
</div>
{isRequired && (
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.35rem',
padding: '0.15rem 0.5rem',
background: 'rgba(239, 68, 68, 0.12)',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: 9999,
color: '#f87171',
fontSize: '0.72rem',
fontWeight: 600,
}}>
<AlertCircle size={12} /> Required
</span>
)}
</div>
<h2 style={{
fontSize: '1.6rem',
fontWeight: 700,
margin: '0 0 0.5rem 0',
color: 'var(--mp-text, #f8fafc)',
letterSpacing: '-0.02em',
}}>
{title}
</h2>
<p style={{
color: 'var(--mp-muted, #94a3b8)',
fontSize: '0.95rem',
lineHeight: 1.5,
maxWidth: 540,
margin: '0 auto',
}}>
{description}
</p>
{isRequired && (
) : (
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{
marginTop: '1rem',
width: 56,
height: 56,
borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(234, 88, 12, 0.05))',
border: '1px solid rgba(249, 115, 22, 0.3)',
color: 'var(--mp-accent, #f97316)',
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.85rem',
background: 'rgba(239, 68, 68, 0.12)',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: 9999,
color: '#f87171',
fontSize: '0.82rem',
fontWeight: 600,
justifyContent: 'center',
marginBottom: '1rem',
}}>
<AlertCircle size={14} /> Required for Plugin Publishing & Developer Verification
<Shield size={28} />
</div>
)}
</div>
<h2 style={{
fontSize: '1.6rem',
fontWeight: 700,
margin: '0 0 0.5rem 0',
color: 'var(--mp-text, #f8fafc)',
letterSpacing: '-0.02em',
}}>
{title}
</h2>
<p style={{
color: 'var(--mp-muted, #94a3b8)',
fontSize: '0.95rem',
lineHeight: 1.5,
maxWidth: 540,
margin: '0 auto',
}}>
{description}
</p>
{isRequired && (
<div style={{
marginTop: '1rem',
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.85rem',
background: 'rgba(239, 68, 68, 0.12)',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: 9999,
color: '#f87171',
fontSize: '0.82rem',
fontWeight: 600,
}}>
<AlertCircle size={14} /> Required for Plugin Publishing & Developer Verification
</div>
)}
</div>
)}
{/* Boxes Grid */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: compact ? '0.65rem' : '1rem' }}>
{/* ── Box 1: Passkey / Biometrics (Recommended) ── */}
<div style={{
border: expandedBox === 'passkey' ? '1px solid var(--mp-accent, #f97316)' : '1px solid var(--mp-border, rgba(255,255,255,0.08))',
borderRadius: 14,
borderRadius: compact ? 10 : 14,
background: expandedBox === 'passkey' ? 'rgba(249, 115, 22, 0.03)' : 'rgba(255,255,255,0.02)',
transition: 'all 0.2s ease',
overflow: 'hidden',
@ -251,7 +306,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
onClick={() => toggleBox('passkey')}
style={{
width: '100%',
padding: '1.25rem 1.5rem',
padding: compact ? '0.75rem 0.9rem' : '1.25rem 1.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
@ -261,11 +316,11 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
textAlign: 'left',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: compact ? '0.75rem' : '1rem' }}>
<div style={{
width: 44,
height: 44,
borderRadius: 12,
width: compact ? 36 : 44,
height: compact ? 36 : 44,
borderRadius: compact ? 8 : 12,
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.05))',
border: '1px solid rgba(34, 197, 94, 0.3)',
color: '#4ade80',
@ -274,55 +329,55 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
justifyContent: 'center',
flexShrink: 0,
}}>
<Fingerprint size={24} />
<Fingerprint size={compact ? 20 : 24} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', marginBottom: '0.2rem' }}>
<span style={{ fontWeight: 700, fontSize: '1.05rem', color: 'var(--mp-text, #f8fafc)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.15rem' }}>
<span style={{ fontWeight: 700, fontSize: compact ? '0.92rem' : '1.05rem', color: 'var(--mp-text, #f8fafc)' }}>
Passkey & Biometrics
</span>
<span style={{
fontSize: '0.72rem',
fontSize: '0.68rem',
fontWeight: 700,
padding: '0.15rem 0.55rem',
padding: '0.1rem 0.45rem',
borderRadius: 999,
background: 'rgba(34, 197, 94, 0.15)',
color: '#4ade80',
border: '1px solid rgba(34, 197, 94, 0.3)',
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem',
gap: '0.2rem',
}}>
<Sparkles size={11} /> Recommended
<Sparkles size={10} /> Recommended
</span>
</div>
<p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--mp-muted, #94a3b8)' }}>
<p style={{ margin: 0, fontSize: compact ? '0.75rem' : '0.85rem', color: 'var(--mp-muted, #94a3b8)' }}>
Fastest & most secure. Touch ID, Face ID, Windows Hello, or Security Key.
</p>
</div>
</div>
<div style={{ color: 'var(--mp-muted, #94a3b8)', marginLeft: '1rem' }}>
{expandedBox === 'passkey' ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
<div style={{ color: 'var(--mp-muted, #94a3b8)', marginLeft: '0.5rem' }}>
{expandedBox === 'passkey' ? <ChevronUp size={compact ? 18 : 20} /> : <ChevronDown size={compact ? 18 : 20} />}
</div>
</button>
{/* Passkey Expanded Content */}
{expandedBox === 'passkey' && (
<div style={{
padding: '0 1.5rem 1.5rem 1.5rem',
padding: compact ? '0 0.9rem 0.9rem 0.9rem' : '0 1.5rem 1.5rem 1.5rem',
borderTop: '1px solid rgba(255,255,255,0.06)',
paddingTop: '1.25rem',
paddingTop: compact ? '0.85rem' : '1.25rem',
}}>
<div style={{
padding: '0.9rem 1rem',
padding: compact ? '0.65rem 0.8rem' : '0.9rem 1rem',
background: 'rgba(255,255,255,0.03)',
borderRadius: 10,
marginBottom: '1.25rem',
fontSize: '0.85rem',
borderRadius: 8,
marginBottom: '1rem',
fontSize: compact ? '0.78rem' : '0.85rem',
color: 'var(--mp-muted, #94a3b8)',
lineHeight: 1.5,
lineHeight: 1.45,
}}>
<strong>How it works:</strong> Passkeys replace passwords and SMS codes with cryptographic keys stored securely in your device’s keychain (Apple Keychain, Google Password Manager, Windows Hello, 1Password, or YubiKey). They are 100% phishing-proof.
<strong>How it works:</strong> Passkeys replace passwords and SMS codes with cryptographic keys stored securely in your device's keychain. They are 100% phishing-proof.
</div>
{passkeyError && (
@ -330,15 +385,15 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.75rem 1rem',
padding: '0.65rem 0.85rem',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: 8,
color: '#f87171',
fontSize: '0.85rem',
marginBottom: '1rem',
fontSize: '0.82rem',
marginBottom: '0.85rem',
}}>
<AlertCircle size={16} />
<AlertCircle size={15} />
<span>{passkeyError}</span>
</div>
)}
@ -348,21 +403,21 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.85rem 1rem',
padding: '0.75rem 1rem',
background: 'rgba(34, 197, 94, 0.12)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: 8,
color: '#4ade80',
fontSize: '0.9rem',
fontSize: '0.85rem',
fontWeight: 600,
}}>
<Check size={18} />
<span>Passkey created successfully! Redirecting...</span>
<Check size={16} />
<span>Passkey created successfully!</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.85rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div>
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: 'var(--mp-text, #f8fafc)', marginBottom: '0.4rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: 'var(--mp-text, #f8fafc)', marginBottom: '0.35rem' }}>
Device / Passkey Name (Optional)
</label>
<input
@ -372,12 +427,12 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
placeholder="e.g. MacBook Pro Touch ID, Windows PC, or YubiKey"
style={{
width: '100%',
padding: '0.75rem 1rem',
padding: compact ? '0.6rem 0.85rem' : '0.75rem 1rem',
background: 'rgba(255,255,255,0.05)',
border: '1px solid var(--mp-border, rgba(255,255,255,0.12))',
borderRadius: 8,
color: '#fff',
fontSize: '0.9rem',
fontSize: compact ? '0.85rem' : '0.9rem',
boxSizing: 'border-box',
outline: 'none',
}}
@ -389,23 +444,24 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
disabled={passkeyLoading}
onClick={handleRegisterPasskey}
style={{
padding: '0.85rem',
padding: compact ? '0.65rem' : '0.85rem',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
fontSize: compact ? '0.85rem' : '0.9rem',
cursor: passkeyLoading ? 'not-allowed' : 'pointer',
borderRadius: 8,
}}
>
{passkeyLoading ? (
<>
<Loader2 size={18} className="animate-spin" /> Setting up Passkey...
<Loader2 size={16} className="animate-spin" /> Setting up Passkey...
</>
) : (
<>
<Fingerprint size={18} /> Create & Register Passkey
<Fingerprint size={16} /> Create & Register Passkey
</>
)}
</button>
@ -418,7 +474,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
{/* ── Box 2: Authenticator App (TOTP 2FA) ── */}
<div style={{
border: expandedBox === 'totp' ? '1px solid var(--mp-accent, #f97316)' : '1px solid var(--mp-border, rgba(255,255,255,0.08))',
borderRadius: 14,
borderRadius: compact ? 10 : 14,
background: expandedBox === 'totp' ? 'rgba(249, 115, 22, 0.03)' : 'rgba(255,255,255,0.02)',
transition: 'all 0.2s ease',
overflow: 'hidden',
@ -428,7 +484,7 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
onClick={() => toggleBox('totp')}
style={{
width: '100%',
padding: '1.25rem 1.5rem',
padding: compact ? '0.75rem 0.9rem' : '1.25rem 1.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
@ -438,11 +494,11 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
textAlign: 'left',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: compact ? '0.75rem' : '1rem' }}>
<div style={{
width: 44,
height: 44,
borderRadius: 12,
width: compact ? 36 : 44,
height: compact ? 36 : 44,
borderRadius: compact ? 8 : 12,
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.05))',
border: '1px solid rgba(59, 130, 246, 0.3)',
color: '#60a5fa',
@ -451,83 +507,86 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
justifyContent: 'center',
flexShrink: 0,
}}>
<Smartphone size={24} />
<Smartphone size={compact ? 20 : 24} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', marginBottom: '0.2rem' }}>
<span style={{ fontWeight: 700, fontSize: '1.05rem', color: 'var(--mp-text, #f8fafc)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.15rem' }}>
<span style={{ fontWeight: 700, fontSize: compact ? '0.92rem' : '1.05rem', color: 'var(--mp-text, #f8fafc)' }}>
Authenticator App (TOTP 2FA)
</span>
</div>
<p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--mp-muted, #94a3b8)' }}>
<p style={{ margin: 0, fontSize: compact ? '0.75rem' : '0.85rem', color: 'var(--mp-muted, #94a3b8)' }}>
Use Google Authenticator, Microsoft Authenticator, Authy, or 1Password.
</p>
</div>
</div>
<div style={{ color: 'var(--mp-muted, #94a3b8)', marginLeft: '1rem' }}>
{expandedBox === 'totp' ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
<div style={{ color: 'var(--mp-muted, #94a3b8)', marginLeft: '0.5rem' }}>
{expandedBox === 'totp' ? <ChevronUp size={compact ? 18 : 20} /> : <ChevronDown size={compact ? 18 : 20} />}
</div>
</button>
{/* TOTP Expanded Content */}
{expandedBox === 'totp' && (
<div style={{
padding: '0 1.5rem 1.5rem 1.5rem',
padding: compact ? '0 0.9rem 0.9rem 0.9rem' : '0 1.5rem 1.5rem 1.5rem',
borderTop: '1px solid rgba(255,255,255,0.06)',
paddingTop: '1.25rem',
paddingTop: compact ? '0.85rem' : '1.25rem',
}}>
{totpLoading ? (
<div style={{ textAlign: 'center', padding: '2rem', color: 'var(--mp-muted, #94a3b8)' }}>
<Loader2 size={24} className="animate-spin" style={{ margin: '0 auto 0.5rem auto' }} />
<p style={{ margin: 0 }}>Generating 2FA Secret & QR Code...</p>
<div style={{ textAlign: 'center', padding: '1.5rem', color: 'var(--mp-muted, #94a3b8)' }}>
<Loader2 size={22} className="animate-spin" style={{ margin: '0 auto 0.5rem auto' }} />
<p style={{ margin: 0, fontSize: '0.85rem' }}>Generating 2FA Secret & QR Code...</p>
</div>
) : totpSuccess ? (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.85rem 1rem',
padding: '0.75rem 1rem',
background: 'rgba(34, 197, 94, 0.12)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: 8,
color: '#4ade80',
fontSize: '0.9rem',
fontSize: '0.85rem',
fontWeight: 600,
}}>
<Check size={18} />
<span>Two-Factor Authentication enabled successfully! Redirecting...</span>
<Check size={16} />
<span>Two-Factor Authentication enabled successfully!</span>
</div>
) : (
<div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5rem', alignItems: 'center', marginBottom: '1.5rem' }}>
{totpQr && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: compact ? '1rem' : '1.5rem', alignItems: 'center', marginBottom: '1rem' }}>
{totpUri && (
<div style={{
padding: '0.5rem',
background: '#fff',
borderRadius: 12,
borderRadius: 10,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<img src={totpQr} alt="2FA QR Code" style={{ width: 140, height: 140, display: 'block' }} />
<QRCodeSVG value={totpUri} size={compact ? 115 : 135} />
</div>
)}
<div style={{ flex: 1, minWidth: 200 }}>
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.85rem', color: 'var(--mp-text, #f8fafc)', fontWeight: 600 }}>
1. Scan this QR Code with your Authenticator App
<div style={{ flex: 1, minWidth: compact ? 180 : 200 }}>
<p style={{ margin: '0 0 0.35rem 0', fontSize: compact ? '0.8rem' : '0.85rem', color: 'var(--mp-text, #f8fafc)', fontWeight: 600 }}>
1. Scan QR Code with Authenticator App
</p>
<p style={{ margin: '0 0 0.8rem 0', fontSize: '0.8rem', color: 'var(--mp-muted, #94a3b8)' }}>
Or manually copy the setup key below if you cannot scan the code:
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.75rem', color: 'var(--mp-muted, #94a3b8)', lineHeight: 1.35 }}>
Or manually copy the setup key below:
</p>
{totpSecret && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
gap: '0.4rem',
background: 'rgba(255,255,255,0.05)',
padding: '0.45rem 0.75rem',
borderRadius: 8,
padding: '0.35rem 0.6rem',
borderRadius: 6,
border: '1px solid rgba(255,255,255,0.1)',
}}>
<code style={{ fontSize: '0.85rem', color: '#f97316', flex: 1, letterSpacing: '0.05em' }}>
<code style={{ fontSize: compact ? '0.75rem' : '0.82rem', color: '#f97316', flex: 1, letterSpacing: '0.05em', wordBreak: 'break-all' }}>
{totpSecret}
</code>
<button
@ -541,19 +600,38 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
padding: '0.2rem',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
}}
title="Copy Secret"
>
{copiedSecret ? <Check size={16} /> : <Copy size={16} />}
{copiedSecret ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
)}
</div>
</div>
<form onSubmit={handleVerifyTotp} style={{ display: 'flex', flexDirection: 'column', gap: '0.85rem' }}>
{totpError && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.6rem 0.8rem',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: 8,
color: '#f87171',
fontSize: '0.82rem',
marginBottom: '0.75rem',
}}>
<AlertCircle size={15} />
<span>{totpError}</span>
</div>
)}
<form onSubmit={handleVerifyTotp} style={{ display: 'flex', flexDirection: 'column', gap: '0.65rem' }}>
<div>
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: 'var(--mp-text, #f8fafc)', marginBottom: '0.4rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: 'var(--mp-text, #f8fafc)', marginBottom: '0.35rem' }}>
2. Enter the 6-Digit Code from the App
</label>
<input
@ -564,21 +642,23 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
setTotpError(null);
setTotpCode(e.target.value.replace(/\D/g, ''));
}}
placeholder="123456"
placeholder="000 000"
style={{
width: '100%',
padding: '0.75rem 1rem',
padding: compact ? '0.6rem 0.85rem' : '0.75rem 1rem',
background: 'rgba(255,255,255,0.05)',
border: totpError ? '1px solid #ef4444' : '1px solid var(--mp-border, rgba(255,255,255,0.12))',
boxShadow: totpError ? '0 0 0 3px rgba(239, 68, 68, 0.2)' : undefined,
borderRadius: 8,
color: '#fff',
fontSize: '1.1rem',
fontSize: compact ? '1rem' : '1.1rem',
letterSpacing: '0.2em',
textAlign: 'center',
boxSizing: 'border-box',
outline: 'none',
}}
autoComplete="one-time-code"
inputMode="numeric"
/>
</div>
<button
@ -586,12 +666,13 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
className="mp-btn mp-btn-primary"
disabled={totpVerifying || (totpCode.trim().length !== 6 && !totpError)}
style={{
padding: '0.85rem',
padding: compact ? '0.65rem' : '0.85rem',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
fontSize: compact ? '0.85rem' : '0.9rem',
cursor: totpVerifying ? 'not-allowed' : 'pointer',
borderRadius: 8,
backgroundColor: totpError ? '#ef4444' : undefined,
@ -602,15 +683,15 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
>
{totpVerifying ? (
<>
<Loader2 size={18} className="animate-spin" /> Verifying Code...
<Loader2 size={16} className="animate-spin" /> Verifying Code...
</>
) : totpError ? (
<>
<AlertCircle size={18} /> {totpError} — Try Again
<AlertCircle size={16} /> Try Again
</>
) : (
<>
<ShieldCheck size={18} /> Verify & Enable 2FA
<ShieldCheck size={16} /> Verify & Enable 2FA
</>
)}
</button>
@ -623,58 +704,60 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
</div>
{/* Footer / Skip Actions */}
<div style={{
marginTop: '1.75rem',
display: 'flex',
alignItems: 'center',
justifyContent: canSkip ? 'space-between' : 'center',
paddingTop: '1rem',
borderTop: '1px solid rgba(255,255,255,0.06)',
}}>
{canSkip ? (
<>
<button
type="button"
onClick={onSkip}
style={{
background: 'none',
border: 'none',
color: 'var(--mp-muted, #94a3b8)',
fontSize: '0.9rem',
cursor: 'pointer',
padding: '0.5rem 0.75rem',
borderRadius: 6,
transition: 'color 0.2s',
}}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--mp-muted, #94a3b8)')}
>
Skip for now →
</button>
{hasSecureAuth && (
{!compact && (
<div style={{
marginTop: '1.75rem',
display: 'flex',
alignItems: 'center',
justifyContent: canSkip ? 'space-between' : 'center',
paddingTop: '1rem',
borderTop: '1px solid rgba(255,255,255,0.06)',
}}>
{canSkip ? (
<>
<button
type="button"
onClick={onSkip}
style={{
background: 'none',
border: 'none',
color: 'var(--mp-muted, #94a3b8)',
fontSize: '0.9rem',
cursor: 'pointer',
padding: '0.5rem 0.75rem',
borderRadius: 6,
transition: 'color 0.2s',
}}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--mp-muted, #94a3b8)')}
>
Skip for now →
</button>
{hasSecureAuth && (
<button
type="button"
className="mp-btn mp-btn-primary"
onClick={onComplete}
style={{ padding: '0.6rem 1.25rem', fontSize: '0.9rem', fontWeight: 600 }}
>
Continue →
</button>
)}
</>
) : (
hasSecureAuth && (
<button
type="button"
className="mp-btn mp-btn-primary"
onClick={onComplete}
style={{ padding: '0.6rem 1.25rem', fontSize: '0.9rem', fontWeight: 600 }}
style={{ width: '100%', padding: '0.85rem', fontSize: '0.95rem', fontWeight: 600 }}
>
Continue →
Continue to Upload →
</button>
)}
</>
) : (
hasSecureAuth && (
<button
type="button"
className="mp-btn mp-btn-primary"
onClick={onComplete}
style={{ width: '100%', padding: '0.85rem', fontSize: '0.95rem', fontWeight: 600 }}
>
Continue to Upload →
</button>
)
)}
</div>
)
)}
</div>
)}
<RecoveryCodesModal
isOpen={showRecoveryModal}
@ -687,3 +770,4 @@ export const SecuritySetupCards: React.FC<SecuritySetupCardsProps> = ({
</div>
);
};

View File

@ -2,7 +2,7 @@ import { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAnalytics } from './useAnalytics';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Settings, Plus, ExternalLink, Sparkles, Search, BarChart2, Package, CheckCircle, X, AlertTriangle } from 'lucide-react';
import { Settings, Plus, ExternalLink, Search, BarChart2, Package, CheckCircle, X, AlertTriangle } from 'lucide-react';
import api from '../../api';
import { useAuth } from '../../App';
import DeveloperOnboardingModal from '../../components/DeveloperOnboardingModal';
@ -13,11 +13,10 @@ const PLUGIN_CATEGORIES = ['Admin Tools', 'Economy', 'Fun', 'World Management',
// --- Main Component ---
const DashboardPlugins = () => {
const { t } = useTranslation();
const { user } = useAuth();
const { user, refreshUser } = useAuth();
const { timeSeries } = useAnalytics();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [isDevModalOpen, setIsDevModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState<string>('all');
const [incomingTransfers, setIncomingTransfers] = useState<any[]>([]);
@ -169,34 +168,11 @@ const DashboardPlugins = () => {
if (!user?.is_developer) {
return (
<div className="dev-plugins-container">
<div className="dev-plugins-card" style={{ textAlign: 'center', padding: '3.5rem 2rem' }}>
<div style={{
width: 64, height: 64, borderRadius: '50%',
background: 'rgba(249, 115, 22, 0.12)', border: '1px solid rgba(249, 115, 22, 0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 1.25rem'
}}>
<Sparkles size={32} color="#f97316" />
</div>
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0 0 0.5rem' }}>Developer Onboarding Required</h2>
<p style={{ color: 'var(--dash-text-muted)', maxWidth: '520px', margin: '0 auto 1.75rem', lineHeight: 1.6 }}>
To manage existing plugins or publish new ones, please complete the free <strong>Developer Onboarding</strong> wizard to set up your creator & legal profile.
</p>
<button
className="btn btn-primary"
style={{ padding: '0.75rem 1.75rem', fontSize: '1rem' }}
onClick={() => setIsDevModalOpen(true)}
>
<Sparkles size={18} style={{ marginRight: '8px' }} />
Complete Free Developer Onboarding
</button>
<DeveloperOnboardingModal
isOpen={isDevModalOpen}
onClose={() => setIsDevModalOpen(false)}
onSuccess={() => setIsDevModalOpen(false)}
/>
</div>
<div className="dev-plugins-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh', padding: '2rem 1rem' }}>
<DeveloperOnboardingModal
embedded={true}
onSuccess={() => refreshUser?.()}
/>
</div>
);
}