738 lines
31 KiB
TypeScript
738 lines
31 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { getCodeList } from 'country-list';
|
|
import {
|
|
Globe, Mail, CheckCircle2, ChevronRight,
|
|
AlertCircle, CreditCard, Code2, ShieldCheck, MapPin,
|
|
Gift,
|
|
} from 'lucide-react';
|
|
import api from '../api';
|
|
import { useAuth } from '../App';
|
|
import { SecuritySetupCards } from './SecuritySetupCards';
|
|
import './DeveloperOnboardingModal.css';
|
|
|
|
interface DeveloperOnboardingModalProps {
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
onSuccess?: () => void;
|
|
embedded?: boolean;
|
|
}
|
|
|
|
export const DeveloperOnboardingModal: React.FC<DeveloperOnboardingModalProps> = ({
|
|
isOpen = true,
|
|
onClose,
|
|
onSuccess,
|
|
embedded = false,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const { user, login } = useAuth();
|
|
const [step, setStep] = useState<number>(1);
|
|
const [entityType, setEntityType] = useState<'individual' | 'organization'>('individual');
|
|
const [orgType, setOrgType] = useState<string>('');
|
|
const [displayName, setDisplayName] = useState(user?.username || '');
|
|
const [nameChecking, setNameChecking] = useState(false);
|
|
const [nameError, setNameError] = useState<string | null>(null);
|
|
const [isSellingPaid, setIsSellingPaid] = useState<boolean | null>(null);
|
|
|
|
// Legal / Address (for paid sellers)
|
|
const [legalName, setLegalName] = useState('');
|
|
const [streetAddress, setStreetAddress] = useState('');
|
|
const [city, setCity] = useState('');
|
|
const [postalCode, setPostalCode] = useState('');
|
|
const [vatId, setVatId] = useState('');
|
|
const [country, setCountry] = useState(user?.country || '');
|
|
|
|
// Contact / Social
|
|
const [supportEmail, setSupportEmail] = useState(user?.email || '');
|
|
const [websiteUrl, setWebsiteUrl] = useState('');
|
|
const [githubUrl, setGithubUrl] = useState('');
|
|
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Debounced check for developer display name availability
|
|
useEffect(() => {
|
|
const trimmed = displayName.trim();
|
|
if (!trimmed) {
|
|
setNameError(null);
|
|
return;
|
|
}
|
|
const timer = setTimeout(async () => {
|
|
setNameChecking(true);
|
|
try {
|
|
const res = await api.get(`/user/developer/check-name?name=${encodeURIComponent(trimmed)}`);
|
|
if (res.data?.available === false) {
|
|
setNameError(res.data.message || t('developer.onboarding.name_taken_error'));
|
|
} else {
|
|
setNameError(null);
|
|
}
|
|
} catch (err: any) {
|
|
if (err.response?.data?.error) {
|
|
setNameError(err.response.data.error);
|
|
}
|
|
} finally {
|
|
setNameChecking(false);
|
|
}
|
|
}, 400);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [displayName, t]);
|
|
|
|
const hasSecureAuth = Boolean(user?.totp_enabled || (user?.passkey_count && user.passkey_count > 0) || user?.has_passkey);
|
|
|
|
if (!embedded && !isOpen) return null;
|
|
|
|
const handleCompleteOnboarding = async (connectStripe: boolean) => {
|
|
if (!acceptedTerms) {
|
|
setError('You must accept the Developer Distribution Agreement to continue.');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await api.post('/user/developer/onboard', {
|
|
entityType,
|
|
displayName,
|
|
legalName: isSellingPaid ? (legalName.trim() || null) : null,
|
|
streetAddress: isSellingPaid ? (streetAddress.trim() || null) : null,
|
|
city: isSellingPaid ? (city.trim() || null) : null,
|
|
postalCode: isSellingPaid ? (postalCode.trim() || null) : null,
|
|
vatId: isSellingPaid ? (vatId.trim() || null) : null,
|
|
publishingIntent: isSellingPaid ? 'paid' : 'free',
|
|
supportEmail: supportEmail.trim() || null,
|
|
websiteUrl: websiteUrl.trim() || null,
|
|
githubUrl: githubUrl.trim() || null,
|
|
country: country || user?.country || null,
|
|
acceptedTerms,
|
|
});
|
|
|
|
if (res.data.token) {
|
|
login(res.data.token);
|
|
}
|
|
|
|
if (connectStripe) {
|
|
const stripeRes = await api.post('/stripe/onboard');
|
|
if (stripeRes.data.url) {
|
|
window.location.href = stripeRes.data.url;
|
|
return;
|
|
}
|
|
}
|
|
|
|
onSuccess?.();
|
|
onClose?.();
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || 'Failed to complete developer onboarding');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const nextStep = () => {
|
|
setStep(s => s + 1);
|
|
};
|
|
|
|
const prevStep = () => {
|
|
setStep(s => Math.max(1, s - 1));
|
|
};
|
|
|
|
const handleStep2Next = async () => {
|
|
const trimmed = displayName.trim();
|
|
if (!trimmed) return;
|
|
setNameChecking(true);
|
|
try {
|
|
const res = await api.get(`/user/developer/check-name?name=${encodeURIComponent(trimmed)}`);
|
|
if (res.data?.available === false) {
|
|
setNameError(res.data.message || t('developer.onboarding.name_taken_error'));
|
|
return;
|
|
}
|
|
setNameError(null);
|
|
nextStep();
|
|
} catch (err: any) {
|
|
setNameError(err.response?.data?.error || 'Failed to verify developer name');
|
|
} finally {
|
|
setNameChecking(false);
|
|
}
|
|
};
|
|
|
|
const isStep2Valid = Boolean(displayName.trim() && !nameError && !nameChecking);
|
|
|
|
const isStep3Valid = Boolean(
|
|
isSellingPaid !== null &&
|
|
(isSellingPaid === false ||
|
|
(isSellingPaid === true &&
|
|
legalName.trim() &&
|
|
streetAddress.trim() &&
|
|
city.trim() &&
|
|
postalCode.trim() &&
|
|
country))
|
|
);
|
|
|
|
const content = (
|
|
<div className="dev-modal-container">
|
|
{!embedded && onClose && (
|
|
<button className="dev-modal-close" onClick={onClose} aria-label="Close modal">×</button>
|
|
)}
|
|
|
|
<div className="dev-modal-header">
|
|
<div className="dev-modal-badge">
|
|
{t('developer.onboarding.badge')}
|
|
</div>
|
|
<h2>{t('developer.onboarding.title')}</h2>
|
|
<p>
|
|
{isSellingPaid
|
|
? t('developer.onboarding.subtitle_paid')
|
|
: t('developer.onboarding.subtitle_free')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Step Indicator */}
|
|
<div className="dev-steps-nav">
|
|
<div className={`dev-step-pill ${step >= 1 ? 'active' : ''}`}>1. {t('developer.onboarding.step_account_type')}</div>
|
|
<div className="dev-step-line" />
|
|
<div className={`dev-step-pill ${step >= 2 ? 'active' : ''}`}>2. {t('developer.onboarding.step_name')}</div>
|
|
<div className="dev-step-line" />
|
|
<div className={`dev-step-pill ${step >= 3 ? 'active' : ''}`}>3. {t('developer.onboarding.step_monetization')}</div>
|
|
<div className="dev-step-line" />
|
|
<div className={`dev-step-pill ${step >= 4 ? 'active' : ''}`}>4. {t('developer.onboarding.step_links')}</div>
|
|
<div className="dev-step-line" />
|
|
<div className={`dev-step-pill ${step >= 5 ? 'active' : ''}`}>5. {t('developer.onboarding.step_finish')}</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="dev-modal-error">
|
|
<AlertCircle size={16} />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 1: Account Type (Google Play Style) ── */}
|
|
{step === 1 && (
|
|
<div className="dev-step-content">
|
|
<div className="dev-account-types-container">
|
|
{/* An organization */}
|
|
<div className={`dev-account-card ${entityType === 'organization' ? 'selected' : ''}`}>
|
|
<div className="dev-account-card-header">
|
|
<h3 className="dev-account-card-title">{t('developer.onboarding.org_title')}</h3>
|
|
<p className="dev-account-card-desc">
|
|
{t('developer.onboarding.org_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="dev-account-card-field">
|
|
<label htmlFor="orgTypeSelect">{t('developer.onboarding.org_type_label')}</label>
|
|
<select
|
|
id="orgTypeSelect"
|
|
className="dev-input dev-select"
|
|
value={orgType}
|
|
onChange={e => {
|
|
setOrgType(e.target.value);
|
|
setEntityType('organization');
|
|
}}
|
|
>
|
|
<option value="">{t('developer.onboarding.org_type_placeholder')}</option>
|
|
<option value="company">Company / Studio</option>
|
|
<option value="nonprofit">Non-profit / Open Source Organization</option>
|
|
<option value="education">Educational Institution</option>
|
|
<option value="other">Other entity</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="dev-account-card-actions">
|
|
<button
|
|
type="button"
|
|
className="dev-account-get-started-btn"
|
|
disabled={!orgType}
|
|
onClick={() => {
|
|
setEntityType('organization');
|
|
nextStep();
|
|
}}
|
|
>
|
|
{t('developer.onboarding.btn_get_started')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Yourself */}
|
|
<div className={`dev-account-card ${entityType === 'individual' ? 'selected' : ''}`}>
|
|
<div className="dev-account-card-header">
|
|
<h3 className="dev-account-card-title">{t('developer.onboarding.individual_title')}</h3>
|
|
<p className="dev-account-card-desc">
|
|
{t('developer.onboarding.individual_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="dev-account-card-actions">
|
|
<button
|
|
type="button"
|
|
className="dev-account-get-started-btn"
|
|
onClick={() => {
|
|
setEntityType('individual');
|
|
nextStep();
|
|
}}
|
|
>
|
|
{t('developer.onboarding.btn_get_started')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 2: Developer Public Name / Brand ── */}
|
|
{step === 2 && (
|
|
<div className="dev-step-content">
|
|
<div style={{ marginBottom: '1.25rem' }}>
|
|
<h3 style={{ fontSize: '1.1rem', fontWeight: 600, color: '#fff', marginBottom: '0.35rem' }}>
|
|
{t('developer.onboarding.dev_name_title')}
|
|
</h3>
|
|
<p style={{ fontSize: '0.85rem', color: 'var(--text-muted, #94a3b8)', lineHeight: 1.5, margin: 0 }}>
|
|
{t('developer.onboarding.dev_name_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="dev-form-group">
|
|
<label htmlFor="onboardingDisplayName">{t('developer.onboarding.display_name_label')}</label>
|
|
<input
|
|
id="onboardingDisplayName"
|
|
name="displayName"
|
|
type="text"
|
|
className={`dev-input ${nameError ? 'dev-input-error' : ''}`}
|
|
placeholder={entityType === 'organization' ? 'e.g. PixelCraft Studios' : t('developer.onboarding.display_name_placeholder')}
|
|
value={displayName}
|
|
onChange={e => {
|
|
setDisplayName(e.target.value);
|
|
setNameError(null);
|
|
}}
|
|
autoComplete="nickname"
|
|
maxLength={60}
|
|
autoFocus
|
|
/>
|
|
{nameError ? (
|
|
<span className="dev-field-error">
|
|
<AlertCircle size={14} />
|
|
<span>{nameError}</span>
|
|
</span>
|
|
) : (
|
|
<span className="dev-hint">{t('developer.onboarding.display_name_hint')}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="dev-btn-row" style={{ marginTop: '1.5rem' }}>
|
|
<button className="dev-btn dev-btn-secondary" onClick={prevStep}>
|
|
{t('developer.onboarding.btn_back')}
|
|
</button>
|
|
<button
|
|
className="dev-btn dev-btn-primary"
|
|
disabled={!isStep2Valid}
|
|
onClick={handleStep2Next}
|
|
>
|
|
{nameChecking ? 'Checking...' : t('developer.onboarding.btn_continue')} <ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 3: Monetization & Seller Details ── */}
|
|
{step === 3 && (
|
|
<div className="dev-step-content">
|
|
{/* Required Radio Question: Do you plan to publish Paid plugins? */}
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" style={{ marginBottom: '0.6rem', display: 'block', fontWeight: 600 }}>
|
|
{t('developer.onboarding.plan_paid_question')}
|
|
</label>
|
|
|
|
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
|
<label style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.55rem',
|
|
cursor: 'pointer',
|
|
padding: '0.6rem 1.25rem',
|
|
borderRadius: '8px',
|
|
background: isSellingPaid === true ? 'rgba(249, 115, 22, 0.12)' : 'rgba(255, 255, 255, 0.03)',
|
|
border: `1px solid ${isSellingPaid === true ? '#f97316' : 'rgba(255, 255, 255, 0.1)'}`,
|
|
color: isSellingPaid === true ? '#fff' : '#cbd5e1',
|
|
fontWeight: 600,
|
|
fontSize: '0.9rem',
|
|
transition: 'all 0.2s ease',
|
|
}}>
|
|
<input
|
|
type="radio"
|
|
name="planPaidChoice"
|
|
checked={isSellingPaid === true}
|
|
onChange={() => setIsSellingPaid(true)}
|
|
style={{ accentColor: '#f97316', width: 16, height: 16, cursor: 'pointer' }}
|
|
/>
|
|
{t('developer.onboarding.option_yes')}
|
|
</label>
|
|
|
|
<label style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.55rem',
|
|
cursor: 'pointer',
|
|
padding: '0.6rem 1.25rem',
|
|
borderRadius: '8px',
|
|
background: isSellingPaid === false ? 'rgba(249, 115, 22, 0.12)' : 'rgba(255, 255, 255, 0.03)',
|
|
border: `1px solid ${isSellingPaid === false ? '#f97316' : 'rgba(255, 255, 255, 0.1)'}`,
|
|
color: isSellingPaid === false ? '#fff' : '#cbd5e1',
|
|
fontWeight: 600,
|
|
fontSize: '0.9rem',
|
|
transition: 'all 0.2s ease',
|
|
}}>
|
|
<input
|
|
type="radio"
|
|
name="planPaidChoice"
|
|
checked={isSellingPaid === false}
|
|
onChange={() => setIsSellingPaid(false)}
|
|
style={{ accentColor: '#f97316', width: 16, height: 16, cursor: 'pointer' }}
|
|
/>
|
|
{t('developer.onboarding.option_no')}
|
|
</label>
|
|
</div>
|
|
|
|
{/* If clicked yes, display all the new required seller fields below */}
|
|
{isSellingPaid === true && (
|
|
<div style={{
|
|
marginTop: '1.25rem',
|
|
padding: '1.25rem',
|
|
background: 'rgba(249, 115, 22, 0.04)',
|
|
border: '1px solid rgba(249, 115, 22, 0.25)',
|
|
borderRadius: '10px',
|
|
}}>
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingLegalName">
|
|
<ShieldCheck size={14} /> {t('developer.onboarding.legal_name_label')}
|
|
</label>
|
|
<input
|
|
id="onboardingLegalName"
|
|
name="legalName"
|
|
type="text"
|
|
className="dev-input"
|
|
value={legalName}
|
|
onChange={e => setLegalName(e.target.value)}
|
|
placeholder={entityType === 'individual' ? 'First and Last Name' : 'Legal Company Name Inc.'}
|
|
autoComplete="name"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingStreetAddress">
|
|
<MapPin size={14} /> {t('developer.onboarding.street_label')}
|
|
</label>
|
|
<input
|
|
id="onboardingStreetAddress"
|
|
name="streetAddress"
|
|
type="text"
|
|
className="dev-input"
|
|
value={streetAddress}
|
|
onChange={e => setStreetAddress(e.target.value)}
|
|
placeholder="123 Main St, Suite 400"
|
|
autoComplete="street-address"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingCity">{t('developer.onboarding.city_label')}</label>
|
|
<input
|
|
id="onboardingCity"
|
|
name="city"
|
|
type="text"
|
|
className="dev-input"
|
|
value={city}
|
|
onChange={e => setCity(e.target.value)}
|
|
placeholder="City"
|
|
autoComplete="address-level2"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingPostalCode">{t('developer.onboarding.postal_label')}</label>
|
|
<input
|
|
id="onboardingPostalCode"
|
|
name="postalCode"
|
|
type="text"
|
|
className="dev-input"
|
|
value={postalCode}
|
|
onChange={e => setPostalCode(e.target.value)}
|
|
placeholder="10001"
|
|
autoComplete="postal-code"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingCountry">
|
|
<Globe size={14} /> {t('developer.onboarding.country_label')}
|
|
</label>
|
|
<select
|
|
id="onboardingCountry"
|
|
name="country"
|
|
className="dev-input dev-select"
|
|
value={country}
|
|
onChange={e => setCountry(e.target.value)}
|
|
autoComplete="country"
|
|
required
|
|
>
|
|
<option value="">Select country...</option>
|
|
{Object.entries(getCodeList()).map(([code, name]) => (
|
|
<option key={code} value={code}>{name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingVatId">{t('developer.onboarding.vat_label')}</label>
|
|
<input
|
|
id="onboardingVatId"
|
|
name="vatId"
|
|
type="text"
|
|
className="dev-input"
|
|
value={vatId}
|
|
onChange={e => setVatId(e.target.value)}
|
|
placeholder="EU123456789 or Tax ID"
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="dev-btn-row" style={{ marginTop: '1.25rem' }}>
|
|
<button className="dev-btn dev-btn-secondary" onClick={prevStep}>
|
|
{t('developer.onboarding.btn_back')}
|
|
</button>
|
|
<button
|
|
className="dev-btn dev-btn-primary"
|
|
disabled={!isStep3Valid}
|
|
onClick={nextStep}
|
|
>
|
|
{t('developer.onboarding.btn_continue')} <ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 4: Support Contact & Portfolio Links ── */}
|
|
{step === 4 && (
|
|
<div className="dev-step-content">
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingSupportEmail"><Mail size={14} /> {t('developer.onboarding.support_email_label')} *</label>
|
|
<input
|
|
id="onboardingSupportEmail"
|
|
name="supportEmail"
|
|
type="email"
|
|
className="dev-input"
|
|
value={supportEmail}
|
|
onChange={e => setSupportEmail(e.target.value)}
|
|
placeholder="support@yourdomain.com"
|
|
autoComplete="email"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingWebsiteUrl"><Globe size={14} /> {t('developer.onboarding.website_label')}</label>
|
|
<input
|
|
id="onboardingWebsiteUrl"
|
|
name="websiteUrl"
|
|
type="url"
|
|
className="dev-input"
|
|
value={websiteUrl}
|
|
onChange={e => setWebsiteUrl(e.target.value)}
|
|
placeholder="https://yourwebsite.com"
|
|
autoComplete="url"
|
|
/>
|
|
</div>
|
|
|
|
<div className="dev-form-group">
|
|
<label className="dev-field-label" htmlFor="onboardingGithubUrl"><Code2 size={14} /> {t('developer.onboarding.github_label')}</label>
|
|
<input
|
|
id="onboardingGithubUrl"
|
|
name="githubUrl"
|
|
type="text"
|
|
className="dev-input"
|
|
value={githubUrl}
|
|
onChange={e => setGithubUrl(e.target.value)}
|
|
placeholder="https://github.com/yourname"
|
|
autoComplete="url"
|
|
/>
|
|
</div>
|
|
|
|
<div className="dev-btn-row" style={{ marginTop: '1.5rem' }}>
|
|
<button className="dev-btn dev-btn-secondary" onClick={prevStep}>{t('developer.onboarding.btn_back')}</button>
|
|
<button
|
|
className="dev-btn dev-btn-primary"
|
|
disabled={!supportEmail.trim()}
|
|
onClick={nextStep}
|
|
>
|
|
{t('developer.onboarding.btn_continue')} <ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── 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 & 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 & 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>
|
|
)}
|
|
|
|
{/* ── 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' }}>
|
|
<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-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>
|
|
|
|
{/* ── 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={{
|
|
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' }}>
|
|
<button
|
|
className="dev-btn dev-btn-stripe"
|
|
disabled={loading || !acceptedTerms || !hasSecureAuth}
|
|
onClick={() => handleCompleteOnboarding(true)}
|
|
>
|
|
<CreditCard size={16} /> {loading ? 'Connecting Stripe...' : t('developer.onboarding.btn_connect_stripe')}
|
|
</button>
|
|
<button
|
|
className="dev-btn dev-btn-secondary"
|
|
disabled={loading || !acceptedTerms || !hasSecureAuth}
|
|
onClick={() => handleCompleteOnboarding(false)}
|
|
>
|
|
{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>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
if (embedded) {
|
|
return (
|
|
<div className="dev-onboarding-embedded">
|
|
{content}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="dev-modal-overlay">
|
|
{content}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DeveloperOnboardingModal;
|