portal, platform, and server fixes
This commit is contained in:
93
portal/AUTHENTICATION_FIX.md
Normal file
93
portal/AUTHENTICATION_FIX.md
Normal 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
84
portal/QUICK_START.md
Normal 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
|
155
portal/README.md
155
portal/README.md
@@ -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
93
portal/TROUBLESHOOTING.md
Normal 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
|
@@ -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
|
@@ -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
76
portal/setup.sh
Executable 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"
|
@@ -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::*;
|
@@ -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))?;
|
||||
|
||||
|
@@ -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>
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
67
portal/src/config.rs
Normal 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());
|
||||
}
|
@@ -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
45
portal/test-env.sh
Normal 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"
|
Reference in New Issue
Block a user