initial commit

This commit is contained in:
Timur Gordon
2025-06-27 04:13:31 +02:00
commit b2ee21999f
134 changed files with 35580 additions and 0 deletions

16
portal/.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Stripe Configuration
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# Server Configuration
PORT=8080
HOST=127.0.0.1
RUST_LOG=info
# Database (if needed)
DATABASE_URL=sqlite:./data/app.db
# Security
JWT_SECRET=your_jwt_secret_here
CORS_ORIGIN=http://127.0.0.1:8080

1518
portal/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

49
portal/Cargo.toml Normal file
View File

@@ -0,0 +1,49 @@
[package]
name = "zanzibar-freezone-portal"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
# Frontend (WASM) dependencies
yew = { version = "0.21", features = ["csr"] }
web-sys = { version = "0.3", features = [
"console",
"Document",
"Element",
"HtmlElement",
"HtmlInputElement",
"HtmlSelectElement",
"HtmlTextAreaElement",
"HtmlFormElement",
"Location",
"Window",
"History",
"MouseEvent",
"Event",
"EventTarget",
"Storage",
"UrlSearchParams",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Headers"
] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
gloo = { version = "0.10", features = ["storage", "timers", "events"] }
gloo-utils = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
base64 = "0.21"
uuid = { version = "1.0", features = ["v4", "js"] }
chrono = { version = "0.4", features = ["serde", "wasm-bindgen"] }
[dev-dependencies]
wasm-bindgen-test = "0.3"

66
portal/README.md Normal file
View File

@@ -0,0 +1,66 @@
# Zanzibar Digital Freezone Portal
This is the entry portal for the Zanzibar Digital Freezone platform. It provides a streamlined registration and login interface for digital residents.
## Features
- **Digital Resident Registration**: Complete multi-step registration process with KYC
- **Stripe Payment Integration**: Secure payment processing for registration fees
- **Responsive Design**: Works on desktop and mobile devices
- **Real-time Validation**: Form validation and error handling
- **Animated UI**: Smooth transitions and professional interface
- **Fresh Start**: No form persistence - users start fresh each time for simplicity
## What's Included
- Resident registration overlay with expandable form
- Stripe Elements integration for secure payments
- Form validation and error handling
- Responsive Bootstrap-based design
- WASM-based Yew frontend
## What's Removed
This portal is a stripped-down version of the main platform that only includes:
- Resident registration components
- Stripe payment integration
- Essential models and services
Removed components:
- Company registration
- Treasury dashboard
- Accounting system
- Business management features
- Admin panels
- Full platform navigation
## Building and Running
```bash
# Install trunk if you haven't already
cargo install trunk
# Build the WASM application
trunk build
# Serve for development
trunk serve
```
## Stripe Configuration
Update the Stripe publishable key in `index.html`:
```javascript
const STRIPE_PUBLISHABLE_KEY = 'pk_test_your_actual_key_here';
```
## Server Integration
The portal expects a server running on `http://127.0.0.1:3001` with the following endpoints:
- `POST /resident/create-payment-intent` - Create payment intent for resident registration
## Purpose
This portal serves as the entry point for new users who want to become digital residents. Once they complete registration, they can be redirected to the full platform.

2
portal/Trunk.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
target = "index.html"

334
portal/index.html Normal file
View File

@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Zanzibar Digital Freezone Portal</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
/* Stripe Elements styling */
#payment-element {
min-height: 40px;
padding: 10px;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
background-color: #ffffff;
}
.payment-ready {
border-color: #198754 !important;
border-width: 2px !important;
box-shadow: 0 0 0 0.2rem rgba(25, 135, 84, 0.25) !important;
}
/* Loading state for payment form */
.payment-loading {
opacity: 0.7;
pointer-events: none;
}
/* Error display styling */
#payment-errors {
margin-top: 1rem;
margin-bottom: 1rem;
display: none;
}
/* Fade in animation for registration form */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Transition animations */
.transition-all {
transition: all 0.5s ease-in-out;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Stripe JavaScript SDK -->
<script src="https://js.stripe.com/v3/"></script>
<!-- Stripe Integration for Portal -->
<script>
let stripe;
let elements;
let paymentElement;
// Stripe publishable key - replace with your actual key from Stripe Dashboard
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51MCkZTC7LG8OeRdIcqmmoDkRwDObXSwYdChprMHJYoD2VRO8OCDBV5KtegLI0tLFXJo9yyvEXi7jzk1NAB5owj8i00DkYSaV9y';
// Initialize Stripe when the script loads
document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 Zanzibar Portal Stripe integration loaded');
// Initialize Stripe
if (window.Stripe) {
stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
console.log('✅ Stripe initialized for portal');
} else {
console.error('❌ Stripe.js not loaded');
}
});
// Create payment intent on server (supports both company and resident registration)
window.createPaymentIntent = async function(formDataJson) {
console.log('💳 Creating payment intent...');
try {
// Parse the JSON string from Rust
let formData;
if (typeof formDataJson === 'string') {
formData = JSON.parse(formDataJson);
} else {
formData = formDataJson;
}
// Determine endpoint based on registration type
const isResidentRegistration = formData.type === 'resident_registration';
const endpoint = isResidentRegistration
? 'http://127.0.0.1:3001/resident/create-payment-intent'
: 'http://127.0.0.1:3001/company/create-payment-intent';
console.log('📋 Registration type:', isResidentRegistration ? 'Resident' : 'Company');
console.log('🔧 Server endpoint:', endpoint);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
console.log('📡 Server response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Payment intent creation failed:', errorText);
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
errorData = { error: errorText };
}
const errorMsg = errorData.error || 'Failed to create payment intent';
console.error('💥 Error details:', errorData);
throw new Error(errorMsg);
}
const responseData = await response.json();
console.log('✅ Payment intent created successfully');
console.log('🔑 Client secret received:', responseData.client_secret ? 'Yes' : 'No');
const { client_secret } = responseData;
if (!client_secret) {
throw new Error('No client secret received from server');
}
return client_secret;
} catch (error) {
console.error('❌ Payment intent creation error:', error.message);
console.error('🔧 Troubleshooting:');
console.error(' 1. Check if server is running on port 3001');
console.error(' 2. Verify Stripe API keys in .env file');
console.error(' 3. Check server logs for detailed error info');
throw error;
}
};
// Initialize Stripe Elements with client secret
window.initializeStripeElements = async function(clientSecret) {
console.log('🔧 Initializing Stripe Elements...');
console.log('🔑 Client secret format check:', clientSecret ? 'Valid' : 'Missing');
try {
if (!stripe) {
throw new Error('Stripe not initialized - check your publishable key');
}
// Create Elements instance with client secret
elements = stripe.elements({
clientSecret: clientSecret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#0099FF',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: 'system-ui, sans-serif',
spacingUnit: '4px',
borderRadius: '6px',
}
}
});
// Clear the payment element container first
const paymentElementDiv = document.getElementById('payment-element');
if (!paymentElementDiv) {
throw new Error('Payment element container not found');
}
paymentElementDiv.innerHTML = '';
// Create and mount the Payment Element
paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle real-time validation errors from the Payment Element
paymentElement.on('change', (event) => {
const displayError = document.getElementById('payment-errors');
if (event.error) {
displayError.textContent = event.error.message;
displayError.style.display = 'block';
displayError.classList.remove('alert-success');
displayError.classList.add('alert-danger');
} else {
displayError.style.display = 'none';
}
});
// Handle when the Payment Element is ready
paymentElement.on('ready', () => {
console.log('✅ Stripe Elements ready for payment');
// Add a subtle success indicator
const paymentCard = paymentElementDiv.closest('.card');
if (paymentCard) {
paymentCard.style.borderColor = '#0099FF';
paymentCard.style.borderWidth = '2px';
}
// Update button text to show payment is ready
const submitButton = document.getElementById('submit-payment');
const submitText = document.getElementById('submit-text');
if (submitButton && submitText) {
submitButton.disabled = false;
submitText.textContent = 'Complete Payment';
submitButton.classList.remove('btn-secondary');
submitButton.classList.add('btn-success');
}
});
console.log('✅ Stripe Elements initialized successfully');
return true;
} catch (error) {
console.error('❌ Error initializing Stripe Elements:', error);
// Show helpful error message
const errorElement = document.getElementById('payment-errors');
if (errorElement) {
errorElement.innerHTML = `
<div class="alert alert-warning alert-dismissible" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Stripe Setup Required:</strong> ${error.message || 'Failed to load payment form'}<br><br>
<strong>Next Steps:</strong><br>
1. Get your Stripe API keys from <a href="https://dashboard.stripe.com/apikeys" target="_blank">Stripe Dashboard</a><br>
2. Replace the placeholder publishable key in the code<br>
3. Set up a server to create payment intents<br><br>
<small>The integration is complete - you just need real Stripe credentials!</small>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
errorElement.style.display = 'block';
}
throw error;
}
};
// Confirm payment with Stripe
window.confirmStripePayment = async function(clientSecret) {
console.log('🔄 Confirming payment...');
try {
// Ensure elements are ready before submitting
if (!elements) {
console.error('❌ Payment elements not initialized');
throw new Error('Payment form not ready. Please wait a moment and try again.');
}
console.log('🔄 Step 1: Submitting payment elements...');
// Step 1: Submit the payment elements first (required by new Stripe API)
const { error: submitError } = await elements.submit();
if (submitError) {
console.error('❌ Elements submit failed:', submitError);
throw new Error(submitError.message || 'Payment form validation failed.');
}
console.log('✅ Step 1 complete: Elements submitted successfully');
console.log('🔄 Step 2: Confirming payment with Stripe...');
// Step 2: Confirm payment with Stripe
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
clientSecret: clientSecret,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
redirect: 'if_required'
});
if (error) {
console.error('❌ Payment confirmation failed:', error);
throw new Error(error.message);
}
if (paymentIntent && paymentIntent.status === 'succeeded') {
console.log('✅ Payment completed successfully!');
console.log('🆔 Payment Intent ID:', paymentIntent.id);
return true;
} else {
console.error('❌ Unexpected payment status:', paymentIntent?.status);
throw new Error('Payment processing failed. Please try again.');
}
} catch (error) {
console.error('❌ Payment confirmation error:', error.message);
throw error;
}
};
console.log('✅ Zanzibar Portal Stripe integration ready');
console.log('🏠 Portal supports resident registration with Stripe payments');
</script>
<!-- WASM Application -->
<script type="module">
async function run() {
try {
// Load the WASM module for the Yew application
const init = await import('./pkg/zanzibar_freezone_portal.js');
await init.default();
console.log('✅ Zanzibar Digital Freezone Portal initialized');
console.log('🏠 Portal ready for resident registration');
} catch (error) {
console.error('❌ Failed to initialize WASM application:', error);
console.error('🔧 Make sure to build the WASM module with: trunk build');
}
}
run();
</script>
</body>
</html>

93
portal/src/app.rs Normal file
View File

@@ -0,0 +1,93 @@
use yew::prelude::*;
use crate::components::{ResidentLandingOverlay, PortalHome};
use crate::models::company::{DigitalResident, DigitalResidentFormData};
#[derive(Clone, Debug)]
pub enum Msg {
ResidentSignIn(String, String), // private_key, unused
ResidentRegistrationComplete(DigitalResident),
}
pub struct App {
is_logged_in: bool,
user_name: Option<String>,
registration_data: Option<DigitalResidentFormData>,
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
wasm_logger::init(wasm_logger::Config::default());
log::info!("Starting Zanzibar Digital Freezone Portal");
Self {
is_logged_in: false,
user_name: None,
registration_data: None,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ResidentSignIn(email, _password) => {
// Handle resident sign in - for now just log them in
log::info!("Resident sign in attempt: {}", email);
self.is_logged_in = true;
self.user_name = Some(email);
true
}
Msg::ResidentRegistrationComplete(resident) => {
// Handle successful resident registration
self.is_logged_in = true;
self.user_name = Some(resident.full_name.clone());
// Convert DigitalResident to DigitalResidentFormData for the residence card
self.registration_data = Some(DigitalResidentFormData {
full_name: resident.full_name,
email: resident.email,
phone: resident.phone,
date_of_birth: resident.date_of_birth,
nationality: resident.nationality,
passport_number: resident.passport_number,
passport_expiry: resident.passport_expiry,
current_address: resident.current_address,
city: resident.city,
country: resident.country,
postal_code: resident.postal_code,
occupation: resident.occupation,
employer: resident.employer,
annual_income: resident.annual_income,
education_level: resident.education_level,
public_key: resident.public_key,
..DigitalResidentFormData::default()
});
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// If user is logged in, show the portal home page
if self.is_logged_in {
return html! {
<PortalHome
user_name={self.user_name.as_ref().unwrap_or(&"Digital Resident".to_string()).clone()}
registration_data={self.registration_data.clone()}
/>
};
}
// Show the registration/login overlay
html! {
<ResidentLandingOverlay
on_registration_complete={link.callback(|resident| Msg::ResidentRegistrationComplete(resident))}
on_sign_in={link.callback(|(private_key, _)| Msg::ResidentSignIn(private_key, "".to_string()))}
on_close={None::<Callback<()>>} // No close button since this is the main portal
/>
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod resident_registration;
pub use resident_registration::*;

View File

@@ -0,0 +1,9 @@
pub mod step_payment_stripe;
pub mod simple_resident_wizard;
pub mod simple_step_info;
pub mod residence_card;
pub use step_payment_stripe::*;
pub use simple_resident_wizard::*;
pub use simple_step_info::*;
pub use residence_card::*;

View File

@@ -0,0 +1,96 @@
use yew::prelude::*;
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct ResidenceCardProps {
pub form_data: DigitalResidentFormData,
}
#[function_component(ResidenceCard)]
pub fn residence_card(props: &ResidenceCardProps) -> Html {
let form_data = &props.form_data;
html! {
<div class="d-flex align-items-center justify-content-center h-100">
<div class="residence-card">
<div class="card border-0 shadow-lg" style="width: 350px; background: white; border-radius: 15px;">
// Header with Zanzibar flag gradient
<div style="background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%); height: 80px; border-radius: 15px 15px 0 0; position: relative;">
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-between px-4">
<div>
<h6 class="mb-0 text-white" style="font-size: 0.9rem; font-weight: 600;">{"DIGITAL RESIDENT"}</h6>
<small class="text-white" style="opacity: 0.9; font-size: 0.75rem;">{"Zanzibar Digital Freezone"}</small>
</div>
<i class="bi bi-shield-check-fill text-white" style="font-size: 1.5rem; opacity: 0.9;"></i>
</div>
</div>
// Card body with white background
<div class="card-body p-4" style="background: white; border-radius: 0 0 15px 15px;">
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"FULL NAME"}</div>
<div class="h5 mb-0 text-dark" style="font-weight: 600;">
{if form_data.full_name.is_empty() {
"Your Name Here"
} else {
&form_data.full_name
}}
</div>
</div>
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"EMAIL"}</div>
<div class="text-dark" style="font-size: 0.9rem;">
{if form_data.email.is_empty() {
"your.email@example.com"
} else {
&form_data.email
}}
</div>
</div>
<div class="mb-3">
<div class="text-muted small d-flex align-items-center" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">
<i class="bi bi-key me-1" style="font-size: 0.8rem;"></i>
{"PUBLIC KEY"}
</div>
<div class="text-dark" style="font-size: 0.7rem; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; word-break: break-all; line-height: 1.3;">
{if let Some(public_key) = &form_data.public_key {
format!("{}...", &public_key[..std::cmp::min(24, public_key.len())])
} else {
"- - - - - - - - - - - - - - - -".to_string()
}}
</div>
</div>
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"RESIDENT SINCE"}</div>
<div class="text-dark" style="font-size: 0.8rem;">
{"2025"}
</div>
</div>
<div class="d-flex justify-content-between align-items-end mb-3">
<div>
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"RESIDENT ID"}</div>
<div class="text-dark" style="font-weight: 600;">{"ZDF-2025-****"}</div>
</div>
<div class="text-end">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"STATUS"}</div>
<div class="badge" style="background: #ffc107; color: #212529; font-weight: 500;">{"PENDING"}</div>
</div>
</div>
// QR Code at bottom
<div class="text-center border-top pt-3" style="border-color: #e9ecef !important;">
<div class="d-inline-block p-2 rounded" style="background: #f8f9fa;">
<div style="width: 60px; height: 60px; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSJ3aGl0ZSIvPgo8cmVjdCB4PSI0IiB5PSI0IiB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjIwIiB5PSI0IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyOCIgeT0iNCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjQiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iOCIgeT0iOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iNDgiIHk9IjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjIwIiB5PSIxMiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMzYiIHk9IjEyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0IiB5PSIyMCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMTIiIHk9IjIwIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjM2IiB5PSIyMCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjIwIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI1MiIgeT0iMjAiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjI0IiB5PSIyNCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iNCIgeT0iMjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjEyIiB5PSIyOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMzYiIHk9IjI4IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0NCIgeT0iMjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjUyIiB5PSIyOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNCIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjEyIiB5PSIzNiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMjAiIHk9IjM2IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyOCIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjM2IiB5PSIzNiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjM2IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI1MiIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjQiIHk9IjQ0IiB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjIwIiB5PSI0NCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMjgiIHk9IjQ0IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0NCIgeT0iNDQiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iOCIgeT0iNDgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjIwIiB5PSI1MiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDgiIHk9IjQ4IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K') no-repeat center; background-size: contain;"></div>
</div>
<div class="text-muted small mt-2" style="font-size: 0.7rem;">{"Scan to verify"}</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@@ -0,0 +1,601 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{console, js_sys};
use serde_json::json;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::{ResidentService, ResidentRegistration, ResidentRegistrationStatus};
use super::{SimpleStepInfo, StepPaymentStripe};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn createPaymentIntent(form_data: &JsValue) -> js_sys::Promise;
}
#[derive(Properties, PartialEq)]
pub struct SimpleResidentWizardProps {
pub on_registration_complete: Callback<DigitalResident>,
pub on_back_to_parent: Callback<()>,
#[prop_or_default]
pub success_resident_id: Option<u32>,
#[prop_or_default]
pub show_failure: bool,
}
pub enum SimpleResidentWizardMsg {
NextStep,
PrevStep,
UpdateFormData(DigitalResidentFormData),
ProcessRegistration,
RegistrationComplete(DigitalResident),
RegistrationError(String),
HideValidationToast,
ProcessPayment,
PaymentPlanChanged(ResidentPaymentPlan),
ConfirmationChanged(bool),
CreatePaymentIntent,
PaymentIntentCreated(String),
PaymentIntentError(String),
}
pub struct SimpleResidentWizard {
current_step: u8,
form_data: DigitalResidentFormData,
validation_errors: Vec<String>,
processing_registration: bool,
show_validation_toast: bool,
client_secret: Option<String>,
processing_payment: bool,
confirmation_checked: bool,
}
impl Component for SimpleResidentWizard {
type Message = SimpleResidentWizardMsg;
type Properties = SimpleResidentWizardProps;
fn create(ctx: &Context<Self>) -> Self {
// Determine initial step based on props - always start fresh for portal
let (form_data, current_step) = if ctx.props().success_resident_id.is_some() {
// Show success step
(DigitalResidentFormData::default(), 3)
} else if ctx.props().show_failure {
// Show failure, go back to payment step
(DigitalResidentFormData::default(), 2)
} else {
// Normal flow - always start from step 1 with fresh data
(DigitalResidentFormData::default(), 1)
};
Self {
current_step,
form_data,
validation_errors: Vec::new(),
processing_registration: false,
show_validation_toast: false,
client_secret: None,
processing_payment: false,
confirmation_checked: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
SimpleResidentWizardMsg::NextStep => {
// Validate current step
let validation_result = ResidentService::validate_resident_step(&self.form_data, self.current_step);
if !validation_result.is_valid {
self.validation_errors = validation_result.errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
return true;
}
if self.current_step < 3 {
if self.current_step == 2 {
// Process registration on final step
ctx.link().send_message(SimpleResidentWizardMsg::ProcessRegistration);
} else {
self.current_step += 1;
// If moving to payment step, create payment intent
if self.current_step == 2 {
ctx.link().send_message(SimpleResidentWizardMsg::CreatePaymentIntent);
}
}
true
} else {
false
}
}
SimpleResidentWizardMsg::PrevStep => {
if self.current_step > 1 {
self.current_step -= 1;
true
} else {
false
}
}
SimpleResidentWizardMsg::UpdateFormData(new_form_data) => {
self.form_data = new_form_data;
true
}
SimpleResidentWizardMsg::ProcessRegistration => {
self.processing_registration = true;
// Simulate registration processing
let link = ctx.link().clone();
let form_data = self.form_data.clone();
Timeout::new(2000, move || {
// Create resident and update registration status
match ResidentService::create_resident_from_form(&form_data) {
Ok(resident) => {
// For portal, we don't need to save registration drafts
// Just complete the registration process
link.send_message(SimpleResidentWizardMsg::RegistrationComplete(resident));
}
Err(error) => {
link.send_message(SimpleResidentWizardMsg::RegistrationError(error));
}
}
}).forget();
true
}
SimpleResidentWizardMsg::RegistrationComplete(resident) => {
self.processing_registration = false;
// Move to success step
self.current_step = 3;
// Notify parent component
ctx.props().on_registration_complete.emit(resident);
true
}
SimpleResidentWizardMsg::RegistrationError(error) => {
self.processing_registration = false;
// Stay on payment step and show error
self.validation_errors = vec![format!("Registration failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
true
}
SimpleResidentWizardMsg::HideValidationToast => {
self.show_validation_toast = false;
true
}
SimpleResidentWizardMsg::ProcessPayment => {
self.processing_payment = true;
true
}
SimpleResidentWizardMsg::PaymentPlanChanged(plan) => {
self.form_data.payment_plan = plan;
self.client_secret = None; // Reset client secret when plan changes
ctx.link().send_message(SimpleResidentWizardMsg::CreatePaymentIntent);
true
}
SimpleResidentWizardMsg::ConfirmationChanged(checked) => {
self.confirmation_checked = checked;
true
}
SimpleResidentWizardMsg::CreatePaymentIntent => {
console::log_1(&"🔧 Creating payment intent for resident registration...".into());
self.create_payment_intent(ctx);
false
}
SimpleResidentWizardMsg::PaymentIntentCreated(client_secret) => {
self.client_secret = Some(client_secret);
true
}
SimpleResidentWizardMsg::PaymentIntentError(error) => {
self.validation_errors = vec![format!("Payment setup failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let (step_title, step_description, step_icon) = self.get_step_info();
html! {
<div class="h-100 d-flex flex-column">
<form class="flex-grow-1 overflow-auto">
{self.render_current_step(ctx)}
</form>
{if self.current_step <= 2 {
self.render_footer_navigation(ctx)
} else {
html! {}
}}
{if self.show_validation_toast {
self.render_validation_toast(ctx)
} else {
html! {}
}}
</div>
}
}
}
impl SimpleResidentWizard {
fn render_current_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let form_data = self.form_data.clone();
let on_form_update = link.callback(SimpleResidentWizardMsg::UpdateFormData);
match self.current_step {
1 => html! {
<SimpleStepInfo
form_data={form_data}
on_change={on_form_update}
/>
},
2 => html! {
<StepPaymentStripe
form_data={form_data}
client_secret={self.client_secret.clone()}
processing_payment={self.processing_payment}
on_process_payment={link.callback(|_| SimpleResidentWizardMsg::ProcessPayment)}
on_payment_complete={link.callback(SimpleResidentWizardMsg::RegistrationComplete)}
on_payment_error={link.callback(SimpleResidentWizardMsg::RegistrationError)}
on_payment_plan_change={link.callback(SimpleResidentWizardMsg::PaymentPlanChanged)}
on_confirmation_change={link.callback(SimpleResidentWizardMsg::ConfirmationChanged)}
/>
},
3 => {
// Success step
self.render_success_step(ctx)
},
_ => html! { <div>{"Invalid step"}</div> }
}
}
fn render_footer_navigation(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
// Previous button (left)
<div style="width: 120px;">
{if self.current_step > 1 {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| SimpleResidentWizardMsg::PrevStep)}
disabled={self.processing_registration}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
// Step indicator (center)
<div class="d-flex align-items-center">
{for (1..=2).map(|step| {
let is_current = step == self.current_step;
let is_completed = step < self.current_step;
let step_class = if is_current {
"bg-primary text-white"
} else if is_completed {
"bg-success text-white"
} else {
"bg-white text-muted border"
};
html! {
<div class="d-flex align-items-center">
<div class={format!("rounded-circle d-flex align-items-center justify-content-center {} fw-bold", step_class)}
style="width: 28px; height: 28px; font-size: 12px;">
{if is_completed {
html! { <i class="bi bi-check"></i> }
} else {
html! { {step} }
}}
</div>
{if step < 2 {
html! {
<div class={format!("mx-1 {}", if is_completed { "bg-success" } else { "bg-secondary" })}
style="height: 2px; width: 24px;"></div>
}
} else {
html! {}
}}
</div>
}
})}
</div>
// Next/Register button (right)
<div style="width: 150px;" class="text-end">
{if self.current_step < 2 {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| SimpleResidentWizardMsg::NextStep)}
disabled={self.processing_registration}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else if self.current_step == 2 {
// Payment is handled by the StepPaymentStripe component itself
// No button needed here as the payment component has its own payment button
html! {}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_validation_toast(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let close_toast = link.callback(|_| SimpleResidentWizardMsg::HideValidationToast);
html! {
<div class="position-fixed bottom-0 start-50 translate-middle-x mb-3" style="z-index: 1055; max-width: 500px;">
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-warning text-dark">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong class="me-auto">{"Required Fields Missing"}</strong>
<button type="button" class="btn-close" onclick={close_toast} aria-label="Close"></button>
</div>
<div class="toast-body">
<div class="mb-2">
<strong>{"Please complete all required fields to continue:"}</strong>
</div>
<ul class="list-unstyled mb-0">
{for self.validation_errors.iter().map(|error| {
html! {
<li class="mb-1">
<i class="bi bi-dot text-danger me-1"></i>{error}
</li>
}
})}
</ul>
</div>
</div>
</div>
}
}
fn get_step_info(&self) -> (&'static str, &'static str, &'static str) {
match self.current_step {
1 => (
"Personal Information & KYC",
"Provide your basic information and complete identity verification.",
"bi-person-vcard"
),
2 => (
"Payment Plan & Legal Agreements",
"Choose your payment plan and review the legal agreements.",
"bi-credit-card"
),
3 => (
"Registration Complete",
"Your digital resident registration has been successfully completed.",
"bi-check-circle-fill"
),
_ => (
"Digital Resident Registration",
"Complete the registration process to become a digital resident.",
"bi-person-plus"
)
}
}
fn create_payment_intent(&self, ctx: &Context<Self>) {
let link = ctx.link().clone();
let form_data = self.form_data.clone();
spawn_local(async move {
match Self::setup_stripe_payment(form_data).await {
Ok(client_secret) => {
link.send_message(SimpleResidentWizardMsg::PaymentIntentCreated(client_secret));
}
Err(e) => {
link.send_message(SimpleResidentWizardMsg::PaymentIntentError(e));
}
}
});
}
async fn setup_stripe_payment(form_data: DigitalResidentFormData) -> Result<String, String> {
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
console::log_1(&"🔧 Setting up Stripe payment for resident registration".into());
console::log_1(&format!("📋 Resident: {}", form_data.full_name).into());
console::log_1(&format!("💳 Payment plan: {}", form_data.payment_plan.get_display_name()).into());
// Prepare form data for payment intent creation
let payment_data = json!({
"resident_name": form_data.full_name,
"email": form_data.email,
"phone": form_data.phone,
"date_of_birth": form_data.date_of_birth,
"nationality": form_data.nationality,
"passport_number": form_data.passport_number,
"address": form_data.current_address,
"payment_plan": form_data.payment_plan.get_display_name(),
"amount": form_data.payment_plan.get_price(),
"type": "resident_registration"
});
console::log_1(&"📡 Calling server endpoint for resident payment intent creation".into());
// Create request to server endpoint
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
let headers = js_sys::Map::new();
headers.set(&"Content-Type".into(), &"application/json".into());
opts.headers(&headers);
opts.body(Some(&JsValue::from_str(&payment_data.to_string())));
let request = Request::new_with_str_and_init(
"http://127.0.0.1:3001/resident/create-payment-intent",
&opts,
).map_err(|e| {
let error_msg = format!("Failed to create request: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
// Make the request
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
.map_err(|e| {
let error_msg = format!("Network request failed: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
let resp: Response = resp_value.dyn_into().unwrap();
if !resp.ok() {
let status = resp.status();
let error_msg = format!("Server error: HTTP {}", status);
console::log_1(&format!("{}", error_msg).into());
return Err(error_msg);
}
// Parse response
let json_value = JsFuture::from(resp.json().unwrap()).await
.map_err(|e| {
let error_msg = format!("Failed to parse response: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
// Extract client secret from response
let response_obj = js_sys::Object::from(json_value);
let client_secret_value = js_sys::Reflect::get(&response_obj, &"client_secret".into())
.map_err(|e| {
let error_msg = format!("No client_secret in response: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
let client_secret = client_secret_value.as_string()
.ok_or_else(|| {
let error_msg = "Invalid client secret received from server";
console::log_1(&format!("{}", error_msg).into());
error_msg.to_string()
})?;
console::log_1(&"✅ Payment intent created successfully".into());
console::log_1(&format!("🔑 Client secret received: {}", if client_secret.len() > 10 { "Yes" } else { "No" }).into());
Ok(client_secret)
}
fn render_success_step(&self, ctx: &Context<Self>) -> Html {
let resident_id = ctx.props().success_resident_id.unwrap_or(1);
html! {
<div class="text-center py-5">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h2 class="text-success mb-3">{"Registration Successful!"}</h2>
<p class="lead mb-4">
{"Your digital resident registration has been successfully submitted and is now pending approval."}
</p>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-body">
<h5 class="card-title text-success">
<i class="bi bi-info-circle me-2"></i>{"What happens next?"}
</h5>
<div class="text-start">
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-success rounded-pill">{"1"}</span>
</div>
<div>
<strong>{"Identity Verification"}</strong>
<p class="mb-0 text-muted">{"Our team will verify your identity and submitted documents."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-primary rounded-pill">{"2"}</span>
</div>
<div>
<strong>{"Background Check"}</strong>
<p class="mb-0 text-muted">{"We'll conduct necessary background checks and compliance verification."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-info rounded-pill">{"3"}</span>
</div>
<div>
<strong>{"Approval & Activation"}</strong>
<p class="mb-0 text-muted">{"Once approved, your digital resident status will be activated and you'll gain access to selected services."}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center">
<button
class="btn btn-success btn-lg"
onclick={ctx.props().on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-list me-2"></i>{"View My Registrations"}
</button>
</div>
</div>
<div class="mt-4">
<div class="alert alert-info">
<i class="bi bi-envelope me-2"></i>
{"You will receive email updates about your registration status. The approval process typically takes 3-5 business days."}
</div>
</div>
</div>
}
}
}

View File

@@ -0,0 +1,293 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::DigitalResidentFormData;
use super::ResidenceCard;
#[derive(Properties, PartialEq)]
pub struct SimpleStepInfoProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(SimpleStepInfo)]
pub fn simple_step_info(props: &SimpleStepInfoProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let show_private_key = use_state(|| false);
let kyc_completed = use_state(|| false);
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"full_name" => updated_data.full_name = value,
"email" => updated_data.email = value,
_ => {}
}
on_change.emit(updated_data);
})
};
let on_terms_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |_: Event| {
let mut updated_data = form_data.clone();
updated_data.legal_agreements.terms = !updated_data.legal_agreements.terms;
on_change.emit(updated_data);
})
};
let on_kyc_click = {
let kyc_completed = kyc_completed.clone();
Callback::from(move |_: MouseEvent| {
// Mock KYC completion
kyc_completed.set(true);
})
};
let on_generate_keys = {
let form_data = form_data.clone();
let on_change = on_change.clone();
let show_private_key = show_private_key.clone();
Callback::from(move |_: MouseEvent| {
// Generate secp256k1 keypair (simplified for demo)
let private_key = generate_private_key();
let public_key = generate_public_key(&private_key);
let mut updated_data = form_data.clone();
updated_data.public_key = Some(public_key);
updated_data.private_key = Some(private_key);
updated_data.private_key_shown = true;
show_private_key.set(true);
on_change.emit(updated_data);
})
};
let copy_private_key = {
let private_key = form_data.private_key.clone();
Callback::from(move |_: MouseEvent| {
if let Some(key) = &private_key {
// Copy to clipboard using a simple approach
web_sys::window()
.unwrap()
.alert_with_message(&format!("Private key copied! Please save it: {}", key))
.unwrap();
}
})
};
html! {
<>
<div class="row" style="padding: 2rem 1rem; height: 100%;">
// Left side - Form inputs
<div class="col-md-6" style="display: flex; flex-direction: column; justify-content: center;">
<div class="mb-3">
<label for="full_name" class="form-label text-muted" style="font-size: 0.875rem; font-weight: 500;">{"Full Name"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="full_name"
name="full_name"
value={form_data.full_name.clone()}
oninput={on_input.clone()}
placeholder="Enter your full legal name"
style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem; font-size: 0.9rem; transition: all 0.2s ease;"
title="As it appears on your government-issued ID"
/>
</div>
<div class="mb-4">
<label for="email" class="form-label text-muted" style="font-size: 0.875rem; font-weight: 500;">{"Email Address"} <span class="text-danger">{"*"}</span></label>
<input
type="email"
class="form-control"
id="email"
name="email"
value={form_data.email.clone()}
oninput={on_input.clone()}
placeholder="your.email@example.com"
style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem; font-size: 0.9rem; transition: all 0.2s ease;"
title="We'll use this to send you updates about your application"
/>
</div>
<div class="mb-4">
<div class="card" style="border: 1px solid #e0e0e0; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); min-height: 280px;">
<div class="card-header d-flex justify-content-between align-items-center" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-bottom: 1px solid #e0e0e0; border-radius: 12px 12px 0 0;">
<h6 class="mb-0 text-dark" style="font-size: 0.9rem; font-weight: 600;">
<i class="bi bi-key me-2" style="color: #6c757d;"></i>
{"Digital Identity Keys"}
</h6>
{if form_data.public_key.is_some() {
html! {
<button
type="button"
class="btn btn-sm btn-outline-secondary"
onclick={&on_generate_keys}
style="padding: 0.25rem 0.5rem; border-radius: 4px;"
title="Generate new keys"
>
<i class="bi bi-arrow-clockwise" style="font-size: 0.8rem;"></i>
</button>
}
} else {
html! {}
}}
</div>
<div class="card-body d-flex flex-column" style="padding: 1.25rem; min-height: 240px;">
{if form_data.public_key.is_none() {
html! {
<>
<p class="text-muted mb-3" style="font-size: 0.85rem; line-height: 1.5;">
{"Generate your unique cryptographic keys for secure digital identity. These keys will be used to authenticate your digital residence."}
</p>
<div class="flex-grow-1 d-flex flex-column justify-content-center">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="terms_agreement"
checked={form_data.legal_agreements.terms}
onchange={on_terms_change}
style="border-radius: 4px;"
/>
<label class="form-check-label text-muted" for="terms_agreement" style="font-size: 0.85rem;">
{"I agree to the "}<a href="#" class="text-decoration-none" style="color: #495057;">{"Terms of Service"}</a>{" and "}<a href="#" class="text-decoration-none" style="color: #495057;">{"Privacy Policy"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
</div>
<div class="d-grid">
<button
type="button"
class="btn"
onclick={&on_generate_keys}
disabled={!form_data.legal_agreements.terms}
style="background: linear-gradient(135deg, #495057 0%, #6c757d 100%); border: none; color: white; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s ease;"
>
<i class="bi bi-key me-2"></i>
{"Generate Keys"}
</button>
</div>
</div>
</>
}
} else {
html! {
<>
{if *show_private_key && form_data.private_key.is_some() {
html! {
<div class="mb-3">
<label class="form-label text-muted mb-2" style="font-size: 0.75rem; font-weight: 500;">{"Private Key"}</label>
<div class="bg-dark text-light p-3 rounded position-relative" style="font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 0.7rem; word-break: break-all; line-height: 1.4; border: 1px solid #495057;">
<div style="padding-right: 2.5rem;">
{form_data.private_key.as_ref().unwrap_or(&"".to_string())}
</div>
<button
type="button"
class="btn btn-sm position-absolute top-0 end-0 m-2"
onclick={copy_private_key}
style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem;"
title="Copy private key"
>
<i class="bi bi-copy" style="color: white; font-size: 0.9rem;"></i>
</button>
</div>
<div class="alert alert-warning mt-2 py-2" style="border-radius: 6px; font-size: 0.75rem;">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>{"Warning:"}</strong> {"Store this private key safely. You cannot recover it if lost!"}
</div>
</div>
}
} else {
html! {}
}}
<div class="mb-3">
<label class="form-label text-muted" style="font-size: 0.75rem; font-weight: 500;">{"Public Key"}</label>
<div class="form-control" style="background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 6px; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 0.7rem; word-break: break-all; min-height: 60px; line-height: 1.4; color: #495057;">
{form_data.public_key.as_ref().unwrap_or(&"".to_string())}
</div>
</div>
</>
}
}}
</div>
</div>
</div>
<div class="mb-4">
<div class="d-grid">
{if *kyc_completed {
html! {
<button
type="button"
class="btn btn-success"
disabled=true
style="padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500;"
>
<i class="bi bi-check-circle-fill me-2"></i>
{"KYC Verification Complete"}
</button>
}
} else {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={on_kyc_click}
style="border: 1px solid #e0e0e0; color: #495057; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s ease;"
>
<i class="bi bi-shield-check me-2"></i>
{"Complete KYC Verification"}
</button>
}
}}
</div>
</div>
</div>
// Right side - Residence card (vertically centered)
<div class="col-md-6 d-flex align-items-center justify-content-center">
<ResidenceCard form_data={form_data.clone()} />
</div>
</div>
</>
}
}
// Simplified key generation functions (for demo purposes)
fn generate_private_key() -> String {
// In a real implementation, this would use proper secp256k1 key generation
// For demo purposes, we'll generate a hex string
use js_sys::Math;
let mut key = String::new();
for _ in 0..64 {
let digit = (Math::random() * 16.0) as u8;
key.push_str(&format!("{:x}", digit));
}
key
}
fn generate_public_key(private_key: &str) -> String {
// In a real implementation, this would derive the public key from the private key
// For demo purposes, we'll generate a different hex string
use js_sys::Math;
let mut key = String::from("04"); // Uncompressed public key prefix
for _ in 0..128 {
let digit = (Math::random() * 16.0) as u8;
key.push_str(&format!("{:x}", digit));
}
key
}

View File

@@ -0,0 +1,336 @@
use yew::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{window, console, js_sys};
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::ResidentService;
use super::ResidenceCard;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn confirmStripePayment(client_secret: &str) -> js_sys::Promise;
#[wasm_bindgen(js_namespace = window)]
fn initializeStripeElements(client_secret: &str);
}
#[derive(Properties, PartialEq)]
pub struct StepPaymentStripeProps {
pub form_data: DigitalResidentFormData,
pub client_secret: Option<String>,
pub processing_payment: bool,
pub on_process_payment: Callback<()>,
pub on_payment_complete: Callback<DigitalResident>,
pub on_payment_error: Callback<String>,
pub on_payment_plan_change: Callback<ResidentPaymentPlan>,
pub on_confirmation_change: Callback<bool>,
}
pub enum StepPaymentStripeMsg {
ProcessPayment,
PaymentComplete,
PaymentError(String),
PaymentPlanChanged(ResidentPaymentPlan),
ToggleConfirmation,
}
pub struct StepPaymentStripe {
form_data: DigitalResidentFormData,
payment_error: Option<String>,
selected_payment_plan: ResidentPaymentPlan,
confirmation_checked: bool,
}
impl Component for StepPaymentStripe {
type Message = StepPaymentStripeMsg;
type Properties = StepPaymentStripeProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
payment_error: None,
selected_payment_plan: ctx.props().form_data.payment_plan.clone(),
confirmation_checked: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepPaymentStripeMsg::ProcessPayment => {
if let Some(client_secret) = &ctx.props().client_secret {
console::log_1(&"🔄 User clicked 'Complete Payment' - processing with Stripe".into());
self.process_stripe_payment(ctx, client_secret.clone());
} else {
console::log_1(&"❌ No client secret available for payment".into());
self.payment_error = Some("Payment not ready. Please try again.".to_string());
}
return false;
}
StepPaymentStripeMsg::PaymentComplete => {
console::log_1(&"✅ Payment completed successfully".into());
// Create resident from form data with current payment plan
let mut updated_form_data = self.form_data.clone();
updated_form_data.payment_plan = self.selected_payment_plan.clone();
match ResidentService::create_resident_from_form(&updated_form_data) {
Ok(resident) => {
ctx.props().on_payment_complete.emit(resident);
}
Err(e) => {
console::log_1(&format!("❌ Failed to create resident: {}", e).into());
ctx.props().on_payment_error.emit(format!("Failed to create resident: {}", e));
}
}
return false;
}
StepPaymentStripeMsg::PaymentError(error) => {
console::log_1(&format!("❌ Payment failed: {}", error).into());
self.payment_error = Some(error.clone());
ctx.props().on_payment_error.emit(error);
}
StepPaymentStripeMsg::PaymentPlanChanged(plan) => {
console::log_1(&format!("💳 Payment plan changed to: {}", plan.get_display_name()).into());
self.selected_payment_plan = plan.clone();
self.payment_error = None; // Clear any previous errors
// Notify parent to create new payment intent
ctx.props().on_payment_plan_change.emit(plan);
return true;
}
StepPaymentStripeMsg::ToggleConfirmation => {
self.confirmation_checked = !self.confirmation_checked;
console::log_1(&format!("📋 Confirmation checkbox toggled: {}", self.confirmation_checked).into());
// Notify parent of confirmation state change
ctx.props().on_confirmation_change.emit(self.confirmation_checked);
}
}
true
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
// Update selected payment plan if it changed from parent
if self.selected_payment_plan != ctx.props().form_data.payment_plan {
self.selected_payment_plan = ctx.props().form_data.payment_plan.clone();
}
// Initialize Stripe Elements if client secret became available
if old_props.client_secret.is_none() && ctx.props().client_secret.is_some() {
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
true
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
// Initialize Stripe Elements if client secret is available
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let has_client_secret = ctx.props().client_secret.is_some();
let can_process_payment = has_client_secret && !ctx.props().processing_payment && self.confirmation_checked;
html! {
<div class="step-content" style="padding: 2rem 1rem; height: 100%;">
<div class="row h-100">
// Left side - Payment form
<div class="col-md-6" style="display: flex; flex-direction: column; justify-content: center;">
// Payment Form with integrated fee display
<div class="mb-4">
<h6 class="text-muted mb-3" style="font-size: 0.9rem; font-weight: 600;">
{"Payment Information"} <span class="text-danger">{"*"}</span>
</h6>
<div class="card" id="payment-information-section" style="border: 1px solid #e0e0e0; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="card-header" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-bottom: 1px solid #e0e0e0; border-radius: 12px 12px 0 0;">
<h6 class="mb-0 text-dark" style="font-size: 0.85rem; font-weight: 600;">
<i class="bi bi-shield-check me-2" style="color: #6c757d;"></i>{"Secure Payment Processing"}
</h6>
</div>
<div class="card-body" style="padding: 1.25rem;">
// Fee display at top of payment card
<div class="mb-3 p-3 rounded" style="background: #f8f9fa; border: 1px solid #e0e0e0;">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="text-muted" style="font-size: 0.75rem; font-weight: 500;">{"Digital Residence Fee"}</div>
<h6 class="mb-0" style="color: #495057; font-weight: 600;">{"$2.00 / month"}</h6>
<small class="text-muted" style="font-size: 0.7rem;">{"Monthly maintenance fee"}</small>
</div>
<i class="bi bi-calendar-month" style="font-size: 1.25rem; color: #6c757d;"></i>
</div>
</div>
// Stripe Elements will be mounted here
<div id="payment-element" style="min-height: 40px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #ffffff;">
{if ctx.props().processing_payment {
html! {
<div class="text-center py-4">
<div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted" style="font-size: 0.85rem;">{"Processing payment..."}</p>
</div>
}
} else if !has_client_secret {
html! {
<div class="text-center py-4">
<div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted" style="font-size: 0.85rem;">{"Preparing payment form..."}</p>
</div>
}
} else {
html! {}
}}
</div>
// Payment button
{if has_client_secret && !ctx.props().processing_payment {
html! {
<div class="d-grid mt-3">
<button
type="button"
class="btn"
onclick={link.callback(|_| StepPaymentStripeMsg::ProcessPayment)}
style="background: linear-gradient(135deg, #495057 0%, #6c757d 100%); border: none; color: white; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s ease;"
>
<i class="bi bi-credit-card me-2"></i>
{"Complete Payment - $2.00"}
</button>
</div>
}
} else {
html! {}
}}
{if let Some(error) = &self.payment_error {
html! {
<div id="payment-errors" class="alert alert-danger mt-3" style="border-radius: 6px; font-size: 0.85rem;">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>{"Payment Error: "}</strong>{error}
</div>
}
} else {
html! {
<div id="payment-errors" class="alert alert-danger mt-3" style="display: none;"></div>
}
}}
</div>
</div>
</div>
</div>
// Right side - Residence card (vertically centered)
<div class="col-md-6 d-flex align-items-center justify-content-center">
<ResidenceCard form_data={self.form_data.clone()} />
</div>
</div>
</div>
}
}
}
impl StepPaymentStripe {
fn render_payment_plan_option(&self, ctx: &Context<Self>, plan: ResidentPaymentPlan, title: &str, description: &str, icon: &str) -> Html {
let link = ctx.link();
let is_selected = self.selected_payment_plan == plan;
let card_style = if is_selected {
"border: 2px solid #495057; background: #f8f9fa;"
} else {
"border: 1px solid #e0e0e0; background: white;"
};
let on_select = link.callback(move |_| StepPaymentStripeMsg::PaymentPlanChanged(plan.clone()));
// Get pricing for this plan
let price = plan.get_price();
html! {
<div class="col-12">
<div class="card mb-3" style={format!("cursor: pointer; border-radius: 8px; transition: all 0.2s ease; {}", card_style)} onclick={on_select}>
<div class="card-body" style="padding: 1rem;">
<div class="d-flex align-items-center">
<i class={format!("bi {} me-3", icon)} style="font-size: 1.5rem; color: #6c757d;"></i>
<div class="flex-grow-1">
<h6 class="card-title mb-1" style="font-size: 0.9rem; font-weight: 600;">{title}</h6>
<p class="card-text text-muted mb-0" style="font-size: 0.75rem;">{description}</p>
<div class="mt-1">
<span class="fw-bold" style="color: #495057; font-size: 0.9rem;">{format!("${:.2}", price)}</span>
{if plan == ResidentPaymentPlan::Yearly {
html! {
<span class="badge ms-2" style="background: #495057; color: white; font-size: 0.65rem;">
{"17% OFF"}
</span>
}
} else if plan == ResidentPaymentPlan::Lifetime {
html! {
<span class="badge ms-2" style="background: #ffc107; color: #212529; font-size: 0.65rem;">
{"BEST VALUE"}
</span>
}
} else {
html! {}
}}
</div>
</div>
<div class="text-end">
{if is_selected {
html! {
<i class="bi bi-check-circle-fill" style="font-size: 1.25rem; color: #495057;"></i>
}
} else {
html! {
<i class="bi bi-circle text-muted" style="font-size: 1.25rem;"></i>
}
}}
</div>
</div>
</div>
</div>
</div>
}
}
fn process_stripe_payment(&mut self, ctx: &Context<Self>, client_secret: String) {
let link = ctx.link().clone();
// Trigger parent to show processing state
ctx.props().on_process_payment.emit(());
spawn_local(async move {
match Self::confirm_payment(&client_secret).await {
Ok(_) => {
link.send_message(StepPaymentStripeMsg::PaymentComplete);
}
Err(e) => {
link.send_message(StepPaymentStripeMsg::PaymentError(e));
}
}
});
}
async fn confirm_payment(client_secret: &str) -> Result<(), String> {
use wasm_bindgen_futures::JsFuture;
console::log_1(&"🔄 Confirming payment with Stripe...".into());
// Call JavaScript function to confirm payment
let promise = confirmStripePayment(client_secret);
JsFuture::from(promise).await
.map_err(|e| format!("Payment confirmation failed: {:?}", e))?;
console::log_1(&"✅ Payment confirmed successfully".into());
Ok(())
}
}

View File

@@ -0,0 +1,7 @@
pub mod entities;
pub mod resident_landing_overlay;
pub mod portal_home;
pub use entities::*;
pub use resident_landing_overlay::*;
pub use portal_home::PortalHome;

View File

@@ -0,0 +1,253 @@
use yew::prelude::*;
use crate::components::entities::resident_registration::ResidenceCard;
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct PortalHomeProps {
pub user_name: String,
pub registration_data: Option<DigitalResidentFormData>,
}
#[function_component(PortalHome)]
pub fn portal_home(props: &PortalHomeProps) -> Html {
let weather_data = use_state(|| WeatherData::default());
// Mock weather data for Zanzibar (in a real app, this would be fetched from an API)
use_effect_with((), {
let weather_data = weather_data.clone();
move |_| {
// Simulate API call with realistic Zanzibar weather
weather_data.set(WeatherData {
temperature: 28,
condition: "Partly Cloudy".to_string(),
humidity: 75,
wind_speed: 12,
icon: "bi-cloud-sun".to_string(),
});
|| ()
}
});
html! {
<>
<style>
{r#"
.portal-card {
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid #e9ecef;
border-radius: 12px;
}
.portal-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
border-color: #dee2e6;
}
.gradient-accent {
background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);
}
.weather-icon {
font-size: 1.4rem;
}
.platform-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: white;
}
.welcome-section {
background: linear-gradient(135deg, rgba(0,153,255,0.05) 0%, rgba(0,204,102,0.05) 100%);
border-radius: 16px;
border: 1px solid rgba(0,153,255,0.1);
}
.user-button {
background: #6c757d;
border: none;
border-radius: 25px;
color: white;
padding: 8px 20px;
font-weight: 600;
transition: all 0.2s ease;
}
.user-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108,117,125,0.3);
color: white;
background: #5a6268;
}
"#}
</style>
// Header
<header class="bg-white border-bottom shadow-sm">
<div class="container-fluid">
<div class="row align-items-center py-3">
<div class="col-md-6">
<div class="d-flex align-items-center">
<div class="gradient-accent rounded-3 me-3" style="width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-geo-alt-fill text-white" style="font-size: 1.2rem;"></i>
</div>
<div>
<h5 class="mb-0 fw-bold text-dark">{"Zanzibar Digital Freezone"}</h5>
<small class="text-muted">{"Portal"}</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center justify-content-end">
<div class="me-4 text-end">
<div class="d-flex align-items-center justify-content-end">
<i class={format!("bi {} me-2 weather-icon text-primary", weather_data.icon)}></i>
<span class="fw-semibold">{weather_data.temperature}{"°C"}</span>
<small class="text-muted ms-2">{&weather_data.condition}</small>
</div>
<small class="text-muted">{"Stone Town, Zanzibar"}</small>
</div>
<div class="vr me-3"></div>
<button class="btn user-button">
<i class="bi bi-person-circle me-2"></i>
{&props.user_name}
</button>
</div>
</div>
</div>
</div>
</header>
// Main Content
<main class="bg-light min-vh-100">
<div class="container-fluid py-5">
<div class="row align-items-start">
// Left Column: Welcome Section and Platform Cards
<div class="col-lg-6 mb-4">
// Welcome Section
<div class="welcome-section p-5 mb-4">
<div class="mb-4">
<h1 class="display-5 fw-bold text-dark mb-3">
{"Welcome to your "}
<span class="text-primary">{"Digital Freezone"}</span>
</h1>
<p class="lead text-muted mb-4">
{"You are now digitally present in Zanzibar. Access your residency services, "}
{"manage your digital identity, and explore the freezone ecosystem."}
</p>
</div>
<div class="alert alert-info border-0 bg-info bg-opacity-10">
<div class="d-flex align-items-center">
<i class="bi bi-info-circle-fill text-info me-2"></i>
<small class="text-info mb-0">
{"Your digital residency gives you access to all freezone services and platforms."}
</small>
</div>
</div>
</div>
// Platform Cards
<div class="d-flex flex-column gap-3">
// Marketplace Card
<div class="card portal-card bg-white">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="platform-icon me-3" style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);">
<i class="bi bi-shop"></i>
</div>
<div class="flex-grow-1">
<h5 class="card-title fw-bold mb-1">{"Digital Marketplace"}</h5>
<p class="card-text text-muted small mb-0">
{"Trade digital assets, services, and products within the freezone ecosystem"}
</p>
</div>
<i class="bi bi-arrow-right text-muted" style="font-size: 1.2rem;"></i>
</div>
</div>
</div>
// Platform Administration Card
<div class="card portal-card bg-white">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="platform-icon me-3 gradient-accent">
<i class="bi bi-building"></i>
</div>
<div class="flex-grow-1">
<h5 class="card-title fw-bold mb-1">{"Freezone Platform"}</h5>
<p class="card-text text-muted small mb-0">
{"Manage your digital residency, register companies, and access admin services"}
</p>
</div>
<i class="bi bi-arrow-right text-muted" style="font-size: 1.2rem;"></i>
</div>
</div>
</div>
// DeFi Platform Card
<div class="card portal-card bg-white">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="platform-icon me-3" style="background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);">
<i class="bi bi-currency-bitcoin"></i>
</div>
<div class="flex-grow-1">
<h5 class="card-title fw-bold mb-1">{"DeFi Platform"}</h5>
<p class="card-text text-muted small mb-0">
{"Access decentralized finance services, trading, and blockchain tools"}
</p>
</div>
<i class="bi bi-arrow-right text-muted" style="font-size: 1.2rem;"></i>
</div>
</div>
</div>
</div>
</div>
// Right Column: Residence Card
<div class="col-lg-6 mb-4">
<div class="d-flex justify-content-center">
<ResidenceCard
form_data={
if let Some(data) = &props.registration_data {
data.clone()
} else {
DigitalResidentFormData {
full_name: props.user_name.clone(),
email: "resident@zanzibar-freezone.com".to_string(),
public_key: Some("zdf1qxy2mlyjkjkpskpsw9fxtpugs450add72nyktmzqau...".to_string()),
..DigitalResidentFormData::default()
}
}
}
/>
</div>
</div>
</div>
</div>
</main>
</>
}
}
#[derive(Clone, PartialEq)]
struct WeatherData {
temperature: i32,
condition: String,
humidity: i32,
wind_speed: i32,
icon: String,
}
impl Default for WeatherData {
fn default() -> Self {
Self {
temperature: 25,
condition: "Loading...".to_string(),
humidity: 70,
wind_speed: 10,
icon: "bi-cloud".to_string(),
}
}
}

View File

@@ -0,0 +1,358 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::{DigitalResidentFormData, DigitalResident};
use crate::components::entities::resident_registration::SimpleResidentWizard;
#[derive(Properties, PartialEq)]
pub struct ResidentLandingOverlayProps {
pub on_registration_complete: Callback<DigitalResident>,
pub on_sign_in: Callback<(String, String)>, // private_key, unused
pub on_close: Option<Callback<()>>,
}
pub enum ResidentLandingMsg {
ShowSignIn,
ShowRegister,
UpdatePrivateKey(String),
SignIn,
StartRegistration,
RegistrationComplete(DigitalResident),
BackToLanding,
}
pub struct ResidentLandingOverlay {
view_mode: ViewMode,
private_key: String,
show_registration_wizard: bool,
}
#[derive(PartialEq)]
enum ViewMode {
Landing,
SignIn,
Register,
}
impl Component for ResidentLandingOverlay {
type Message = ResidentLandingMsg;
type Properties = ResidentLandingOverlayProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
view_mode: ViewMode::SignIn,
private_key: String::new(),
show_registration_wizard: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ResidentLandingMsg::ShowSignIn => {
self.view_mode = ViewMode::SignIn;
self.show_registration_wizard = false;
true
}
ResidentLandingMsg::ShowRegister => {
self.view_mode = ViewMode::Register;
self.show_registration_wizard = false;
true
}
ResidentLandingMsg::UpdatePrivateKey(private_key) => {
self.private_key = private_key;
true
}
ResidentLandingMsg::SignIn => {
// For now, use the private key as both email and password
// In a real implementation, you'd derive the public key and look up the user
ctx.props().on_sign_in.emit((self.private_key.clone(), "".to_string()));
false
}
ResidentLandingMsg::StartRegistration => {
self.view_mode = ViewMode::Register;
self.show_registration_wizard = true;
true
}
ResidentLandingMsg::RegistrationComplete(resident) => {
ctx.props().on_registration_complete.emit(resident);
false
}
ResidentLandingMsg::BackToLanding => {
self.view_mode = ViewMode::Landing;
self.show_registration_wizard = false;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<style>
{"@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }"}
</style>
<div class="position-fixed top-0 start-0 w-100 h-100 d-flex" style="z-index: 9999; background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);">
{self.render_content(ctx)}
// Close button (if callback provided)
{if ctx.props().on_close.is_some() {
html! {
<button
class="btn btn-outline-light position-absolute top-0 end-0 m-3"
style="z-index: 10000;"
onclick={ctx.props().on_close.as_ref().unwrap().reform(|_| ())}
>
<i class="bi bi-x-lg"></i>
</button>
}
} else {
html! {}
}}
</div>
</>
}
}
}
impl ResidentLandingOverlay {
fn render_content(&self, ctx: &Context<Self>) -> Html {
// Determine column sizes based on view mode
let (left_col_class, right_col_class) = match self.view_mode {
ViewMode::Register if self.show_registration_wizard => ("col-lg-4", "col-lg-8"),
_ => ("col-lg-7", "col-lg-5"),
};
html! {
<div class="container-fluid h-100">
<div class="row h-100">
// Left side - Branding and description (shrinks when registration is active)
<div class={format!("{} d-flex align-items-center justify-content-center text-white p-5 transition-all", left_col_class)}
style="transition: all 0.5s ease-in-out;">
<div class="text-center text-lg-start" style="max-width: 600px;">
<h1 class="display-4 fw-bold mb-4">
{"Zanzibar Digital Freezone"}
</h1>
<h2 class="h3 mb-4 text-white-75">
{"Your Portal to Digital Residency"}
</h2>
<p class="lead mb-4 text-white-75">
{"An accelerator of digitalization where governance meets real-world digital asset trade. Participate in a regulated freezone that bridges traditional business with blockchain technology and decentralized finance."}
</p>
{if !self.show_registration_wizard {
html! {
<div class="row text-center mt-5">
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-shield-check display-6 mb-2"></i>
<h6 class="fw-bold">{"Secure Identity"}</h6>
<small class="text-white-75">{"Blockchain-verified digital identity"}</small>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-building display-6 mb-2"></i>
<h6 class="fw-bold">{"Business Ready"}</h6>
<small class="text-white-75">{"Register companies in minutes"}</small>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-globe display-6 mb-2"></i>
<h6 class="fw-bold">{"Global Access"}</h6>
<small class="text-white-75">{"Worldwide financial services"}</small>
</div>
</div>
</div>
}
} else {
html! {}
}}
</div>
</div>
// Right side - Sign in/Register form (expands when registration is active)
<div class={format!("{} d-flex align-items-center justify-content-center bg-white", right_col_class)}
style="transition: all 0.5s ease-in-out;">
<div class="w-100 h-100" style={if self.show_registration_wizard { "padding: 1rem;" } else { "max-width: 400px; padding: 2rem;" }}>
{match self.view_mode {
ViewMode::Landing => self.render_landing_form(ctx),
ViewMode::SignIn => self.render_sign_in_form(ctx),
ViewMode::Register if self.show_registration_wizard => self.render_embedded_registration_wizard(ctx),
ViewMode::Register => self.render_register_form(ctx),
}}
</div>
</div>
</div>
</div>
}
}
fn render_landing_form(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="text-center">
<div class="mb-4">
<i class="bi bi-person-circle text-primary" style="font-size: 4rem;"></i>
</div>
<h3 class="mb-4">{"Welcome to ZDF"}</h3>
<p class="text-muted mb-4">
{"Get started with your digital residency journey"}
</p>
<div class="d-grid gap-3">
<button
class="btn btn-primary btn-lg"
onclick={link.callback(|_| ResidentLandingMsg::StartRegistration)}
>
<i class="bi bi-person-plus me-2"></i>
{"Become a Digital Resident"}
</button>
<button
class="btn btn-outline-primary btn-lg"
onclick={link.callback(|_| ResidentLandingMsg::ShowSignIn)}
>
<i class="bi bi-box-arrow-in-right me-2"></i>
{"Sign In to Your Account"}
</button>
</div>
<div class="mt-4 pt-4 border-top">
<small class="text-muted">
{"Already have an account? "}
<a href="#" class="text-primary text-decoration-none" onclick={link.callback(|e: MouseEvent| {
e.prevent_default();
ResidentLandingMsg::ShowSignIn
})}>
{"Sign in here"}
</a>
</small>
</div>
</div>
}
}
fn render_sign_in_form(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_private_key_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdatePrivateKey(input.value()));
})
};
let on_submit = {
let link = link.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
link.send_message(ResidentLandingMsg::SignIn);
})
};
html! {
<div class="d-flex flex-column h-100 justify-content-center" style="max-width: 400px; margin: 0 auto; padding: 2rem;">
<div class="text-center mb-4">
<div class="mb-3">
<i class="bi bi-key" style="font-size: 3rem; color: #495057;"></i>
</div>
<h3 class="mb-2" style="color: #495057; font-weight: 600;">{"Welcome Back"}</h3>
<p class="text-muted" style="font-size: 0.9rem;">{"Sign in with your private key"}</p>
</div>
<form onsubmit={on_submit}>
<div class="mb-4">
<label for="signin-private-key" class="form-label text-muted" style="font-size: 0.875rem; font-weight: 500;">{"Private Key"}</label>
<textarea
class="form-control"
id="signin-private-key"
value={self.private_key.clone()}
oninput={on_private_key_input}
placeholder="Enter your private key..."
rows="4"
style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem; font-size: 0.8rem; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; transition: all 0.2s ease; resize: vertical;"
required={true}
/>
<small class="text-muted">{"Your private key is used to securely access your digital residency account."}</small>
</div>
<div class="d-grid mb-4">
<button type="submit" class="btn" style="background: linear-gradient(135deg, #495057 0%, #6c757d 100%); border: none; color: white; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s ease;">
<i class="bi bi-key me-2"></i>
{"Sign In"}
</button>
</div>
</form>
<div class="text-center mb-4">
<div class="position-relative">
<hr style="border-color: #e0e0e0;"/>
<span class="position-absolute top-50 start-50 translate-middle bg-white px-3 text-muted" style="font-size: 0.8rem;">{"New to ZDF?"}</span>
</div>
</div>
<div class="d-grid mb-3">
<button
type="button"
class="btn btn-outline-primary"
onclick={link.callback(|_| ResidentLandingMsg::StartRegistration)}
style="border: 1px solid #495057; color: #495057; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s ease;"
>
<i class="bi bi-person-plus me-2"></i>
{"Become a Digital Resident"}
</button>
</div>
<div class="text-center mt-auto">
<small class="text-muted" style="font-size: 0.75rem;">
{"Lost your private key? "}
<a href="#" class="text-decoration-none" style="color: #495057;">{"Recovery options"}</a>
</small>
</div>
</div>
}
}
fn render_register_form(&self, _ctx: &Context<Self>) -> Html {
// This form is no longer used - registration goes directly to the wizard
html! {
<div class="text-center">
<h3>{"Registration"}</h3>
<p class="text-muted">{"Redirecting to registration wizard..."}</p>
</div>
}
}
fn render_embedded_registration_wizard(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="h-100 d-flex flex-column">
// Header with back button (always visible)
<div class="d-flex justify-content-between align-items-center p-3 border-bottom">
<h4 class="mb-0">{"Digital Resident Registration"}</h4>
<button
class="btn btn-outline-secondary btn-sm"
onclick={link.callback(|_| ResidentLandingMsg::BackToLanding)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
// Registration wizard content with fade-in animation
<div class="flex-grow-1 overflow-auto"
style="opacity: 0; animation: fadeIn 0.5s ease-in-out 0.25s forwards;">
<SimpleResidentWizard
on_registration_complete={link.callback(ResidentLandingMsg::RegistrationComplete)}
on_back_to_parent={link.callback(|_| ResidentLandingMsg::BackToLanding)}
success_resident_id={None}
show_failure={false}
/>
</div>
</div>
}
}
}

16
portal/src/lib.rs Normal file
View File

@@ -0,0 +1,16 @@
use wasm_bindgen::prelude::*;
mod app;
mod components;
mod models;
mod services;
use app::App;
// This is the entry point for the web app
#[wasm_bindgen(start)]
pub fn run_app() {
wasm_logger::init(wasm_logger::Config::default());
log::info!("Starting Zanzibar Digital Freezone Portal");
yew::Renderer::<App>::new().render();
}

View File

@@ -0,0 +1,747 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct Company {
pub id: u32,
pub name: String,
pub company_type: CompanyType,
pub status: CompanyStatus,
pub registration_number: String,
pub incorporation_date: String,
pub email: Option<String>,
pub phone: Option<String>,
pub website: Option<String>,
pub address: Option<String>,
pub industry: Option<String>,
pub description: Option<String>,
pub fiscal_year_end: Option<String>,
pub shareholders: Vec<Shareholder>,
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum CompanyType {
SingleFZC,
StartupFZC,
GrowthFZC,
GlobalFZC,
CooperativeFZC,
}
impl CompanyType {
pub fn to_string(&self) -> String {
match self {
CompanyType::SingleFZC => "Single FZC".to_string(),
CompanyType::StartupFZC => "Startup FZC".to_string(),
CompanyType::GrowthFZC => "Growth FZC".to_string(),
CompanyType::GlobalFZC => "Global FZC".to_string(),
CompanyType::CooperativeFZC => "Cooperative FZC".to_string(),
}
}
pub fn from_string(s: &str) -> Option<Self> {
match s {
"Single FZC" => Some(CompanyType::SingleFZC),
"Startup FZC" => Some(CompanyType::StartupFZC),
"Growth FZC" => Some(CompanyType::GrowthFZC),
"Global FZC" => Some(CompanyType::GlobalFZC),
"Cooperative FZC" => Some(CompanyType::CooperativeFZC),
_ => None,
}
}
pub fn get_pricing(&self) -> CompanyPricing {
match self {
CompanyType::SingleFZC => CompanyPricing {
setup_fee: 20.0,
monthly_fee: 20.0,
max_shareholders: 1,
features: vec![
"1 shareholder".to_string(),
"Cannot issue digital assets".to_string(),
"Can hold external shares".to_string(),
"Connect to bank".to_string(),
"Participate in ecosystem".to_string(),
],
},
CompanyType::StartupFZC => CompanyPricing {
setup_fee: 50.0,
monthly_fee: 50.0,
max_shareholders: 5,
features: vec![
"Up to 5 shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
],
},
CompanyType::GrowthFZC => CompanyPricing {
setup_fee: 100.0,
monthly_fee: 100.0,
max_shareholders: 20,
features: vec![
"Up to 20 shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
"Hold physical assets".to_string(),
],
},
CompanyType::GlobalFZC => CompanyPricing {
setup_fee: 2000.0,
monthly_fee: 200.0,
max_shareholders: 999,
features: vec![
"Unlimited shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
"Hold physical assets".to_string(),
],
},
CompanyType::CooperativeFZC => CompanyPricing {
setup_fee: 2000.0,
monthly_fee: 200.0,
max_shareholders: 999,
features: vec![
"Unlimited members".to_string(),
"Democratic governance".to_string(),
"Collective decision-making".to_string(),
"Equitable distribution".to_string(),
],
},
}
}
pub fn get_capabilities(&self) -> HashMap<String, bool> {
let mut capabilities = HashMap::new();
// All types have these basic capabilities
capabilities.insert("digital_assets".to_string(), true);
capabilities.insert("ecosystem".to_string(), true);
capabilities.insert("ai_dispute".to_string(), true);
capabilities.insert("digital_signing".to_string(), true);
capabilities.insert("external_shares".to_string(), true);
capabilities.insert("bank_account".to_string(), true);
// Type-specific capabilities
match self {
CompanyType::SingleFZC => {
capabilities.insert("issue_assets".to_string(), false);
capabilities.insert("physical_assets".to_string(), false);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::StartupFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), false);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::GrowthFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::GlobalFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::CooperativeFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), true);
capabilities.insert("collective".to_string(), true);
},
}
capabilities
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum CompanyStatus {
Active,
Inactive,
Suspended,
PendingPayment,
}
impl CompanyStatus {
pub fn to_string(&self) -> String {
match self {
CompanyStatus::Active => "Active".to_string(),
CompanyStatus::Inactive => "Inactive".to_string(),
CompanyStatus::Suspended => "Suspended".to_string(),
CompanyStatus::PendingPayment => "Pending Payment".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
CompanyStatus::Active => "badge bg-success".to_string(),
CompanyStatus::Inactive => "badge bg-secondary".to_string(),
CompanyStatus::Suspended => "badge bg-warning text-dark".to_string(),
CompanyStatus::PendingPayment => "badge bg-info".to_string(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CompanyPricing {
pub setup_fee: f64,
pub monthly_fee: f64,
pub max_shareholders: u32,
pub features: Vec<String>,
}
// Registration form data
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CompanyFormData {
// Step 1: General Information
pub company_name: String,
pub company_email: String,
pub company_phone: String,
pub company_website: Option<String>,
pub company_address: String,
pub company_industry: Option<String>,
pub company_purpose: Option<String>,
pub fiscal_year_end: Option<String>,
// Step 2: Company Type
pub company_type: CompanyType,
// Step 3: Shareholders
pub shareholder_structure: ShareholderStructure,
pub shareholders: Vec<Shareholder>,
// Step 4: Payment & Agreements
pub payment_plan: PaymentPlan,
pub legal_agreements: LegalAgreements,
}
impl Default for CompanyFormData {
fn default() -> Self {
Self {
company_name: String::new(),
company_email: String::new(),
company_phone: String::new(),
company_website: None,
company_address: String::new(),
company_industry: None,
company_purpose: None,
fiscal_year_end: None,
company_type: CompanyType::StartupFZC,
shareholder_structure: ShareholderStructure::Equal,
shareholders: vec![Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
}],
payment_plan: PaymentPlan::Monthly,
legal_agreements: LegalAgreements::default(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct Shareholder {
pub name: String,
pub resident_id: String,
pub percentage: f64,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum ShareholderStructure {
Equal,
Custom,
}
impl ShareholderStructure {
pub fn to_string(&self) -> String {
match self {
ShareholderStructure::Equal => "equal".to_string(),
ShareholderStructure::Custom => "custom".to_string(),
}
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum PaymentPlan {
Monthly,
Yearly,
TwoYear,
}
impl PaymentPlan {
pub fn to_string(&self) -> String {
match self {
PaymentPlan::Monthly => "monthly".to_string(),
PaymentPlan::Yearly => "yearly".to_string(),
PaymentPlan::TwoYear => "two_year".to_string(),
}
}
pub fn from_string(s: &str) -> Option<Self> {
match s {
"monthly" => Some(PaymentPlan::Monthly),
"yearly" => Some(PaymentPlan::Yearly),
"two_year" => Some(PaymentPlan::TwoYear),
_ => None,
}
}
pub fn get_display_name(&self) -> String {
match self {
PaymentPlan::Monthly => "Monthly".to_string(),
PaymentPlan::Yearly => "Yearly".to_string(),
PaymentPlan::TwoYear => "2 Years".to_string(),
}
}
pub fn get_discount(&self) -> f64 {
match self {
PaymentPlan::Monthly => 1.0,
PaymentPlan::Yearly => 0.8, // 20% discount
PaymentPlan::TwoYear => 0.6, // 40% discount
}
}
pub fn get_badge_class(&self) -> Option<String> {
match self {
PaymentPlan::Monthly => None,
PaymentPlan::Yearly => Some("badge bg-success".to_string()),
PaymentPlan::TwoYear => Some("badge bg-warning".to_string()),
}
}
pub fn get_badge_text(&self) -> Option<String> {
match self {
PaymentPlan::Monthly => None,
PaymentPlan::Yearly => Some("20% OFF".to_string()),
PaymentPlan::TwoYear => Some("40% OFF".to_string()),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct LegalAgreements {
pub terms: bool,
pub privacy: bool,
pub compliance: bool,
pub articles: bool,
pub final_agreement: bool,
}
impl Default for LegalAgreements {
fn default() -> Self {
Self {
terms: false,
privacy: false,
compliance: false,
articles: false,
final_agreement: false,
}
}
}
impl LegalAgreements {
pub fn all_agreed(&self) -> bool {
self.terms && self.privacy && self.compliance && self.articles && self.final_agreement
}
pub fn missing_agreements(&self) -> Vec<String> {
let mut missing = Vec::new();
if !self.terms {
missing.push("Terms of Service".to_string());
}
if !self.privacy {
missing.push("Privacy Policy".to_string());
}
if !self.compliance {
missing.push("Compliance Agreement".to_string());
}
if !self.articles {
missing.push("Articles of Incorporation".to_string());
}
if !self.final_agreement {
missing.push("Final Agreement".to_string());
}
missing
}
}
// State management structures
#[derive(Clone, PartialEq)]
pub struct EntitiesState {
pub active_tab: ActiveTab,
pub companies: Vec<Company>,
pub registration_state: RegistrationState,
pub loading: bool,
pub error: Option<String>,
}
impl Default for EntitiesState {
fn default() -> Self {
Self {
active_tab: ActiveTab::Companies,
companies: Vec::new(),
registration_state: RegistrationState::default(),
loading: false,
error: None,
}
}
}
#[derive(Clone, PartialEq)]
pub struct RegistrationState {
pub current_step: u8,
pub form_data: CompanyFormData,
pub validation_errors: std::collections::HashMap<String, String>,
pub payment_intent: Option<String>, // Payment intent ID
pub auto_save_enabled: bool,
pub processing_payment: bool,
}
impl Default for RegistrationState {
fn default() -> Self {
Self {
current_step: 1,
form_data: CompanyFormData::default(),
validation_errors: std::collections::HashMap::new(),
payment_intent: None,
auto_save_enabled: true,
processing_payment: false,
}
}
}
#[derive(Clone, PartialEq)]
pub enum ActiveTab {
Companies,
RegisterCompany,
}
impl ActiveTab {
pub fn to_string(&self) -> String {
match self {
ActiveTab::Companies => "Companies".to_string(),
ActiveTab::RegisterCompany => "Register Company".to_string(),
}
}
}
// Payment-related structures
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct PaymentIntent {
pub id: String,
pub client_secret: String,
pub amount: f64,
pub currency: String,
pub status: String,
}
// Validation result
#[derive(Clone, PartialEq, Debug)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
pub fn invalid(errors: Vec<String>) -> Self {
Self {
is_valid: false,
errors,
}
}
}
// Digital Resident Registration Models
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DigitalResidentFormData {
// Step 1: Personal Information
pub full_name: String,
pub email: String,
pub phone: String,
pub date_of_birth: String,
pub nationality: String,
pub passport_number: String,
pub passport_expiry: String,
// Cryptographic Keys
pub public_key: Option<String>,
pub private_key: Option<String>,
pub private_key_shown: bool, // Track if private key has been shown
// Step 2: Address Information
pub current_address: String,
pub city: String,
pub country: String,
pub postal_code: String,
pub permanent_address: Option<String>,
// Step 3: Professional Information
pub occupation: String,
pub employer: Option<String>,
pub annual_income: Option<String>,
pub education_level: String,
pub skills: Vec<String>,
// Step 4: Digital Services
pub requested_services: Vec<DigitalService>,
pub preferred_language: String,
pub communication_preferences: CommunicationPreferences,
// Step 5: Payment & Agreements
pub payment_plan: ResidentPaymentPlan,
pub legal_agreements: LegalAgreements,
}
impl Default for DigitalResidentFormData {
fn default() -> Self {
Self {
full_name: String::new(),
email: String::new(),
phone: String::new(),
date_of_birth: String::new(),
nationality: String::new(),
passport_number: String::new(),
passport_expiry: String::new(),
public_key: None,
private_key: None,
private_key_shown: false,
current_address: String::new(),
city: String::new(),
country: String::new(),
postal_code: String::new(),
permanent_address: None,
occupation: String::new(),
employer: None,
annual_income: None,
education_level: String::new(),
skills: Vec::new(),
requested_services: Vec::new(),
preferred_language: "English".to_string(),
communication_preferences: CommunicationPreferences::default(),
payment_plan: ResidentPaymentPlan::Monthly,
legal_agreements: LegalAgreements::default(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum DigitalService {
BankingAccess,
TaxFiling,
HealthcareAccess,
EducationServices,
BusinessLicensing,
PropertyServices,
LegalServices,
DigitalIdentity,
}
impl DigitalService {
pub fn get_display_name(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "Banking Access",
DigitalService::TaxFiling => "Tax Filing Services",
DigitalService::HealthcareAccess => "Healthcare Access",
DigitalService::EducationServices => "Education Services",
DigitalService::BusinessLicensing => "Business Licensing",
DigitalService::PropertyServices => "Property Services",
DigitalService::LegalServices => "Legal Services",
DigitalService::DigitalIdentity => "Digital Identity",
}
}
pub fn get_description(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "Access to digital banking services and financial institutions",
DigitalService::TaxFiling => "Automated tax filing and compliance services",
DigitalService::HealthcareAccess => "Access to healthcare providers and medical services",
DigitalService::EducationServices => "Educational resources and certification programs",
DigitalService::BusinessLicensing => "Business registration and licensing services",
DigitalService::PropertyServices => "Property rental and purchase assistance",
DigitalService::LegalServices => "Legal consultation and document services",
DigitalService::DigitalIdentity => "Secure digital identity verification",
}
}
pub fn get_icon(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "bi-bank",
DigitalService::TaxFiling => "bi-calculator",
DigitalService::HealthcareAccess => "bi-heart-pulse",
DigitalService::EducationServices => "bi-mortarboard",
DigitalService::BusinessLicensing => "bi-briefcase",
DigitalService::PropertyServices => "bi-house",
DigitalService::LegalServices => "bi-scales",
DigitalService::DigitalIdentity => "bi-person-badge",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CommunicationPreferences {
pub email_notifications: bool,
pub sms_notifications: bool,
pub push_notifications: bool,
pub newsletter: bool,
}
impl Default for CommunicationPreferences {
fn default() -> Self {
Self {
email_notifications: true,
sms_notifications: false,
push_notifications: true,
newsletter: false,
}
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum ResidentPaymentPlan {
Monthly,
Yearly,
Lifetime,
}
impl ResidentPaymentPlan {
pub fn get_display_name(&self) -> &'static str {
match self {
ResidentPaymentPlan::Monthly => "Monthly",
ResidentPaymentPlan::Yearly => "Yearly",
ResidentPaymentPlan::Lifetime => "Lifetime",
}
}
pub fn get_price(&self) -> f64 {
match self {
ResidentPaymentPlan::Monthly => 29.99,
ResidentPaymentPlan::Yearly => 299.99, // ~17% discount
ResidentPaymentPlan::Lifetime => 999.99,
}
}
pub fn get_discount(&self) -> f64 {
match self {
ResidentPaymentPlan::Monthly => 1.0,
ResidentPaymentPlan::Yearly => 0.83, // 17% discount
ResidentPaymentPlan::Lifetime => 0.0, // Special pricing
}
}
pub fn get_description(&self) -> &'static str {
match self {
ResidentPaymentPlan::Monthly => "Pay monthly with full flexibility",
ResidentPaymentPlan::Yearly => "Save 17% with annual payment",
ResidentPaymentPlan::Lifetime => "One-time payment for lifetime access",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DigitalResident {
pub id: u32,
pub full_name: String,
pub email: String,
pub phone: String,
pub date_of_birth: String,
pub nationality: String,
pub passport_number: String,
pub passport_expiry: String,
pub current_address: String,
pub city: String,
pub country: String,
pub postal_code: String,
pub occupation: String,
pub employer: Option<String>,
pub annual_income: Option<String>,
pub education_level: String,
pub selected_services: Vec<DigitalService>,
pub payment_plan: ResidentPaymentPlan,
pub registration_date: String,
pub status: ResidentStatus,
// KYC fields
pub kyc_documents_uploaded: bool,
pub kyc_status: KycStatus,
// Cryptographic Keys
pub public_key: Option<String>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum ResidentStatus {
Pending,
Active,
Suspended,
Expired,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum KycStatus {
NotStarted,
DocumentsUploaded,
UnderReview,
Approved,
Rejected,
RequiresAdditionalInfo,
}
impl KycStatus {
pub fn to_string(&self) -> String {
match self {
KycStatus::NotStarted => "Not Started".to_string(),
KycStatus::DocumentsUploaded => "Documents Uploaded".to_string(),
KycStatus::UnderReview => "Under Review".to_string(),
KycStatus::Approved => "Approved".to_string(),
KycStatus::Rejected => "Rejected".to_string(),
KycStatus::RequiresAdditionalInfo => "Requires Additional Info".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
KycStatus::NotStarted => "badge bg-secondary".to_string(),
KycStatus::DocumentsUploaded => "badge bg-info".to_string(),
KycStatus::UnderReview => "badge bg-warning text-dark".to_string(),
KycStatus::Approved => "badge bg-success".to_string(),
KycStatus::Rejected => "badge bg-danger".to_string(),
KycStatus::RequiresAdditionalInfo => "badge bg-warning text-dark".to_string(),
}
}
}
impl ResidentStatus {
pub fn to_string(&self) -> String {
match self {
ResidentStatus::Pending => "Pending".to_string(),
ResidentStatus::Active => "Active".to_string(),
ResidentStatus::Suspended => "Suspended".to_string(),
ResidentStatus::Expired => "Expired".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
ResidentStatus::Pending => "badge bg-warning text-dark".to_string(),
ResidentStatus::Active => "badge bg-success".to_string(),
ResidentStatus::Suspended => "badge bg-danger".to_string(),
ResidentStatus::Expired => "badge bg-secondary".to_string(),
}
}
}

3
portal/src/models/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod company;
pub use company::*;

View File

@@ -0,0 +1,3 @@
pub mod resident_service;
pub use resident_service::*;

View File

@@ -0,0 +1,257 @@
use crate::models::company::{DigitalResident, DigitalResidentFormData, KycStatus};
use gloo::storage::{LocalStorage, Storage};
const RESIDENTS_STORAGE_KEY: &str = "freezone_residents";
const RESIDENT_REGISTRATIONS_STORAGE_KEY: &str = "freezone_resident_registrations";
const RESIDENT_FORM_KEY: &str = "freezone_resident_registration_form";
const FORM_EXPIRY_HOURS: i64 = 24;
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ResidentRegistration {
pub id: u32,
pub full_name: String,
pub email: String,
pub status: ResidentRegistrationStatus,
pub created_at: String,
pub form_data: DigitalResidentFormData,
pub current_step: u8,
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ResidentRegistrationStatus {
Draft,
PendingPayment,
PaymentFailed,
PendingApproval,
Approved,
Rejected,
}
impl ResidentRegistrationStatus {
pub fn to_string(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "Draft",
ResidentRegistrationStatus::PendingPayment => "Pending Payment",
ResidentRegistrationStatus::PaymentFailed => "Payment Failed",
ResidentRegistrationStatus::PendingApproval => "Pending Approval",
ResidentRegistrationStatus::Approved => "Approved",
ResidentRegistrationStatus::Rejected => "Rejected",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "bg-secondary",
ResidentRegistrationStatus::PendingPayment => "bg-warning",
ResidentRegistrationStatus::PaymentFailed => "bg-danger",
ResidentRegistrationStatus::PendingApproval => "bg-info",
ResidentRegistrationStatus::Approved => "bg-success",
ResidentRegistrationStatus::Rejected => "bg-danger",
}
}
}
pub struct ResidentService;
impl ResidentService {
/// Get all residents from local storage
pub fn get_residents() -> Vec<DigitalResident> {
match LocalStorage::get::<Vec<DigitalResident>>(RESIDENTS_STORAGE_KEY) {
Ok(residents) => residents,
Err(_) => {
// Initialize with empty list if not found
let residents = Vec::new();
let _ = LocalStorage::set(RESIDENTS_STORAGE_KEY, &residents);
residents
}
}
}
/// Save residents to local storage
pub fn save_residents(residents: &[DigitalResident]) -> Result<(), String> {
LocalStorage::set(RESIDENTS_STORAGE_KEY, residents)
.map_err(|e| format!("Failed to save residents: {:?}", e))
}
/// Add a new resident
pub fn add_resident(mut resident: DigitalResident) -> Result<DigitalResident, String> {
let mut residents = Self::get_residents();
// Generate new ID
let max_id = residents.iter().map(|r| r.id).max().unwrap_or(0);
resident.id = max_id + 1;
residents.push(resident.clone());
Self::save_residents(&residents)?;
Ok(resident)
}
/// Get all resident registrations from local storage
pub fn get_resident_registrations() -> Vec<ResidentRegistration> {
match LocalStorage::get::<Vec<ResidentRegistration>>(RESIDENT_REGISTRATIONS_STORAGE_KEY) {
Ok(registrations) => registrations,
Err(_) => {
// Initialize with empty list if not found
let registrations = Vec::new();
let _ = LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, &registrations);
registrations
}
}
}
/// Save resident registrations to local storage
pub fn save_resident_registrations(registrations: &[ResidentRegistration]) -> Result<(), String> {
LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, registrations)
.map_err(|e| format!("Failed to save resident registrations: {:?}", e))
}
/// Add or update a resident registration
pub fn save_resident_registration(mut registration: ResidentRegistration) -> Result<ResidentRegistration, String> {
let mut registrations = Self::get_resident_registrations();
if registration.id == 0 {
// Generate new ID for new registration
let max_id = registrations.iter().map(|r| r.id).max().unwrap_or(0);
registration.id = max_id + 1;
registrations.push(registration.clone());
} else {
// Update existing registration
if let Some(existing) = registrations.iter_mut().find(|r| r.id == registration.id) {
*existing = registration.clone();
} else {
return Err("Registration not found".to_string());
}
}
Self::save_resident_registrations(&registrations)?;
Ok(registration)
}
/// Save registration form data with expiration
pub fn save_resident_registration_form(form_data: &DigitalResidentFormData, current_step: u8) -> Result<(), String> {
let now = js_sys::Date::now() as i64;
let expires_at = now + (FORM_EXPIRY_HOURS * 60 * 60 * 1000);
let saved_form = SavedResidentRegistrationForm {
form_data: form_data.clone(),
current_step,
saved_at: now,
expires_at,
};
LocalStorage::set(RESIDENT_FORM_KEY, &saved_form)
.map_err(|e| format!("Failed to save form: {:?}", e))
}
/// Load registration form data if not expired
pub fn load_resident_registration_form() -> Option<(DigitalResidentFormData, u8)> {
match LocalStorage::get::<SavedResidentRegistrationForm>(RESIDENT_FORM_KEY) {
Ok(saved_form) => {
let now = js_sys::Date::now() as i64;
if now < saved_form.expires_at {
Some((saved_form.form_data, saved_form.current_step))
} else {
// Form expired, remove it
let _ = LocalStorage::delete(RESIDENT_FORM_KEY);
None
}
}
Err(_) => None,
}
}
/// Clear saved registration form
pub fn clear_resident_registration_form() -> Result<(), String> {
LocalStorage::delete(RESIDENT_FORM_KEY);
Ok(())
}
/// Create a resident from form data
pub fn create_resident_from_form(form_data: &DigitalResidentFormData) -> Result<DigitalResident, String> {
let now = js_sys::Date::new_0();
let registration_date = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let resident = DigitalResident {
id: 0, // Will be set by add_resident
full_name: form_data.full_name.clone(),
email: form_data.email.clone(),
phone: form_data.phone.clone(),
date_of_birth: form_data.date_of_birth.clone(),
nationality: form_data.nationality.clone(),
passport_number: form_data.passport_number.clone(),
passport_expiry: form_data.passport_expiry.clone(),
current_address: form_data.current_address.clone(),
city: form_data.city.clone(),
country: form_data.country.clone(),
postal_code: form_data.postal_code.clone(),
occupation: form_data.occupation.clone(),
employer: form_data.employer.clone(),
annual_income: form_data.annual_income.clone(),
education_level: form_data.education_level.clone(),
selected_services: form_data.requested_services.clone(),
payment_plan: form_data.payment_plan.clone(),
registration_date,
status: crate::models::company::ResidentStatus::Pending,
kyc_documents_uploaded: false, // Will be updated when documents are uploaded
kyc_status: KycStatus::NotStarted,
public_key: form_data.public_key.clone(),
};
Self::add_resident(resident)
}
/// Validate form data for a specific step (simplified 2-step form)
pub fn validate_resident_step(form_data: &DigitalResidentFormData, step: u8) -> crate::models::ValidationResult {
let mut errors = Vec::new();
match step {
1 => {
// Step 1: Personal Information & KYC (simplified - only name, email, and terms required)
if form_data.full_name.trim().is_empty() {
errors.push("Full name is required".to_string());
}
if form_data.email.trim().is_empty() {
errors.push("Email is required".to_string());
} else if !Self::is_valid_email(&form_data.email) {
errors.push("Please enter a valid email address".to_string());
}
if !form_data.legal_agreements.terms {
errors.push("You must agree to the Terms of Service and Privacy Policy".to_string());
}
// Note: KYC verification is handled separately via button click
}
2 => {
// Step 2: Payment only (no additional agreements needed)
// Payment validation will be handled by Stripe
}
_ => {
errors.push("Invalid step".to_string());
}
}
if errors.is_empty() {
crate::models::ValidationResult { is_valid: true, errors: Vec::new() }
} else {
crate::models::ValidationResult { is_valid: false, errors }
}
}
/// Simple email validation
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.') && email.len() > 5
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SavedResidentRegistrationForm {
form_data: DigitalResidentFormData,
current_step: u8,
saved_at: i64,
expires_at: i64,
}

599
portal/static/css/main.css Normal file
View File

@@ -0,0 +1,599 @@
/* Zanzibar Digital Freezone - Main CSS */
/* Based on the original Actix MVC app styling */
/* Custom CSS Variables for Very Soft Pastel Colors */
:root {
/* Very Muted Pastel Colors */
--bs-primary: #d4e6f1;
--bs-primary-rgb: 212, 230, 241;
--bs-secondary: #e8eaed;
--bs-secondary-rgb: 232, 234, 237;
--bs-success: #d5f4e6;
--bs-success-rgb: 213, 244, 230;
--bs-info: #d6f0f7;
--bs-info-rgb: 214, 240, 247;
--bs-warning: #fef9e7;
--bs-warning-rgb: 254, 249, 231;
--bs-danger: #fdeaea;
--bs-danger-rgb: 253, 234, 234;
/* Light theme colors */
--bs-light: #f8f9fa;
--bs-dark: #343a40;
/* Text colors - always black or white */
--text-primary: #212529;
--text-secondary: #495057;
--text-muted: #6c757d;
}
/* Dark theme variables */
[data-bs-theme="dark"] {
/* Very Muted Dark Pastels */
--bs-primary: #2c3e50;
--bs-primary-rgb: 44, 62, 80;
--bs-secondary: #34495e;
--bs-secondary-rgb: 52, 73, 94;
--bs-success: #27ae60;
--bs-success-rgb: 39, 174, 96;
--bs-info: #3498db;
--bs-info-rgb: 52, 152, 219;
--bs-warning: #f39c12;
--bs-warning-rgb: 243, 156, 18;
--bs-danger: #e74c3c;
--bs-danger-rgb: 231, 76, 60;
--text-primary: #ffffff;
--text-secondary: #adb5bd;
--text-muted: #6c757d;
}
/* Global Styles */
* {
box-sizing: border-box;
}
body {
padding-top: 50px; /* Height of the fixed header */
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bs-light);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Dark theme body */
[data-bs-theme="dark"] body {
background-color: #1a1d20;
color: var(--text-primary);
}
/* Header Styles */
.header {
height: 50px;
position: fixed;
top: 0;
width: 100%;
z-index: 1030;
background-color: #212529 !important;
color: white;
}
.header .container-fluid {
height: 100%;
}
.header h5 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.header .navbar-toggler {
border: none;
padding: 0.25rem 0.5rem;
background: none;
}
.header .navbar-toggler:focus {
box-shadow: none;
}
.header .nav-link {
color: white !important;
text-decoration: none;
padding: 0.5rem 1rem;
}
.header .nav-link:hover {
color: #adb5bd !important;
}
.header .nav-link.active {
color: white !important;
font-weight: 600;
}
.header .dropdown-menu {
border: 1px solid #dee2e6;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
/* Sidebar Styles */
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px); /* Subtract header and footer height */
top: 50px; /* Position below header */
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
z-index: 1010;
transition: background-color 0.3s ease;
}
/* Dark theme sidebar */
[data-bs-theme="dark"] .sidebar {
background-color: #1a1d20 !important;
border-right: 1px solid #495057;
}
.sidebar .nav-link {
color: #495057;
text-decoration: none;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
border-radius: 0;
transition: all 0.2s ease;
}
.sidebar .nav-link:hover {
background-color: #e9ecef;
color: #212529;
}
.sidebar .nav-link.active {
background-color: #e7f3ff;
color: #212529;
border-left: 4px solid #d4e6f1;
font-weight: 600;
}
/* Dark theme sidebar nav links */
[data-bs-theme="dark"] .sidebar .nav-link {
color: #ffffff !important;
}
[data-bs-theme="dark"] .sidebar .nav-link:hover {
background-color: #2d3339 !important;
color: #ffffff !important;
}
[data-bs-theme="dark"] .sidebar .nav-link.active {
background-color: #34495e !important;
color: #ffffff !important;
border-left: 4px solid #2c3e50 !important;
}
/* Dark theme sidebar cards */
[data-bs-theme="dark"] .sidebar .card {
background-color: #2d3339 !important;
border-color: #495057 !important;
color: #ffffff !important;
}
[data-bs-theme="dark"] .sidebar .card.bg-white {
background-color: #2d3339 !important;
color: #ffffff !important;
}
[data-bs-theme="dark"] .sidebar .card.bg-dark {
background-color: #34495e !important;
color: #ffffff !important;
}
/* Dark theme sidebar card icons */
[data-bs-theme="dark"] .sidebar .bg-dark {
background-color: #ffffff !important;
color: #212529 !important;
}
[data-bs-theme="dark"] .sidebar .bg-white {
background-color: #2d3339 !important;
color: #ffffff !important;
}
/* Dark theme dividers */
[data-bs-theme="dark"] .sidebar hr {
border-color: #495057 !important;
opacity: 0.5;
}
/* Dark theme text colors */
[data-bs-theme="dark"] .sidebar .text-muted {
color: #adb5bd !important;
}
[data-bs-theme="dark"] .sidebar h6 {
color: #ffffff !important;
}
[data-bs-theme="dark"] .sidebar small {
color: #adb5bd !important;
}
.sidebar .nav-link i {
margin-right: 0.5rem;
width: 1.2rem;
text-align: center;
}
/* Main Content Area */
.main-content {
margin-left: 240px;
min-height: calc(100vh - 90px);
padding: 1rem;
}
/* Footer Styles */
.footer {
height: 40px;
line-height: 40px;
background-color: #212529 !important;
color: white;
position: relative;
margin-top: auto;
}
.footer a {
color: white;
text-decoration: none;
}
.footer a:hover {
color: #adb5bd;
}
/* Feature Cards (Home Page) */
.compact-card {
max-height: 150px;
overflow-y: auto;
}
.compact-card .card-body {
padding: 0.75rem;
}
.compact-card .card-text {
font-size: 0.875rem;
line-height: 1.4;
margin-bottom: 0;
}
.card-header {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.card-header h6 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1055;
}
.toast {
min-width: 300px;
margin-bottom: 0.5rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.toast-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
padding: 0.5rem 0.75rem;
}
.toast-success .toast-header {
background-color: var(--bs-success);
color: var(--text-primary);
}
.toast-error .toast-header {
background-color: var(--bs-danger);
color: var(--text-primary);
}
.toast-warning .toast-header {
background-color: var(--bs-warning);
color: var(--text-primary);
}
.toast-info .toast-header {
background-color: var(--bs-info);
color: var(--text-primary);
}
.toast-body {
padding: 0.75rem;
background-color: white;
}
/* Login Form Styles */
.login-container {
min-height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);
}
.login-card {
width: 100%;
max-width: 400px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.login-card .card-header {
background-color: var(--bs-primary);
color: var(--text-primary);
border-bottom: none;
}
.login-card .form-control:focus {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
}
.login-card .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: var(--text-primary);
}
.login-card .btn-primary:hover {
background-color: rgba(var(--bs-primary-rgb), 0.8);
border-color: rgba(var(--bs-primary-rgb), 0.8);
color: var(--text-primary);
}
/* Responsive Design */
@media (min-width: 768px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px);
top: 50px;
}
.main-content {
margin-left: 240px;
min-height: calc(100vh - 90px);
}
}
@media (max-width: 767.98px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px);
top: 50px;
left: -240px;
transition: left 0.3s ease;
z-index: 1020;
box-shadow: 0.5rem 0 1rem rgba(0, 0, 0, 0.15);
}
.sidebar.show {
left: 0;
}
.main-content {
margin-left: 0;
}
.header .d-md-flex {
display: none !important;
}
}
/* Utility Classes */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shadow-sm {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
}
.border-start {
border-left: 1px solid #dee2e6 !important;
}
.border-4 {
border-width: 4px !important;
}
/* Loading States */
.loading {
opacity: 0.6;
pointer-events: none;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
/* Focus States for Accessibility */
.nav-link:focus,
.btn:focus,
.form-control:focus {
outline: 2px solid var(--bs-primary);
outline-offset: 2px;
}
/* Button and component overrides for very muted pastel colors */
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: #212529;
}
.btn-primary:hover {
background-color: #c3d9ed;
border-color: #c3d9ed;
color: #212529;
}
.btn-outline-primary {
color: #212529;
border-color: var(--bs-primary);
}
.btn-outline-primary:hover {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: #212529;
}
.btn-success {
background-color: var(--bs-success);
border-color: var(--bs-success);
color: #212529;
}
.btn-success:hover {
background-color: #c8f0dd;
border-color: #c8f0dd;
color: #212529;
}
/* Dark theme button overrides */
[data-bs-theme="dark"] .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: #ffffff;
}
[data-bs-theme="dark"] .btn-primary:hover {
background-color: #34495e;
border-color: #34495e;
color: #ffffff;
}
[data-bs-theme="dark"] .btn-outline-primary {
color: #ffffff;
border-color: var(--bs-primary);
}
[data-bs-theme="dark"] .btn-outline-primary:hover {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: #ffffff;
}
[data-bs-theme="dark"] .btn-success {
background-color: var(--bs-success);
border-color: var(--bs-success);
color: #ffffff;
}
[data-bs-theme="dark"] .btn-success:hover {
background-color: #2ecc71;
border-color: #2ecc71;
color: #ffffff;
}
/* Card styling improvements */
.card {
border: 1px solid #e9ecef;
background-color: #ffffff;
color: #212529;
}
[data-bs-theme="dark"] .card {
background-color: #2d3339 !important;
border-color: #495057 !important;
color: #ffffff !important;
}
/* Text color overrides - always black or white */
.text-primary {
color: #212529 !important;
}
.text-secondary {
color: #495057 !important;
}
.text-muted {
color: #6c757d !important;
}
/* Dark theme text overrides */
[data-bs-theme="dark"] .text-primary {
color: #ffffff !important;
}
[data-bs-theme="dark"] .text-secondary {
color: #adb5bd !important;
}
[data-bs-theme="dark"] .text-muted {
color: #6c757d !important;
}
/* Border color overrides */
.border-primary {
border-color: var(--bs-primary) !important;
}
.border-success {
border-color: var(--bs-success) !important;
}
/* Background color overrides */
.bg-primary {
background-color: var(--bs-primary) !important;
color: #212529 !important;
}
.bg-success {
background-color: var(--bs-success) !important;
color: #212529 !important;
}
/* Dark theme background overrides */
[data-bs-theme="dark"] .bg-primary {
background-color: var(--bs-primary) !important;
color: #ffffff !important;
}
[data-bs-theme="dark"] .bg-success {
background-color: var(--bs-success) !important;
color: #ffffff !important;
}
/* Print Styles */
@media print {
.header,
.sidebar,
.footer {
display: none !important;
}
.main-content {
margin-left: 0 !important;
padding: 0 !important;
}
}

View File

@@ -0,0 +1,399 @@
// Stripe Integration for Company Registration
// This file handles the Stripe Elements integration for the Yew WASM application
let stripe;
let elements;
let paymentElement;
// Stripe publishable key - this should be set from the server or environment
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51234567890abcdef'; // Replace with actual key
// Initialize Stripe when the script loads
document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 Stripe integration script loaded');
// Initialize Stripe
if (window.Stripe) {
stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
console.log('✅ Stripe initialized');
} else {
console.error('❌ Stripe.js not loaded');
}
});
// Initialize Stripe Elements with client secret
window.initializeStripeElements = async function(clientSecret) {
console.log('🔧 Initializing Stripe Elements with client secret:', clientSecret);
try {
if (!stripe) {
throw new Error('Stripe not initialized');
}
// Create Elements instance with client secret
elements = stripe.elements({
clientSecret: clientSecret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#198754',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: 'system-ui, sans-serif',
spacingUnit: '4px',
borderRadius: '6px',
}
}
});
// Clear the payment element container first
const paymentElementDiv = document.getElementById('payment-element');
if (!paymentElementDiv) {
throw new Error('Payment element container not found');
}
paymentElementDiv.innerHTML = '';
// Create and mount the Payment Element
paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle real-time validation errors from the Payment Element
paymentElement.on('change', (event) => {
const displayError = document.getElementById('payment-errors');
if (event.error) {
displayError.textContent = event.error.message;
displayError.style.display = 'block';
displayError.classList.remove('alert-success');
displayError.classList.add('alert-danger');
} else {
displayError.style.display = 'none';
}
});
// Handle when the Payment Element is ready
paymentElement.on('ready', () => {
console.log('✅ Stripe Elements ready for payment');
// Add a subtle success indicator
const paymentCard = paymentElementDiv.closest('.card');
if (paymentCard) {
paymentCard.style.borderColor = '#198754';
paymentCard.style.borderWidth = '2px';
}
// Update button text to show payment is ready
const submitButton = document.getElementById('submit-payment');
const submitText = document.getElementById('submit-text');
if (submitButton && submitText) {
submitButton.disabled = false;
submitText.textContent = 'Complete Payment';
submitButton.classList.remove('btn-secondary');
submitButton.classList.add('btn-success');
}
});
// Handle loading state
paymentElement.on('loaderstart', () => {
console.log('🔄 Stripe Elements loading...');
});
paymentElement.on('loaderror', (event) => {
console.error('❌ Stripe Elements load error:', event.error);
showAdBlockerGuidance(event.error.message || 'Failed to load payment form');
});
console.log('✅ Stripe Elements initialized successfully');
return true;
} catch (error) {
console.error('❌ Error initializing Stripe Elements:', error);
// Check if this might be an ad blocker issue
const isAdBlockerError = error.message && (
error.message.includes('blocked') ||
error.message.includes('Failed to fetch') ||
error.message.includes('ERR_BLOCKED_BY_CLIENT') ||
error.message.includes('network') ||
error.message.includes('CORS')
);
if (isAdBlockerError) {
showAdBlockerGuidance(error.message || 'Failed to load payment form');
} else {
// Show generic error for non-ad-blocker issues
const errorElement = document.getElementById('payment-errors');
if (errorElement) {
errorElement.innerHTML = `
<div class="alert alert-danger alert-dismissible" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Payment Form Error:</strong> ${error.message || 'Failed to load payment form'}<br><br>
Please refresh the page and try again. If the problem persists, contact support.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
errorElement.style.display = 'block';
}
}
throw error;
}
};
// Create payment intent on server
window.createPaymentIntent = async function(formDataJson) {
console.log('💳 Creating payment intent...');
try {
// Parse the JSON string from Rust
let formData;
if (typeof formDataJson === 'string') {
formData = JSON.parse(formDataJson);
} else {
formData = formDataJson;
}
console.log('Form data:', formData);
const response = await fetch('/company/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorText = await response.text();
console.error('Payment intent creation failed:', errorText);
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
errorData = { error: errorText };
}
throw new Error(errorData.error || 'Failed to create payment intent');
}
const responseData = await response.json();
console.log('✅ Payment intent created:', responseData);
const { client_secret } = responseData;
if (!client_secret) {
throw new Error('No client secret received from server');
}
return client_secret;
} catch (error) {
console.error('❌ Payment intent creation error:', error);
throw error;
}
};
// Confirm payment with Stripe
window.confirmStripePayment = async function(clientSecret) {
console.log('🔄 Confirming payment...');
try {
// Ensure elements are ready before submitting
if (!elements) {
throw new Error('Payment form not ready. Please wait a moment and try again.');
}
console.log('🔄 Step 1: Submitting payment elements...');
// Step 1: Submit the payment elements first (required by new Stripe API)
const { error: submitError } = await elements.submit();
if (submitError) {
console.error('Elements submit failed:', submitError);
// Provide more specific error messages
if (submitError.type === 'validation_error') {
throw new Error('Please check your payment details and try again.');
} else if (submitError.type === 'card_error') {
throw new Error(submitError.message || 'Card error. Please check your card details.');
} else {
throw new Error(submitError.message || 'Payment form validation failed.');
}
}
console.log('✅ Step 1 complete: Elements submitted successfully');
console.log('🔄 Step 2: Confirming payment...');
// Step 2: Confirm payment with Stripe
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
clientSecret: clientSecret,
confirmParams: {
return_url: `${window.location.origin}/company/payment-success`,
},
redirect: 'if_required' // Handle success without redirect if possible
});
if (error) {
// Payment failed - redirect to failure page
console.error('Payment confirmation failed:', error);
window.location.href = `${window.location.origin}/company/payment-failure`;
return false;
}
if (paymentIntent && paymentIntent.status === 'succeeded') {
// Payment succeeded
console.log('✅ Payment completed successfully:', paymentIntent.id);
// Clear saved form data since registration is complete
localStorage.removeItem('freezone_company_registration');
// Redirect to success page with payment details
window.location.href = `${window.location.origin}/company/payment-success?payment_intent=${paymentIntent.id}&payment_intent_client_secret=${clientSecret}`;
return true;
} else if (paymentIntent && paymentIntent.status === 'requires_action') {
// Payment requires additional authentication (3D Secure, etc.)
console.log('🔐 Payment requires additional authentication');
// Stripe will handle the authentication flow automatically
return false; // Don't redirect yet
} else {
// Unexpected status - redirect to failure page
console.error('Unexpected payment status:', paymentIntent?.status);
window.location.href = `${window.location.origin}/company/payment-failure`;
return false;
}
} catch (error) {
console.error('❌ Payment confirmation error:', error);
throw error;
}
};
// Show comprehensive ad blocker guidance
function showAdBlockerGuidance(errorMessage) {
const errorElement = document.getElementById('payment-errors');
if (!errorElement) return;
// Detect browser type for specific instructions
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
const isFirefox = /Firefox/.test(navigator.userAgent);
const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
const isEdge = /Edg/.test(navigator.userAgent);
let browserSpecificInstructions = '';
if (isChrome) {
browserSpecificInstructions = `
<strong>Chrome Instructions:</strong><br>
1. Click the shield icon 🛡️ in the address bar<br>
2. Select "Allow" for this site<br>
3. Or go to Settings → Privacy → Ad blockers<br>
`;
} else if (isFirefox) {
browserSpecificInstructions = `
<strong>Firefox Instructions:</strong><br>
1. Click the shield icon 🛡️ in the address bar<br>
2. Turn off "Enhanced Tracking Protection" for this site<br>
3. Or disable uBlock Origin/AdBlock Plus temporarily<br>
`;
} else if (isSafari) {
browserSpecificInstructions = `
<strong>Safari Instructions:</strong><br>
1. Go to Safari → Preferences → Extensions<br>
2. Temporarily disable ad blocking extensions<br>
3. Or add this site to your allowlist<br>
`;
} else if (isEdge) {
browserSpecificInstructions = `
<strong>Edge Instructions:</strong><br>
1. Click the shield icon 🛡️ in the address bar<br>
2. Turn off tracking prevention for this site<br>
3. Or disable ad blocking extensions<br>
`;
}
errorElement.innerHTML = `
<div class="alert alert-warning alert-dismissible" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-shield-exclamation fs-1 text-warning me-3 mt-1"></i>
<div class="flex-grow-1">
<h5 class="alert-heading mb-3">🛡️ Ad Blocker Detected</h5>
<p class="mb-3"><strong>Error:</strong> ${errorMessage}</p>
<p class="mb-3">Your ad blocker or privacy extension is preventing the secure payment form from loading. This is normal security behavior, but we need to process your payment securely through Stripe.</p>
<div class="row">
<div class="col-md-6">
<h6>🔧 Quick Fix:</h6>
${browserSpecificInstructions}
</div>
<div class="col-md-6">
<h6>🔒 Why This Happens:</h6>
• Ad blockers block payment tracking<br>
• Privacy extensions block third-party scripts<br>
• This protects your privacy normally<br>
• Stripe needs access for secure payments<br>
</div>
</div>
<div class="mt-3 p-3 rounded">
<h6>✅ Alternative Solutions:</h6>
<div class="row">
<div class="col-md-4">
<strong>1. Incognito/Private Mode</strong><br>
<small class="text-muted">Usually has fewer extensions</small>
</div>
<div class="col-md-4">
<strong>2. Different Browser</strong><br>
<small class="text-muted">Try Chrome, Firefox, or Safari</small>
</div>
<div class="col-md-4">
<strong>3. Mobile Device</strong><br>
<small class="text-muted">Often has fewer blockers</small>
</div>
</div>
</div>
<div class="mt-3 text-center">
<button type="button" class="btn btn-primary me-2" onclick="location.reload()">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh & Try Again
</button>
<button type="button" class="btn btn-outline-secondary" onclick="showContactInfo()">
<i class="bi bi-headset me-1"></i>Contact Support
</button>
</div>
<div class="mt-2 text-center">
<small class="text-muted">
<i class="bi bi-shield-check me-1"></i>
We use Stripe for secure payment processing. Your payment information is encrypted and safe.
</small>
</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
errorElement.style.display = 'block';
// Scroll to error
errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Add visual indication
const paymentCard = document.querySelector('#payment-information-section');
if (paymentCard) {
paymentCard.style.borderColor = '#ffc107';
paymentCard.style.borderWidth = '2px';
paymentCard.classList.add('border-warning');
}
}
// Show contact information
function showContactInfo() {
alert('Contact Support:\n\nEmail: support@hostbasket.com\nPhone: +1 (555) 123-4567\nLive Chat: Available 24/7\n\nPlease mention "Payment Form Loading Issue" when contacting us.');
}
// Export functions for use by Rust/WASM
window.stripeIntegration = {
initializeElements: window.initializeStripeElements,
createPaymentIntent: window.createPaymentIntent,
confirmPayment: window.confirmStripePayment
};
console.log('✅ Stripe integration script ready');