more refactor wip

This commit is contained in:
Timur Gordon 2025-06-28 17:31:54 +02:00
parent 6f8fb27221
commit ddbc9d3a75
4 changed files with 484 additions and 66 deletions

View File

@ -3,9 +3,11 @@ pub mod simple_resident_wizard;
pub mod simple_step_info;
pub mod residence_card;
pub mod refactored_resident_wizard;
pub mod multi_step_resident_wizard;
pub use step_payment_stripe::*;
pub use simple_resident_wizard::*;
pub use simple_step_info::*;
pub use residence_card::*;
pub use refactored_resident_wizard::*;
pub use refactored_resident_wizard::*;
pub use multi_step_resident_wizard::*;

View File

@ -0,0 +1,441 @@
//! Resident registration wizard using the generic MultiStepForm component
use yew::prelude::*;
use std::rc::Rc;
use std::collections::HashMap;
use wasm_bindgen_futures::spawn_local;
use wasm_bindgen::JsCast;
use web_sys::console;
use serde_json::json;
use js_sys;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::ResidentService;
use crate::components::common::forms::{MultiStepForm, FormStep, StepValidator, ValidationResult};
use crate::components::common::forms::multi_step_form::MultiStepFormMsg;
use super::{SimpleStepInfo, StepPaymentStripe};
/// Step 1: Personal Information and KYC
pub struct PersonalInfoStep;
impl FormStep<DigitalResidentFormData> for PersonalInfoStep {
fn render(&self, ctx: &Context<MultiStepForm<DigitalResidentFormData>>, data: &DigitalResidentFormData) -> Html {
let on_change = ctx.link().callback(|new_data| {
MultiStepFormMsg::UpdateFormData(new_data)
});
html! {
<SimpleStepInfo
form_data={data.clone()}
on_change={on_change}
/>
}
}
fn get_title(&self) -> &'static str {
"Personal Information & KYC"
}
fn get_description(&self) -> &'static str {
"Provide your basic information and complete identity verification"
}
fn get_icon(&self) -> &'static str {
"bi-person-vcard"
}
}
/// Step 2: Payment and Legal Agreements
pub struct PaymentStep {
client_secret: Option<String>,
processing_payment: bool,
}
impl PaymentStep {
pub fn new() -> Self {
Self {
client_secret: None,
processing_payment: false,
}
}
pub fn set_client_secret(&mut self, client_secret: Option<String>) {
self.client_secret = client_secret;
}
pub fn set_processing(&mut self, processing: bool) {
self.processing_payment = processing;
}
}
impl FormStep<DigitalResidentFormData> for PaymentStep {
fn render(&self, ctx: &Context<MultiStepForm<DigitalResidentFormData>>, data: &DigitalResidentFormData) -> Html {
let on_process_payment = ctx.link().callback(|_| {
// This would trigger payment processing
MultiStepFormMsg::NextStep
});
let on_payment_complete = ctx.link().callback(|resident: DigitalResident| {
// This would complete the form
MultiStepFormMsg::Complete
});
let on_payment_error = ctx.link().callback(|error: String| {
console::log_1(&format!("Payment error: {}", error).into());
// Could trigger validation error display
MultiStepFormMsg::HideValidationToast
});
let data_clone = data.clone();
let on_payment_plan_change = ctx.link().callback(move |plan: ResidentPaymentPlan| {
let mut updated_data = data_clone.clone();
updated_data.payment_plan = plan;
MultiStepFormMsg::UpdateFormData(updated_data)
});
let on_confirmation_change = ctx.link().callback(|_confirmed: bool| {
// Handle confirmation state change
MultiStepFormMsg::HideValidationToast
});
html! {
<StepPaymentStripe
form_data={data.clone()}
client_secret={self.client_secret.clone()}
processing_payment={self.processing_payment}
on_process_payment={on_process_payment}
on_payment_complete={on_payment_complete}
on_payment_error={on_payment_error}
on_payment_plan_change={on_payment_plan_change}
on_confirmation_change={on_confirmation_change}
/>
}
}
fn get_title(&self) -> &'static str {
"Payment & Legal Agreements"
}
fn get_description(&self) -> &'static str {
"Choose your payment plan and review legal agreements"
}
fn get_icon(&self) -> &'static str {
"bi-credit-card"
}
fn show_navigation(&self) -> bool {
false // Payment step handles its own navigation
}
}
/// Validator for personal information step
pub struct PersonalInfoValidator;
impl StepValidator<DigitalResidentFormData> for PersonalInfoValidator {
fn validate(&self, data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
if data.full_name.trim().is_empty() {
errors.push("Full name is required".to_string());
}
if data.email.trim().is_empty() {
errors.push("Email address is required".to_string());
} else if !data.email.contains('@') {
errors.push("Please enter a valid email address".to_string());
}
if data.public_key.is_none() {
errors.push("Please generate your digital identity keys".to_string());
}
if !data.legal_agreements.terms {
errors.push("You must agree to the Terms of Service and Privacy Policy".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
}
/// Validator for payment step
pub struct PaymentValidator;
impl StepValidator<DigitalResidentFormData> for PaymentValidator {
fn validate(&self, data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
// Basic payment validation - in real implementation this would check payment completion
if data.payment_plan == ResidentPaymentPlan::Monthly && data.full_name.is_empty() {
errors.push("Payment information is incomplete".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
}
/// Properties for the multi-step resident wizard
#[derive(Properties, PartialEq)]
pub struct MultiStepResidentWizardProps {
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,
}
/// Messages for the multi-step resident wizard
pub enum MultiStepResidentWizardMsg {
FormDataChanged(DigitalResidentFormData),
FormCompleted(DigitalResidentFormData),
FormCancelled,
CreatePaymentIntent,
PaymentIntentCreated(String),
PaymentIntentError(String),
RegistrationComplete(DigitalResident),
RegistrationError(String),
}
/// Multi-step resident wizard component
pub struct MultiStepResidentWizard {
form_data: DigitalResidentFormData,
steps: Vec<Rc<dyn FormStep<DigitalResidentFormData>>>,
validators: HashMap<usize, Rc<dyn StepValidator<DigitalResidentFormData>>>,
client_secret: Option<String>,
processing_registration: bool,
}
impl Component for MultiStepResidentWizard {
type Message = MultiStepResidentWizardMsg;
type Properties = MultiStepResidentWizardProps;
fn create(ctx: &Context<Self>) -> Self {
// Initialize form data based on props
let form_data = if ctx.props().success_resident_id.is_some() || ctx.props().show_failure {
// For demo purposes, start with default data
DigitalResidentFormData::default()
} else {
DigitalResidentFormData::default()
};
// Create steps
let steps: Vec<Rc<dyn FormStep<DigitalResidentFormData>>> = vec![
Rc::new(PersonalInfoStep),
Rc::new(PaymentStep::new()),
];
// Create validators
let mut validators: HashMap<usize, Rc<dyn StepValidator<DigitalResidentFormData>>> = HashMap::new();
validators.insert(0, Rc::new(PersonalInfoValidator));
validators.insert(1, Rc::new(PaymentValidator));
Self {
form_data,
steps,
validators,
client_secret: None,
processing_registration: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
MultiStepResidentWizardMsg::FormDataChanged(new_data) => {
self.form_data = new_data;
true
}
MultiStepResidentWizardMsg::FormCompleted(final_data) => {
console::log_1(&"🎉 Form completed, processing registration...".into());
self.processing_registration = true;
self.form_data = final_data;
// Start registration process
let link = ctx.link().clone();
let form_data = self.form_data.clone();
spawn_local(async move {
// Simulate registration processing with a simple timeout
let promise = js_sys::Promise::new(&mut |resolve, _| {
let window = web_sys::window().unwrap();
window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 2000).unwrap();
});
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
match ResidentService::create_resident_from_form(&form_data) {
Ok(resident) => {
link.send_message(MultiStepResidentWizardMsg::RegistrationComplete(resident));
}
Err(error) => {
link.send_message(MultiStepResidentWizardMsg::RegistrationError(error));
}
}
});
true
}
MultiStepResidentWizardMsg::FormCancelled => {
ctx.props().on_back_to_parent.emit(());
false
}
MultiStepResidentWizardMsg::CreatePaymentIntent => {
console::log_1(&"🔧 Creating payment intent...".into());
self.create_payment_intent(ctx);
false
}
MultiStepResidentWizardMsg::PaymentIntentCreated(client_secret) => {
console::log_1(&"✅ Payment intent created".into());
self.client_secret = Some(client_secret);
// Update the payment step with the client secret
if let Some(_payment_step) = self.steps.get_mut(1) {
// This is a bit tricky with Rc - in a real implementation,
// we might use a different pattern for mutable step state
}
true
}
MultiStepResidentWizardMsg::PaymentIntentError(error) => {
console::log_1(&format!("❌ Payment intent error: {}", error).into());
true
}
MultiStepResidentWizardMsg::RegistrationComplete(resident) => {
self.processing_registration = false;
console::log_1(&"✅ Registration completed successfully".into());
ctx.props().on_registration_complete.emit(resident);
true
}
MultiStepResidentWizardMsg::RegistrationError(error) => {
self.processing_registration = false;
console::log_1(&format!("❌ Registration error: {}", error).into());
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_form_change = link.callback(MultiStepResidentWizardMsg::FormDataChanged);
let on_complete = link.callback(MultiStepResidentWizardMsg::FormCompleted);
let on_cancel = link.callback(|_| MultiStepResidentWizardMsg::FormCancelled);
html! {
<div class="h-100 d-flex flex-column position-relative">
<MultiStepForm<DigitalResidentFormData>
form_data={self.form_data.clone()}
on_form_change={on_form_change}
on_complete={on_complete}
on_cancel={Some(on_cancel)}
steps={self.steps.clone()}
validators={self.validators.clone()}
show_progress={true}
allow_skip_validation={false}
validation_toast_duration={5000}
/>
// Loading overlay when processing registration
{if self.processing_registration {
html! {
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style="background: rgba(255, 255, 255, 0.9); z-index: 1050;">
<div class="text-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="mt-3 text-muted">{"Processing registration..."}</p>
</div>
</div>
}
} else {
html! {}
}}
</div>
}
}
}
impl MultiStepResidentWizard {
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(MultiStepResidentWizardMsg::PaymentIntentCreated(client_secret));
}
Err(e) => {
link.send_message(MultiStepResidentWizardMsg::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};
use wasm_bindgen::JsValue;
console::log_1(&"🔧 Setting up Stripe payment for resident registration".into());
// Prepare form data for payment intent creation
let payment_data = json!({
"resident_name": form_data.full_name,
"email": form_data.email,
"payment_plan": form_data.payment_plan.get_display_name(),
"amount": form_data.payment_plan.get_price(),
"type": "resident_registration"
});
// Create request to server endpoint
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
let headers = web_sys::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| format!("Failed to create request: {:?}", e))?;
// Make the request
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
.map_err(|e| format!("Network request failed: {:?}", e))?;
let resp: Response = resp_value.dyn_into().unwrap();
if !resp.ok() {
return Err(format!("Server error: HTTP {}", resp.status()));
}
// Parse response
let json_value = JsFuture::from(resp.json().unwrap()).await
.map_err(|e| format!("Failed to parse response: {:?}", e))?;
// Extract client secret from response
let response_obj = web_sys::js_sys::Object::from(json_value);
let client_secret_value = web_sys::js_sys::Reflect::get(&response_obj, &"client_secret".into())
.map_err(|e| format!("No client_secret in response: {:?}", e))?;
let client_secret = client_secret_value.as_string()
.ok_or_else(|| "Invalid client secret received from server".to_string())?;
console::log_1(&"✅ Payment intent created successfully".into());
Ok(client_secret)
}
}

View File

@ -6,6 +6,9 @@ use web_sys::{console, js_sys};
use serde_json::json;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::{ResidentService, ResidentRegistration, ResidentRegistrationStatus};
use crate::components::common::ui::progress_indicator::{ProgressIndicator, ProgressVariant, ProgressColor, ProgressSize};
use crate::components::common::ui::validation_toast::{ValidationToast, ToastType};
use crate::components::common::ui::loading_spinner::LoadingSpinner;
use super::{SimpleStepInfo, StepPaymentStripe};
#[wasm_bindgen]
@ -217,7 +220,7 @@ impl Component for SimpleResidentWizard {
let (step_title, step_description, step_icon) = self.get_step_info();
html! {
<div class="h-100 d-flex flex-column">
<div class="h-100 d-flex flex-column position-relative">
<form class="flex-grow-1 overflow-auto">
{self.render_current_step(ctx)}
</form>
@ -233,6 +236,21 @@ impl Component for SimpleResidentWizard {
} else {
html! {}
}}
// Loading overlay when processing registration
{if self.processing_registration {
html! {
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style="background: rgba(255, 255, 255, 0.9); z-index: 1050;">
<div class="text-center">
<LoadingSpinner />
<p class="mt-3 text-muted">{"Processing registration..."}</p>
</div>
</div>
}
} else {
html! {}
}}
</div>
}
}
@ -295,40 +313,16 @@ impl SimpleResidentWizard {
}}
</div>
// Step indicator (center)
// Step indicator (center) - Using our generic ProgressIndicator
<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>
}
})}
<ProgressIndicator
current_step={self.current_step as usize - 1} // Convert to 0-based index
total_steps={2}
variant={ProgressVariant::Dots}
color={ProgressColor::Primary}
size={ProgressSize::Small}
show_step_numbers={true}
/>
</div>
// Next/Register button (right)
@ -362,29 +356,13 @@ impl SimpleResidentWizard {
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>
<ValidationToast
toast_type={ToastType::Warning}
title={"Required Fields Missing"}
messages={self.validation_errors.clone()}
show={self.show_validation_toast}
on_close={close_toast}
/>
}
}

View File

@ -4,6 +4,7 @@ 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 crate::components::common::ui::loading_spinner::LoadingSpinner;
use super::ResidenceCard;
#[wasm_bindgen]
@ -175,19 +176,15 @@ impl Component for StepPaymentStripe {
{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>
<LoadingSpinner />
<p class="text-muted mt-3" 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>
<LoadingSpinner />
<p class="text-muted mt-3" style="font-size: 0.85rem;">{"Preparing payment form..."}</p>
</div>
}
} else {