portal, platform, and server fixes

This commit is contained in:
Timur Gordon
2025-06-30 17:01:40 +02:00
parent 1c96fa4087
commit a5b46bffb1
59 changed files with 9158 additions and 1057 deletions

View File

@@ -0,0 +1,93 @@
# Portal Authentication Fix Summary
## Problem
The portal client was getting 401 errors when calling portal-server endpoints because the HTTP requests were missing the required `x-api-key` authentication header.
## Root Cause
The HTTP requests were being made from Rust code in [`multi_step_resident_wizard.rs`](src/components/entities/resident_registration/multi_step_resident_wizard.rs), not from JavaScript as initially assumed. The Rust code was missing the API key header and using an incorrect endpoint URL.
## Solution Implemented
### 1. Fixed Rust HTTP Request Code
**File**: [`src/components/entities/resident_registration/multi_step_resident_wizard.rs`](src/components/entities/resident_registration/multi_step_resident_wizard.rs)
**Changes**:
- Added `x-api-key` header to the HTTP request
- Fixed endpoint URL from `/resident/create-payment-intent` to `/api/resident/create-payment-intent`
- Integrated with new configuration system
### 2. Created Configuration Module
**File**: [`src/config.rs`](src/config.rs)
**Features**:
- Centralized API key management
- Configurable API base URL
- Development fallback with `dev_key_123` key
- Helper methods for endpoint URL construction
### 3. Updated Application Initialization
**File**: [`src/lib.rs`](src/lib.rs)
**Changes**:
- Added config module import
- Initialize configuration on app startup
- Added logging for configuration status
### 4. Cleaned Up JavaScript Code
**File**: [`index.html`](index.html)
**Changes**:
- Removed unused `createPaymentIntent` function (now handled in Rust)
- Removed unused API key configuration variables
- Kept only Stripe Elements initialization functions
### 5. Updated Documentation
**Files**:
- [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md) - Updated for Rust-based authentication
- [`test-env.sh`](test-env.sh) - Environment testing script (now less relevant)
## API Key Configuration
### Development
- **Client**: Hardcoded `dev_key_123` in [`src/config.rs`](src/config.rs)
- **Server**: Must include `dev_key_123` in `API_KEYS` environment variable
### Production
To change the API key for production:
1. Edit [`src/config.rs`](src/config.rs) and update the `get_api_key()` function
2. Rebuild the client: `trunk build --release`
3. Update server's `.env` file to include the new key in `API_KEYS`
## Testing
### Manual Test with curl
```bash
curl -X POST http://127.0.0.1:3001/api/resident/create-payment-intent \
-H "Content-Type: application/json" \
-H "x-api-key: dev_key_123" \
-d '{"type":"resident_registration","amount":5000}'
```
### Browser Console Logs
When the portal starts, you should see:
```
✅ Portal configuration initialized
🔧 Portal config loaded - API key: Present
🔑 Using API key: dev_key_123
```
When making payment requests:
```
🔧 Creating payment intent...
🔧 Setting up Stripe payment for resident registration
```
## Files Modified
1. [`src/components/entities/resident_registration/multi_step_resident_wizard.rs`](src/components/entities/resident_registration/multi_step_resident_wizard.rs) - Fixed HTTP request
2. [`src/config.rs`](src/config.rs) - New configuration module
3. [`src/lib.rs`](src/lib.rs) - Added config initialization
4. [`index.html`](index.html) - Cleaned up unused JavaScript
5. [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md) - Updated documentation
## Result
The portal client now properly authenticates with the portal-server using the `x-api-key` header, resolving the 401 authentication errors.

84
portal/QUICK_START.md Normal file
View File

