diff --git a/portal/src/components/entities/resident_registration/mod.rs b/portal/src/components/entities/resident_registration/mod.rs index 376ac90..ed3550b 100644 --- a/portal/src/components/entities/resident_registration/mod.rs +++ b/portal/src/components/entities/resident_registration/mod.rs @@ -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::*; \ No newline at end of file +pub use refactored_resident_wizard::*; +pub use multi_step_resident_wizard::*; \ No newline at end of file diff --git a/portal/src/components/entities/resident_registration/multi_step_resident_wizard.rs b/portal/src/components/entities/resident_registration/multi_step_resident_wizard.rs new file mode 100644 index 0000000..a3b51f3 --- /dev/null +++ b/portal/src/components/entities/resident_registration/multi_step_resident_wizard.rs @@ -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 for PersonalInfoStep { + fn render(&self, ctx: &Context>, data: &DigitalResidentFormData) -> Html { + let on_change = ctx.link().callback(|new_data| { + MultiStepFormMsg::UpdateFormData(new_data) + }); + + html! { + + } + } + + 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, + 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) { + self.client_secret = client_secret; + } + + pub fn set_processing(&mut self, processing: bool) { + self.processing_payment = processing; + } +} + +impl FormStep for PaymentStep { + fn render(&self, ctx: &Context>, 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! { + + } + } + + 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 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 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, + pub on_back_to_parent: Callback<()>, + #[prop_or_default] + pub success_resident_id: Option, + #[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>>, + validators: HashMap>>, + client_secret: Option, + processing_registration: bool, +} + +impl Component for MultiStepResidentWizard { + type Message = MultiStepResidentWizardMsg; + type Properties = MultiStepResidentWizardProps; + + fn create(ctx: &Context) -> 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>> = vec![ + Rc::new(PersonalInfoStep), + Rc::new(PaymentStep::new()), + ]; + + // Create validators + let mut validators: HashMap>> = 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, 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) -> 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! { +
+ + 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! { +
+
+
+ {"Loading..."} +
+

{"Processing registration..."}

+
+
+ } + } else { + html! {} + }} +
+ } + } +} + +impl MultiStepResidentWizard { + fn create_payment_intent(&self, ctx: &Context) { + 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 { + 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) + } +} \ No newline at end of file diff --git a/portal/src/components/entities/resident_registration/simple_resident_wizard.rs b/portal/src/components/entities/resident_registration/simple_resident_wizard.rs index c46896c..f505465 100644 --- a/portal/src/components/entities/resident_registration/simple_resident_wizard.rs +++ b/portal/src/components/entities/resident_registration/simple_resident_wizard.rs @@ -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! { -
+
{self.render_current_step(ctx)}
@@ -233,6 +236,21 @@ impl Component for SimpleResidentWizard { } else { html! {} }} + + // Loading overlay when processing registration + {if self.processing_registration { + html! { +
+
+ +

{"Processing registration..."}

+
+
+ } + } else { + html! {} + }}
} } @@ -295,40 +313,16 @@ impl SimpleResidentWizard { }}
- // Step indicator (center) + // Step indicator (center) - Using our generic ProgressIndicator
- {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! { -
-
- {if is_completed { - html! { } - } else { - html! { {step} } - }} -
- {if step < 2 { - html! { -
- } - } else { - html! {} - }} -
- } - })} +
// Next/Register button (right) @@ -362,29 +356,13 @@ impl SimpleResidentWizard { let close_toast = link.callback(|_| SimpleResidentWizardMsg::HideValidationToast); html! { -
- -
+ } } diff --git a/portal/src/components/entities/resident_registration/step_payment_stripe.rs b/portal/src/components/entities/resident_registration/step_payment_stripe.rs index 171a1e9..a4da69d 100644 --- a/portal/src/components/entities/resident_registration/step_payment_stripe.rs +++ b/portal/src/components/entities/resident_registration/step_payment_stripe.rs @@ -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! {
-
- {"Loading..."} -
-

{"Processing payment..."}

+ +

{"Processing payment..."}

} } else if !has_client_secret { html! {
-
- {"Loading..."} -
-

{"Preparing payment form..."}

+ +

{"Preparing payment form..."}

} } else {