Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
535 lines
25 KiB
HTML
535 lines
25 KiB
HTML
{# app/templates/platform/signup.html #}
|
|
{# Multi-step Signup Wizard #}
|
|
{% extends "platform/base.html" %}
|
|
|
|
{% block title %}Start Your Free Trial - Orion{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
{# Stripe.js for payment #}
|
|
<script src="https://js.stripe.com/v3/"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div x-data="signupWizard()" class="min-h-screen py-12 bg-gray-50 dark:bg-gray-900">
|
|
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
{# Progress Steps #}
|
|
<div class="mb-12">
|
|
<div class="flex items-center justify-between">
|
|
<template x-for="(stepName, index) in ['Select Plan', 'Claim Shop', 'Account', 'Payment']" :key="index">
|
|
<div class="flex items-center" :class="index < 3 ? 'flex-1' : ''">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-full font-semibold transition-colors"
|
|
:class="currentStep > index + 1 ? 'bg-green-500 text-white' : currentStep === index + 1 ? 'bg-indigo-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-500'">
|
|
<template x-if="currentStep > index + 1">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</template>
|
|
<template x-if="currentStep <= index + 1">
|
|
<span x-text="index + 1"></span>
|
|
</template>
|
|
</div>
|
|
<span class="ml-2 text-sm font-medium hidden sm:inline"
|
|
:class="currentStep >= index + 1 ? 'text-gray-900 dark:text-white' : 'text-gray-500'"
|
|
x-text="stepName"></span>
|
|
<template x-if="index < 3">
|
|
<div class="flex-1 h-1 mx-4 bg-gray-200 dark:bg-gray-700 rounded">
|
|
<div class="h-full bg-indigo-600 rounded transition-all"
|
|
:style="'width: ' + (currentStep > index + 1 ? '100%' : '0%')"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
{# Form Card #}
|
|
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
|
|
{# ===============================================================
|
|
STEP 1: SELECT PLAN
|
|
=============================================================== #}
|
|
<div x-show="currentStep === 1" class="p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Choose Your Plan</h2>
|
|
|
|
{# Billing Toggle #}
|
|
<div class="flex items-center justify-center mb-8 space-x-4">
|
|
<span :class="!isAnnual ? 'font-semibold text-gray-900 dark:text-white' : 'text-gray-500'">Monthly</span>
|
|
<button @click="isAnnual = !isAnnual"
|
|
class="relative w-12 h-6 rounded-full transition-colors"
|
|
:class="isAnnual ? 'bg-indigo-600' : 'bg-gray-300'">
|
|
<span class="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform"
|
|
:class="isAnnual ? 'translate-x-6' : ''"></span>
|
|
</button>
|
|
<span :class="isAnnual ? 'font-semibold text-gray-900 dark:text-white' : 'text-gray-500'">
|
|
Annual <span class="text-green-600 text-xs">Save 17%</span>
|
|
</span>
|
|
</div>
|
|
|
|
{# Tier Options #}
|
|
<div class="space-y-4">
|
|
{% for tier in tiers %}
|
|
{% if not tier.is_enterprise %}
|
|
<label class="block">
|
|
<input type="radio" name="tier" value="{{ tier.code }}"
|
|
x-model="selectedTier" class="hidden peer"/>
|
|
<div class="p-4 border-2 rounded-xl cursor-pointer transition-all
|
|
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/20
|
|
border-gray-200 dark:border-gray-700 hover:border-gray-300">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 dark:text-white">{{ tier.name }}</h3>
|
|
<p class="text-sm text-gray-500">
|
|
{% if tier.orders_per_month %}{{ tier.orders_per_month }} orders/mo{% else %}Unlimited{% endif %}
|
|
•
|
|
{% if tier.team_members %}{{ tier.team_members }} user{% if tier.team_members > 1 %}s{% endif %}{% else %}Unlimited{% endif %}
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<template x-if="!isAnnual">
|
|
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ tier.price_monthly|int }}€/mo</span>
|
|
</template>
|
|
<template x-if="isAnnual">
|
|
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ ((tier.price_annual or tier.price_monthly * 12) / 12)|round(0)|int }}€/mo</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{# Free Trial Note #}
|
|
<div class="mt-6 p-4 bg-green-50 dark:bg-green-900/20 rounded-xl">
|
|
<p class="text-sm text-green-800 dark:text-green-300">
|
|
<strong>{{ trial_days }}-day free trial.</strong>
|
|
We'll collect your payment info, but you won't be charged until the trial ends.
|
|
</p>
|
|
</div>
|
|
|
|
<button @click="startSignup()"
|
|
:disabled="!selectedTier || loading"
|
|
class="mt-8 w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl transition-colors disabled:opacity-50">
|
|
Continue
|
|
</button>
|
|
</div>
|
|
|
|
{# ===============================================================
|
|
STEP 2: CLAIM LETZSHOP SHOP (Optional)
|
|
=============================================================== #}
|
|
<div x-show="currentStep === 2" class="p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Connect Your Letzshop Shop</h2>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-6">Optional: Link your Letzshop account to sync orders automatically.</p>
|
|
|
|
<div class="space-y-4">
|
|
<input
|
|
type="text"
|
|
x-model="letzshopUrl"
|
|
placeholder="letzshop.lu/vendors/your-shop"
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"
|
|
/>
|
|
|
|
<template x-if="letzshopStore">
|
|
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-xl">
|
|
<p class="text-green-800 dark:text-green-300">
|
|
Found: <strong x-text="letzshopStore.name"></strong>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="letzshopError">
|
|
<div class="p-4 bg-red-50 dark:bg-red-900/20 rounded-xl">
|
|
<p class="text-red-800 dark:text-red-300" x-text="letzshopError"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="mt-8 flex gap-4">
|
|
<button @click="currentStep = 1"
|
|
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
|
Back
|
|
</button>
|
|
<button @click="claimStore()"
|
|
:disabled="loading"
|
|
class="flex-1 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
|
<span x-text="letzshopUrl.trim() ? 'Connect & Continue' : 'Skip This Step'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{# ===============================================================
|
|
STEP 3: CREATE ACCOUNT
|
|
=============================================================== #}
|
|
<div x-show="currentStep === 3" class="p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Create Your Account</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
<span class="text-red-500">*</span> Required fields
|
|
</p>
|
|
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
First Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="account.firstName" required
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Last Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="account.lastName" required
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Merchant Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="account.merchantName" required
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Email <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="email" x-model="account.email" required
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Password <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="password" x-model="account.password" required minlength="8"
|
|
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
|
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
|
|
</div>
|
|
|
|
<template x-if="accountError">
|
|
<div class="p-4 bg-red-50 dark:bg-red-900/20 rounded-xl">
|
|
<p class="text-red-800 dark:text-red-300" x-text="accountError"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="mt-8 flex gap-4">
|
|
<button @click="currentStep = 2"
|
|
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
|
Back
|
|
</button>
|
|
<button @click="createAccount()"
|
|
:disabled="loading || !isAccountValid()"
|
|
class="flex-1 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
|
Continue to Payment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{# ===============================================================
|
|
STEP 4: PAYMENT
|
|
=============================================================== #}
|
|
<div x-show="currentStep === 4" class="p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Add Payment Method</h2>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-6">You won't be charged until your {{ trial_days }}-day trial ends.</p>
|
|
|
|
{# Stripe Card Element #}
|
|
<div id="card-element" class="p-4 border border-gray-300 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-900"></div>
|
|
<div id="card-errors" class="text-red-600 text-sm mt-2"></div>
|
|
|
|
<div class="mt-8 flex gap-4">
|
|
<button @click="currentStep = 3"
|
|
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
|
Back
|
|
</button>
|
|
<button @click="submitPayment()"
|
|
:disabled="loading || paymentProcessing"
|
|
class="flex-1 py-3 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
|
<template x-if="paymentProcessing">
|
|
<span>Processing...</span>
|
|
</template>
|
|
<template x-if="!paymentProcessing">
|
|
<span>Start Free Trial</span>
|
|
</template>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function signupWizard() {
|
|
return {
|
|
currentStep: 1,
|
|
loading: false,
|
|
sessionId: null,
|
|
|
|
// Step 1: Plan
|
|
selectedTier: '{{ selected_tier or "professional" }}',
|
|
isAnnual: {{ 'true' if is_annual else 'false' }},
|
|
|
|
// Step 2: Letzshop
|
|
letzshopUrl: '',
|
|
letzshopStore: null,
|
|
letzshopError: null,
|
|
|
|
// Step 3: Account
|
|
account: {
|
|
firstName: '',
|
|
lastName: '',
|
|
merchantName: '',
|
|
email: '',
|
|
password: ''
|
|
},
|
|
accountError: null,
|
|
|
|
// Step 4: Payment
|
|
stripe: null,
|
|
cardElement: null,
|
|
paymentProcessing: false,
|
|
clientSecret: null,
|
|
|
|
init() {
|
|
// Check URL params for pre-selection
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('tier')) {
|
|
this.selectedTier = params.get('tier');
|
|
}
|
|
if (params.get('annual') === 'true') {
|
|
this.isAnnual = true;
|
|
}
|
|
if (params.get('letzshop')) {
|
|
this.letzshopUrl = params.get('letzshop');
|
|
}
|
|
|
|
// Initialize Stripe when we get to step 4
|
|
this.$watch('currentStep', (step) => {
|
|
if (step === 4) {
|
|
this.initStripe();
|
|
}
|
|
});
|
|
},
|
|
|
|
async startSignup() {
|
|
this.loading = true;
|
|
try {
|
|
const response = await fetch('/api/v1/platform/signup/start', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tier_code: this.selectedTier,
|
|
is_annual: this.isAnnual
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
this.sessionId = data.session_id;
|
|
this.currentStep = 2;
|
|
} else {
|
|
alert(data.detail || 'Failed to start signup');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Failed to start signup. Please try again.');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async claimStore() {
|
|
if (this.letzshopUrl.trim()) {
|
|
this.loading = true;
|
|
this.letzshopError = null;
|
|
|
|
try {
|
|
// First lookup the store
|
|
const lookupResponse = await fetch('/api/v1/platform/letzshop-stores/lookup', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url: this.letzshopUrl })
|
|
});
|
|
|
|
const lookupData = await lookupResponse.json();
|
|
|
|
if (lookupData.found && !lookupData.store.is_claimed) {
|
|
this.letzshopStore = lookupData.store;
|
|
|
|
// Claim the store
|
|
const claimResponse = await fetch('/api/v1/platform/signup/claim-store', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
session_id: this.sessionId,
|
|
letzshop_slug: lookupData.store.slug
|
|
})
|
|
});
|
|
|
|
if (claimResponse.ok) {
|
|
const claimData = await claimResponse.json();
|
|
this.account.merchantName = claimData.store_name || '';
|
|
this.currentStep = 3;
|
|
} else {
|
|
const error = await claimResponse.json();
|
|
this.letzshopError = error.detail || 'Failed to claim store';
|
|
}
|
|
} else if (lookupData.store?.is_claimed) {
|
|
this.letzshopError = 'This shop has already been claimed.';
|
|
} else {
|
|
this.letzshopError = lookupData.error || 'Shop not found.';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
this.letzshopError = 'Failed to lookup store.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
} else {
|
|
// Skip this step
|
|
this.currentStep = 3;
|
|
}
|
|
},
|
|
|
|
isAccountValid() {
|
|
return this.account.firstName.trim() &&
|
|
this.account.lastName.trim() &&
|
|
this.account.merchantName.trim() &&
|
|
this.account.email.trim() &&
|
|
this.account.password.length >= 8;
|
|
},
|
|
|
|
async createAccount() {
|
|
this.loading = true;
|
|
this.accountError = null;
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/platform/signup/create-account', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
session_id: this.sessionId,
|
|
email: this.account.email,
|
|
password: this.account.password,
|
|
first_name: this.account.firstName,
|
|
last_name: this.account.lastName,
|
|
merchant_name: this.account.merchantName
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
this.currentStep = 4;
|
|
} else {
|
|
this.accountError = data.detail || 'Failed to create account';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
this.accountError = 'Failed to create account. Please try again.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async initStripe() {
|
|
{% if stripe_publishable_key %}
|
|
this.stripe = Stripe('{{ stripe_publishable_key }}');
|
|
const elements = this.stripe.elements();
|
|
|
|
this.cardElement = elements.create('card', {
|
|
style: {
|
|
base: {
|
|
fontSize: '16px',
|
|
color: '#374151',
|
|
'::placeholder': { color: '#9CA3AF' }
|
|
}
|
|
}
|
|
});
|
|
|
|
this.cardElement.mount('#card-element');
|
|
this.cardElement.on('change', (event) => {
|
|
const displayError = document.getElementById('card-errors');
|
|
displayError.textContent = event.error ? event.error.message : '';
|
|
});
|
|
|
|
// Get SetupIntent
|
|
try {
|
|
const response = await fetch('/api/v1/platform/signup/setup-payment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ session_id: this.sessionId })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
this.clientSecret = data.client_secret;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting SetupIntent:', error);
|
|
}
|
|
{% else %}
|
|
console.warn('Stripe not configured');
|
|
{% endif %}
|
|
},
|
|
|
|
async submitPayment() {
|
|
if (!this.stripe || !this.clientSecret) {
|
|
alert('Payment not configured. Please contact support.');
|
|
return;
|
|
}
|
|
|
|
this.paymentProcessing = true;
|
|
|
|
try {
|
|
const { setupIntent, error } = await this.stripe.confirmCardSetup(
|
|
this.clientSecret,
|
|
{ payment_method: { card: this.cardElement } }
|
|
);
|
|
|
|
if (error) {
|
|
document.getElementById('card-errors').textContent = error.message;
|
|
this.paymentProcessing = false;
|
|
return;
|
|
}
|
|
|
|
// Complete signup
|
|
const response = await fetch('/api/v1/platform/signup/complete', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
session_id: this.sessionId,
|
|
setup_intent_id: setupIntent.id
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
// Store access token for automatic login
|
|
if (data.access_token) {
|
|
localStorage.setItem('store_token', data.access_token);
|
|
localStorage.setItem('storeCode', data.store_code);
|
|
console.log('Store token stored for automatic login');
|
|
}
|
|
window.location.href = '/signup/success?store_code=' + data.store_code;
|
|
} else {
|
|
alert(data.detail || 'Failed to complete signup');
|
|
}
|
|
} catch (error) {
|
|
console.error('Payment error:', error);
|
|
alert('Payment failed. Please try again.');
|
|
} finally {
|
|
this.paymentProcessing = false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|