@@ -0,0 +1,84 @@
# Portal Client - Quick Start
## 🚀 5-Minute Setup
### 1. Run Setup Script
```bash
./setup.sh
```
### 2. Start Portal Server
```bash
cd ../portal-server
cargo run -- --from-env --verbose
```
### 3. Start Portal Client
```bash
cd ../portal
source .env && trunk serve
```
### 4. Open Browser
```
http://127.0.0.1:8080
```
## 🔧 Manual Setup
### Portal Server (.env)
```bash
cd ../portal-server
cp .env.example .env
# Edit .env with your keys:
API_KEYS=dev_key_123,test_key_456
STRIPE_SECRET_KEY=sk_test_your_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_key
IDENTIFY_API_KEY=your_identify_key
```
### Portal Client (.env)
```bash
cd ../portal
# .env file (already created):
PORTAL_API_KEY=dev_key_123 # Must match server API_KEYS
```
## 🐛 Troubleshooting
### 401 Unauthorized?
- ✅ Check `PORTAL_API_KEY` matches server `API_KEYS`
- ✅ Run `source .env && trunk serve` (not just `trunk serve`)
- ✅ Verify server is running on port 3001
### Portal won't load?
- ✅ Install: `cargo install trunk`
- ✅ Add target: `rustup target add wasm32-unknown-unknown`
- ✅ Build first: `trunk build`
### Environment variables not working?
- ✅ Use: `source .env && trunk serve`
- ✅ Or: `PORTAL_API_KEY=dev_key_123 trunk serve`
- ✅ Or edit `index.html` directly with your API key
## 📞 Test API Connection
```bash
# Test server is working
curl -X GET http://127.0.0.1:3001/api/health \
-H "x-api-key: dev_key_123"
# Should return: {"status":"ok"}
```
## 🔄 Development Workflow
1. **Terminal 1**: `cd ../portal-server && cargo run -- --from-env --verbose`
2. **Terminal 2**: `cd ../portal && source .env && trunk serve`
3. **Browser**: `http://127.0.0.1:8080`
## 📚 More Help
- [Full README](README.md) - Complete documentation
- [Portal Server Setup](../portal-server/SETUP.md) - Server configuration
- [Portal Server README](../portal-server/README.md) - Server documentation

View File

@@ -34,21 +34,67 @@ Removed components:
- Admin panels
- Full platform navigation
## Building and Running
## Quick Setup
### 1. Set Up Portal Server
First, make sure the portal-server is running with API keys configured:
```bash
# In the portal-server directory
cd ../portal-server
cp .env.example .env
# Edit .env file with your API keys (see portal-server README)
cargo run -- --from-env --verbose
```
### 2. Configure Portal Client
Set up the API key for the portal client:
```bash
# In the portal directory
# The .env file is already created with a default API key
cat .env
```
Make sure the `PORTAL_API_KEY` in the portal `.env` matches one of the `API_KEYS` in the portal-server `.env`.
### 3. Run the Portal
```bash
# Install trunk if you haven't already
cargo install trunk
# Build the WASM application
trunk build
# Serve for development
trunk serve
# Load environment variables and serve
source .env && trunk serve
```
## Stripe Configuration
## Building and Running
### Development Mode
```bash
# Load environment variables and serve for development
source .env && trunk serve
# Or set the API key inline
PORTAL_API_KEY=dev_key_123 trunk serve
```
### Production Build
```bash
# Build the WASM application
PORTAL_API_KEY=your_production_api_key trunk build --release
```
## Configuration
### Environment Variables
Create a `.env` file in the portal directory:
```bash
# Portal Client Configuration
PORTAL_API_KEY=dev_key_123 # Must match portal-server API_KEYS
```
### Stripe Configuration
Update the Stripe publishable key in `index.html`:
```javascript
@@ -57,9 +103,100 @@ 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:
The portal connects to the portal-server running on `http://127.0.0.1:3001` with these endpoints:
- `POST /resident/create-payment-intent` - Create payment intent for resident registration
- `POST /api/resident/create-payment-intent` - Create payment intent for resident registration (requires API key)
### API Authentication
All API calls include the `x-api-key` header for authentication. The API key is configured via the `PORTAL_API_KEY` environment variable.
## Troubleshooting
### Getting 401 Unauthorized Errors?
**Problem**: API calls to portal-server return 401 errors
**Solutions**:
1. **Check API Key Configuration**:
```bash
# Portal client .env
PORTAL_API_KEY=dev_key_123
# Portal server .env (must include the same key)
API_KEYS=dev_key_123,other_keys_here
```
2. **Verify Server is Running**:
```bash
curl -X GET http://127.0.0.1:3001/api/health \
-H "x-api-key: dev_key_123"
```
3. **Check Environment Variable Loading**:
```bash
# Make sure to source the .env file
source .env && trunk serve
# Or set inline
PORTAL_API_KEY=dev_key_123 trunk serve
```
### Portal Won't Start?
**Problem**: Trunk serve fails or portal doesn't load
**Solutions**:
1. **Install Dependencies**:
```bash
cargo install trunk
rustup target add wasm32-unknown-unknown
```
2. **Check WASM Target**:
```bash
rustup target list --installed | grep wasm32
```
3. **Build First**:
```bash
trunk build
trunk serve
```
### API Key Not Working?
**Problem**: Environment variable substitution not working
**Solutions**:
1. **Check Trunk Version**: Make sure you have a recent version of Trunk
2. **Manual Configuration**: If environment substitution fails, edit `index.html` directly:
```javascript
const PORTAL_API_KEY = 'your_actual_api_key_here';
```
## Development Workflow
### 1. Start Portal Server
```bash
cd ../portal-server
cargo run -- --from-env --verbose
```
### 2. Start Portal Client
```bash
cd ../portal
source .env && trunk serve
```
### 3. Test Integration
```bash
# Test server directly
curl -X GET http://127.0.0.1:3001/api/health \
-H "x-api-key: dev_key_123"
# Open portal in browser
open http://127.0.0.1:8080
```
## Purpose

