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