93
portal/TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,93 @@
# Portal Authentication Troubleshooting Guide
## Issue: 401 Errors - Missing Authentication Header
If you're getting 401 errors when the portal client calls the portal-server endpoints, follow this debugging checklist:
### 1. Verify API Key Configuration
**Server Side (portal-server/.env file):**
```
API_KEYS=dev_key_123,test_key_456
```
**Client Side**: The API key is now configured in Rust code at [`src/config.rs`](src/config.rs). For development, it's hardcoded to `dev_key_123` to match the server.
⚠️ **Important**: The client's API key must match one of the keys in the server's `API_KEYS` list.
### 2. Check Browser Console Logs
When you make a request, you should see these debug logs in the browser console:
```
✅ Portal configuration initialized
🔧 Portal config loaded - API key: Present
🔑 Using API key: dev_key_123
🔧 Creating payment intent...
🔧 Setting up Stripe payment for resident registration
```
### 3. Common Issues and Solutions
#### Issue: API Key authentication still failing
**Cause**: Client API key doesn't match server configuration
**Solution**:
1. Check [`src/config.rs`](src/config.rs) - the client uses `dev_key_123` by default
2. Ensure portal-server/.env has `API_KEYS=dev_key_123,test_key_456`
3. Restart both client and server after changes
#### Issue: Headers show correct API key but server still returns 401
**Cause**: Server API key mismatch
**Solution**:
1. Check portal-server/.env file has matching key in `API_KEYS`
2. Restart portal-server after changing .env
#### Issue: CORS errors
**Cause**: Portal-server CORS configuration
**Solution**: Ensure portal-server allows requests from `http://127.0.0.1:8080`
### 4. Manual Testing
Test the API key directly with curl:
```bash
curl -X POST http://127.0.0.1:3001/api/resident/create-payment-intent \
-H "Content-Type: application/json" \
-H "x-api-key: dev_key_123" \
-d '{"type":"resident_registration","amount":5000}'
```
### 5. Network Tab Inspection
1. Open browser Developer Tools (F12)
2. Go to Network tab
3. Make a request from the portal
4. Click on the request in the Network tab
5. Check the "Request Headers" section
6. Verify `x-api-key` header is present with value `dev_key_123`
### 6. Configuration Changes
To change the API key for production:
1. Edit [`src/config.rs`](src/config.rs) and update the `get_api_key()` function
2. Rebuild the client: `trunk build --release`
3. Update server's `.env` file to include the new key in `API_KEYS`
## Quick Start Commands
```bash
# 1. Start portal-server (in portal-server directory)
cd ../portal-server
cargo run
# 2. Start portal client (in portal directory)
cd ../portal
trunk serve --open
```
## Getting Help
If the issue persists:
1. Check all console logs in browser
2. Verify network requests in Developer Tools
3. Confirm both client and server .env files are correct
4. Test with curl to isolate client vs server issues

View File

@@ -1,2 +1,8 @@
[build]
target = "index.html"
target = "index.html"
[serve]
# Enable environment variable substitution
# Trunk will replace {{PORTAL_API_KEY}} with the value from the environment
# Set PORTAL_API_KEY environment variable before running trunk serve
env = true

View File

@@ -68,8 +68,10 @@
let elements;
let paymentElement;
// Stripe publishable key - replace with your actual key from Stripe Dashboard
// Configuration - replace with your actual keys
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51MCkZTC7LG8OeRdIcqmmoDkRwDObXSwYdChprMHJYoD2VRO8OCDBV5KtegLI0tLFXJo9yyvEXi7jzk1NAB5owj8i00DkYSaV9y';
// Note: API key authentication is now handled by Rust code
// Initialize Stripe when the script loads
document.addEventListener('DOMContentLoaded', function() {
@@ -84,74 +86,7 @@
}
});
// 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;
}
};
// Note: Payment intent creation is now handled by Rust code in multi_step_resident_wizard.rs
// Initialize Stripe Elements with client secret
window.initializeStripeElements = async function(clientSecret) {

76
portal/setup.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# Portal Client Setup Script
# This script helps set up the portal client with the correct API key configuration
set -e
echo "🏠 Portal Client Setup"
echo "====================="
# Check if we're in the right directory
if [ ! -f "Cargo.toml" ] || [ ! -f "index.html" ]; then
echo "❌ Error: Please run this script from the portal directory"
exit 1
fi
# Check if portal-server is configured
if [ ! -f "../portal-server/.env" ]; then
echo "⚠️ Warning: Portal server .env file not found"
echo " Please set up the portal-server first:"
echo " cd ../portal-server && cp .env.example .env"
echo " Then edit the .env file with your API keys"
echo ""
fi
# Create .env file if it doesn't exist
if [ ! -f ".env" ]; then
echo "📝 Creating .env file..."
cat > .env << EOF
# Portal Client Configuration
# This file configures the frontend portal app
# API Key for portal-server authentication
# This must match one of the API_KEYS in the portal-server .env file
PORTAL_API_KEY=dev_key_123
# Optional: Override server URL (defaults to http://127.0.0.1:3001)
# PORTAL_SERVER_URL=http://localhost:3001
EOF
echo "✅ Created .env file with default API key"
else
echo "✅ .env file already exists"
fi
# Check if trunk is installed
if ! command -v trunk &> /dev/null; then
echo "📦 Installing trunk..."
cargo install trunk
echo "✅ Trunk installed"
else
echo "✅ Trunk is already installed"
fi
# Check if wasm32 target is installed
if ! rustup target list --installed | grep -q "wasm32-unknown-unknown"; then
echo "🎯 Adding wasm32 target..."
rustup target add wasm32-unknown-unknown
echo "✅ WASM target added"
else
echo "✅ WASM target is already installed"
fi
echo ""
echo "🎉 Setup complete!"
echo ""
echo "Next steps:"
echo "1. Make sure portal-server is running:"
echo " cd ../portal-server && cargo run -- --from-env --verbose"
echo ""
echo "2. Start the portal client:"
echo " source .env && trunk serve"
echo ""
echo "3. Open your browser to:"
echo " http://127.0.0.1:8080"
echo ""
echo "📚 For troubleshooting, see README.md"

View File

@@ -1,13 +1,9 @@
pub mod step_payment_stripe;
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 multi_step_resident_wizard::*;

View File

@@ -9,6 +9,7 @@ use web_sys::console;
use serde_json::json;
use js_sys;
use crate::config::get_config;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::ResidentService;
use crate::components::common::forms::{MultiStepForm, FormStep, StepValidator, ValidationResult};
@@ -414,6 +415,11 @@ impl MultiStepResidentWizard {
"type": "resident_registration"
});
// Get configuration for API key and endpoint
let config = get_config();
let endpoint_url = config.get_endpoint_url("resident/create-payment-intent");
let api_key = config.api_key.clone();
// Create request to server endpoint
let mut opts = RequestInit::new();
opts.method("POST");
@@ -421,12 +427,13 @@ impl MultiStepResidentWizard {
let headers = web_sys::js_sys::Map::new();
headers.set(&"Content-Type".into(), &"application/json".into());
opts.headers(&headers);
headers.set(&"x-api-key".into(), &api_key.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",
&endpoint_url,
&opts,
).map_err(|e| format!("Failed to create request: {:?}", e))?;

View File

@@ -1,294 +0,0 @@
use yew::prelude::*;
use crate::models::company::{DigitalResidentFormData, DigitalResident};
use crate::services::ResidentService;
use crate::components::common::ui::progress_indicator::{ProgressIndicator, ProgressVariant, ProgressColor, ProgressSize};
use crate::components::common::ui::loading_spinner::LoadingSpinner;
use super::{SimpleStepInfo, StepPaymentStripe, ResidenceCard};
use web_sys::console;
#[derive(Properties, PartialEq)]
pub struct RefactoredResidentWizardProps {
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 RefactoredResidentWizardMsg {
NextStep,
PrevStep,
UpdateFormData(DigitalResidentFormData),
RegistrationComplete(DigitalResident),
RegistrationError(String),
}
pub struct RefactoredResidentWizard {
current_step: usize,
form_data: DigitalResidentFormData,
validation_errors: Vec<String>,
}
impl Component for RefactoredResidentWizard {
type Message = RefactoredResidentWizardMsg;
type Properties = RefactoredResidentWizardProps;
fn create(ctx: &Context<Self>) -> Self {
let current_step = if ctx.props().success_resident_id.is_some() {
2 // Success step
} else if ctx.props().show_failure {
1 // Payment step
} else {
0 // Start from beginning
};
Self {
current_step,
form_data: DigitalResidentFormData::default(),
validation_errors: Vec::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
RefactoredResidentWizardMsg::NextStep => {
// Simple validation for demo
if self.current_step == 0 {
if self.form_data.full_name.trim().is_empty() || self.form_data.email.trim().is_empty() {
self.validation_errors = vec!["Please fill in all required fields".to_string()];
return true;
}
}
self.validation_errors.clear();
if self.current_step < 2 {
self.current_step += 1;
}
true
}
RefactoredResidentWizardMsg::PrevStep => {
if self.current_step > 0 {
self.current_step -= 1;
}
true
}
RefactoredResidentWizardMsg::UpdateFormData(new_data) => {
self.form_data = new_data;
true
}
RefactoredResidentWizardMsg::RegistrationComplete(resident) => {
self.current_step = 2; // Move to success step
ctx.props().on_registration_complete.emit(resident);
true
}
RefactoredResidentWizardMsg::RegistrationError(error) => {
self.validation_errors = vec![error];
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="h-100 d-flex flex-column">
{if self.current_step < 2 {
html! {
<>
// Progress indicator using our generic component
<ProgressIndicator
current_step={self.current_step}
total_steps={2}
variant={ProgressVariant::Dots}
color={ProgressColor::Primary}
size={ProgressSize::Medium}
show_step_numbers={true}
/>
// Step content
<div class="flex-grow-1">
{self.render_current_step(ctx)}
</div>
// Navigation footer
{if self.current_step < 2 {
self.render_navigation_footer(ctx)
} else {
html! {}
}}
// Validation errors
{if !self.validation_errors.is_empty() {
html! {
<div class="alert alert-danger mt-3">
<ul class="mb-0">
{for self.validation_errors.iter().map(|error| {
html! { <li>{error}</li> }
})}
</ul>
</div>
}
} else {
html! {}
}}
</>
}
} else {
// Success step
html! {
<div class="flex-grow-1">
{self.render_success_step(ctx)}
</div>
}
}}
</div>
}
}
}
impl RefactoredResidentWizard {
fn render_current_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_form_update = link.callback(RefactoredResidentWizardMsg::UpdateFormData);
match self.current_step {
0 => html! {
<SimpleStepInfo
form_data={self.form_data.clone()}
on_change={on_form_update}
/>
},
1 => html! {
<StepPaymentStripe
form_data={self.form_data.clone()}
client_secret={Option::<String>::None}
processing_payment={false}
on_process_payment={link.callback(|_| RefactoredResidentWizardMsg::NextStep)}
on_payment_complete={link.callback(RefactoredResidentWizardMsg::RegistrationComplete)}
on_payment_error={link.callback(RefactoredResidentWizardMsg::RegistrationError)}
on_payment_plan_change={link.callback(|_| RefactoredResidentWizardMsg::NextStep)}
on_confirmation_change={link.callback(|_| RefactoredResidentWizardMsg::NextStep)}
/>
},
_ => html! { <div>{"Invalid step"}</div> }
}
}
fn render_navigation_footer(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div style="width: 120px;">
{if self.current_step > 0 {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| RefactoredResidentWizardMsg::PrevStep)}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
<div style="width: 150px;" class="text-end">
{if self.current_step == 0 {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| RefactoredResidentWizardMsg::NextStep)}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_success_step(&self, ctx: &Context<Self>) -> Html {
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">
<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 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

@@ -1,579 +0,0 @@
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 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]
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 position-relative">
<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! {}
}}
// 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>
}
}
}
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) - Using our generic ProgressIndicator
<div class="d-flex align-items-center">
<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)
<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! {
<ValidationToast
toast_type={ToastType::Warning}
title={"Required Fields Missing"}
messages={self.validation_errors.clone()}
show={self.show_validation_toast}
on_close={close_toast}
/>
}
}
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>
}
}
}

67
portal/src/config.rs Normal file
View File

@@ -0,0 +1,67 @@
//! Configuration management for the portal application
use web_sys::console;
/// Configuration for the portal application
pub struct Config {
/// API key for authenticating with portal-server
pub api_key: String,
/// Base URL for the portal-server API
pub api_base_url: String,
}
impl Config {
/// Load configuration from environment or use defaults
pub fn load() -> Self {
let api_key = Self::get_api_key();
let api_base_url = Self::get_api_base_url();
console::log_1(&format!("🔧 Portal config loaded - API key: {}",
if api_key.is_empty() { "Missing" } else { "Present" }).into());
Self {
api_key,
api_base_url,
}
}
/// Get API key from environment or use fallback
fn get_api_key() -> String {
// In a WASM environment, we can't access environment variables directly
// For now, use a hardcoded development key that matches the server
// TODO: In production, this should be configured via build-time environment variables
// or loaded from a secure configuration endpoint
let dev_key = "dev_key_123";
console::log_1(&format!("🔑 Using API key: {}", dev_key).into());
dev_key.to_string()
}
/// Get API base URL
fn get_api_base_url() -> String {
// For development, use localhost
// TODO: Make this configurable for different environments
"http://127.0.0.1:3001/api".to_string()
}
/// Get the full URL for a specific endpoint
pub fn get_endpoint_url(&self, endpoint: &str) -> String {
format!("{}/{}", self.api_base_url, endpoint.trim_start_matches('/'))
}
}
/// Global configuration instance
static mut CONFIG: Option<Config> = None;
/// Get the global configuration instance
pub fn get_config() -> &'static Config {
unsafe {
CONFIG.get_or_insert_with(Config::load)
}
}
/// Initialize the configuration (call this early in the application)
pub fn init_config() {
let _ = get_config();
console::log_1(&"✅ Portal configuration initialized".into());
}

View File

@@ -2,6 +2,7 @@ use wasm_bindgen::prelude::*;
mod app;
mod components;
mod config;
mod models;
mod services;
@@ -12,5 +13,9 @@ use app::App;
pub fn run_app() {
wasm_logger::init(wasm_logger::Config::default());
log::info!("Starting Zanzibar Digital Freezone Portal");
// Initialize configuration
config::init_config();
yew::Renderer::<App>::new().render();
}

45
portal/test-env.sh Normal file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
echo "🧪 Testing Portal Environment Configuration"
echo "=========================================="
# Check if .env file exists
if [ -f ".env" ]; then
echo "✅ .env file found"
echo "📄 Contents:"
cat .env
echo ""
else
echo "❌ .env file not found"
exit 1
fi
# Check if Trunk.toml exists and has env = true
if [ -f "Trunk.toml" ]; then
echo "✅ Trunk.toml found"
if grep -q "env = true" Trunk.toml; then
echo "✅ Environment variable support enabled in Trunk.toml"
else
echo "❌ Environment variable support not enabled in Trunk.toml"
echo "💡 Add 'env = true' to your Trunk.toml"
fi
echo ""
else
echo "❌ Trunk.toml not found"
fi
# Test environment variable
echo "🔍 Testing PORTAL_API_KEY environment variable:"
if [ -n "$PORTAL_API_KEY" ]; then
echo "✅ PORTAL_API_KEY is set: $PORTAL_API_KEY"
else
echo "❌ PORTAL_API_KEY is not set"
echo "💡 Run: export PORTAL_API_KEY=dev_key_123"
fi
echo ""
echo "🚀 To test the portal with proper environment setup:"
echo "1. export PORTAL_API_KEY=dev_key_123"
echo "2. trunk serve --open"
echo ""
echo "🔧 Check browser console for debugging logs when making requests"