Fluid Platform Mobile Widget Implementation Guide
Overview
Mobile widgets on the Fluid platform allow you to create custom functionality that can be integrated into mobile apps through embedded web views. These widgets are web applications that maintain secure authentication and access to company data while being displayed within the mobile app interface.
Architecture Overview
Widget Components
- Standalone Web Application: Each widget is hosted at its own URL
- Authentication Layer: Secure token-based authentication via Fluid's auth token system
- Data Integration: Connection to backend systems (your existing databases, APIs, etc.)
- Mobile App Integration: Embedded via web views in mobile applications
Key Concepts
- Mobile Widgets: Custom components for mobile app integration via
MobileWidget
model - Authentication Tokens: Secure access tokens generated by
UserCompany#generate_auth_token
- API Access: Widgets can access both Fluid APIs and your own backend APIs
- User Identification: Using
external_id
fromUserCompany
as the primary identifier for business operations
Implementation Steps
Step 1: Widget Planning
Before implementation, consider:
- Widget Purpose: KPIs, volumes, bonus tracking, team performance, etc.
- Data Sources: Determine if data comes from Fluid or your existing systems
- Update Frequency: Real-time vs. periodic updates
- Required User Fields: Ensure users have
external_id
for business operations
Step 2: Create the Widget Application
Basic HTML Structure
<!DOCTYPE html> <html> <head> <title>Widget Name</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> /* Mobile-optimized styles */ body { margin: 0; padding: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .loading { text-align: center; padding: 20px; } .error { color: #e74c3c; padding: 20px; text-align: center; } .widget-content { padding: 15px; } .metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 20px; } .metric { background: #f5f5f5; padding: 15px; border-radius: 8px; text-align: center; } .label { display: block; font-size: 12px; color: #666; margin-bottom: 5px; } .value { display: block; font-size: 20px; font-weight: bold; color: #333; } </style> </head> <body> <div id="widget-container"> <div class="loading">Loading...</div> </div> <script src="widget.js"></script> </body> </html>
JavaScript Implementation
// widget.js class FluidWidget { constructor() { this.authToken = this.getAuthToken(); this.fluidApiUrl = 'https://your-fluid-api.com/api'; // Configure this this.backendApiUrl = 'https://your-backend.com/api'; // Your backend this.init(); } getAuthToken() { // Extract auth token from URL parameters const urlParams = new URLSearchParams(window.location.search); return urlParams.get('auth_token'); } async init() { if (!this.authToken) { this.showError('No authentication token provided'); return; } try { // Validate token with Fluid and get user info const fluidUser = await this.getUserFromFluid(); if (!fluidUser) { this.showError('Failed to authenticate with Fluid'); return; } // Validate user has required fields if (!fluidUser.externalId) { this.showError('User account not properly configured (missing external ID)'); return; } // Fetch widget-specific data from your backend using external_id const widgetData = await this.fetchWidgetData(fluidUser); // Render the widget this.render(widgetData, fluidUser); } catch (error) { this.showError('Failed to load widget data'); console.error('Widget error:', error); } } async getUserFromFluid() { try { // Use the auth token in the URL path - the API accepts either user_company.id or auth_token.token // The auth_token is also passed as a Bearer token for authentication const response = await fetch(`${this.fluidApiUrl}/v2025-06/users/${this.authToken}`, { headers: { 'Authorization': `Bearer ${this.authToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { console.error('Fluid authentication failed:', response.status); return null; } const userData = await response.json(); // Map response to expected structure based on UserCompanyBlueprinter const userCompany = userData.user_company || userData; return { id: userCompany.id, externalId: userCompany.external_id, companyId: userCompany.company_id, email: userCompany.email, username: userCompany.username, roles: userCompany.roles, metadata: userCompany.metadata || {} }; } catch (error) { console.error('Error calling Fluid API:', error); return null; } } async fetchWidgetData(fluidUser) { // Use external_id as the primary identifier for your business logic const response = await fetch( `${this.backendApiUrl}/widget-data?external_id=${encodeURIComponent(fluidUser.externalId)}`, { headers: { 'Authorization': `Bearer ${this.authToken}`, 'X-Company-Id': fluidUser.companyId || '', 'Content-Type': 'application/json' } } ); if (!response.ok) { throw new Error('Failed to fetch widget data'); } return await response.json(); } render(data, fluidUser) { const container = document.getElementById('widget-container'); container.innerHTML = ` <div class="widget-content"> <h2>Welcome, ${data.userName || fluidUser.username}</h2> <div class="user-info"> <small>ID: ${fluidUser.externalId} | Company: ${fluidUser.companyId}</small> </div> <div class="metrics"> <div class="metric"> <span class="label">Personal Volume</span> <span class="value">$${(data.personalVolume || 0).toLocaleString()}</span> </div> <div class="metric"> <span class="label">Team Volume</span> <span class="value">$${(data.teamVolume || 0).toLocaleString()}</span> </div> <div class="metric"> <span class="label">Current Rank</span> <span class="value">${data.rank || 'N/A'}</span> </div> <div class="metric"> <span class="label">New Enrollments</span> <span class="value">${data.newEnrollments || 0}</span> </div> </div> </div> `; } showError(message) { const container = document.getElementById('widget-container'); container.innerHTML = `<div class="error">${message}</div>`; } } // Initialize widget when page loads document.addEventListener('DOMContentLoaded', () => { new FluidWidget(); });
Step 3: Backend API Implementation
Node.js/Express Example
const express = require('express'); const axios = require('axios'); const app = express(); // Configuration const FLUID_API_URL = process.env.FLUID_API_URL || 'https://your-fluid-api.com/api'; // Middleware to validate Fluid auth token and get user info async function validateFluidToken(req, res, next) { const authToken = req.headers.authorization?.replace('Bearer ', ''); if (!authToken) { return res.status(401).json({ error: 'No auth token provided' }); } try { // Validate token with Fluid API using auth token in URL path const response = await axios.get(`${FLUID_API_URL}/v2025-06/users/${authToken}`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } }); const fluidUser = response.data.user_company || response.data; // Validate user has required fields if (!fluidUser.external_id) { return res.status(400).json({ error: 'User missing external_id - cannot proceed with business operations' }); } // Attach user to request object req.fluidUser = { id: fluidUser.id, externalId: fluidUser.external_id, companyId: fluidUser.company_id, email: fluidUser.email, username: fluidUser.username, roles: fluidUser.roles, metadata: fluidUser.metadata || {} }; next(); } catch (error) { if (error.response?.status === 401) { return res.status(401).json({ error: 'Invalid or expired token' }); } else if (error.response?.status === 404) { return res.status(404).json({ error: 'User not found for this token' }); } console.error('Fluid API error:', error.message); res.status(500).json({ error: 'Authentication error' }); } } // Widget data endpoint app.get('/api/widget-data', validateFluidToken, async (req, res) => { const externalId = req.query.external_id || req.fluidUser.externalId; const companyId = req.headers['x-company-id'] || req.fluidUser.companyId; try { // Use external_id as the primary identifier for your business logic const widgetData = await getWidgetDataFromDatabase(externalId, companyId); res.json(widgetData); } catch (error) { console.error('Error fetching widget data:', error); res.status(500).json({ error: 'Failed to fetch data' }); } }); async function getWidgetDataFromDatabase(externalId, companyId) { // This would connect to your existing database/systems // Use external_id as customer_id or rep_id in your system // Example query to your database: // const userData = await db.query( // 'SELECT * FROM users WHERE external_id = ? AND company_id = ?', // [externalId, companyId] // ); return { userName: 'John Doe', personalVolume: 5420.50, teamVolume: 45230.75, rank: 'Gold', newEnrollments: 3, monthlyGoal: 10000, goalProgress: 54.2 }; } // CORS configuration (if needed) app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Company-Id'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); next(); }); app.listen(3000, () => { console.log('Widget backend running on port 3000'); });
Ruby on Rails Backend Example
class Api::WidgetController < ApplicationController before_action :validate_fluid_token def widget_data external_id = params[:external_id] || @fluid_user[:external_id] company_id = request.headers['X-Company-Id'] || @fluid_user[:company_id] # Use external_id for business operations widget_data = get_widget_data_from_database(external_id, company_id) render json: widget_data rescue StandardError => e Rails.logger.error "Error fetching widget data: #{e.message}" render json: { error: 'Failed to fetch data' }, status: :internal_server_error end private def validate_fluid_token auth_token = request.headers['Authorization']&.gsub(/^Bearer /, '') return render json: { error: 'No auth token provided' }, status: :unauthorized if auth_token.blank? begin response = HTTParty.get( "#{ENV['FLUID_API_URL']}/api/v2025-06/users/#{auth_token}", headers: { 'Authorization' => "Bearer #{auth_token}", 'Content-Type' => 'application/json' } ) if response.success? fluid_user = response.parsed_response['user_company'] || response.parsed_response if fluid_user['external_id'].blank? return render json: { error: 'User missing external_id - cannot proceed' }, status: :bad_request end @fluid_user = { id: fluid_user['id'], external_id: fluid_user['external_id'], company_id: fluid_user['company_id'], email: fluid_user['email'], username: fluid_user['username'], roles: fluid_user['roles'] } else render json: { error: 'Invalid or expired token' }, status: :unauthorized end rescue StandardError => e Rails.logger.error "Fluid API error: #{e.message}" render json: { error: 'Authentication error' }, status: :internal_server_error end end def get_widget_data_from_database(external_id, company_id) # Connect to your existing database/systems using external_id { userName: 'John Doe', personalVolume: 5420.50, teamVolume: 45230.75, rank: 'Gold', newEnrollments: 3, monthlyGoal: 10000, goalProgress: 54.2 } end end
Step 4: Authentication Flow Details
How Fluid Generates Auth Tokens
Based on the codebase analysis, here's how authentication works:
- Token Generation:
UserCompany#generate_auth_token
creates anAuthToken
record - Token Structure: Auth tokens are stored in the
auth_tokens
table, linked touser_companies
- Token Validation: Fluid validates tokens through
UserCompany.find_with_active_auth_token(token)
- Mobile Widget Integration: The mobile app automatically appends the auth token to widget URLs
Token Validation Process
# In Fluid's codebase (CommonEntityConcern) def handle_user_company_token(token) user_company = UserCompany.active.find_by(token: token) || UserCompany.active.find_with_active_auth_token(token) return unless user_company # Set current entities for the request assign_current_user(user_company.user) assign_current_company(user_company.company) assign_current_affiliate(user_company) user_company end
Step 5: Common Backend Endpoints
These endpoints should be created in YOUR backend system (not Fluid's):
// KPI Dashboard Data GET /api/widgets/kpi-dashboard?external_id={external_id} Headers: Authorization: Bearer {token} Response: { personalVolume: 5420.50, teamVolume: 45230.75, monthlyGoal: 10000, goalProgress: 54.2, rank: "Gold", nextRankProgress: 75 } // Team Performance Data GET /api/widgets/team-performance?external_id={external_id} Headers: Authorization: Bearer {token} Response: { directTeamCount: 12, totalTeamSize: 156, activeMembers: 89, newMembersThisMonth: 5, topPerformers: [ { name: "Jane Doe", volume: 12450.00, rank: "Silver" }, { name: "Bob Smith", volume: 8930.50, rank: "Bronze" } ] } // Bonus Tracking Information GET /api/widgets/bonus-tracker?external_id={external_id} Headers: Authorization: Bearer {token} Response: { currentBonuses: [ { name: "Fast Start Bonus", amount: 500, qualified: true }, { name: "Leadership Bonus", amount: 1200, qualified: false } ], projectedEarnings: 2340.00, qualifiedBonuses: 3, pendingQualifications: 2 }
Step 6: Mobile App Integration
Widget URL Format
https://your-backend.com/widgets/{widget-name}
Note: The auth_token is appended by Fluid automatically when the widget is loaded in the mobile app.
Mobile Widget Configuration
Based on the codebase, widgets are configured through the MobileWidget
model:
# Example mobile widget configuration mobile_widget = MobileWidget.create!( company: company, name: "KPI Dashboard", embed_url: "https://your-backend.com/widgets/kpi-dashboard", active: true )
The mobile app will:
- Fetch widget configuration from Fluid via
/api/mobile_widgets/:id
- Automatically append authentication token to the embed URL
- Load widget in a WebView component
- Handle navigation and refresh events
Step 7: Common Widget Types
1. KPI Dashboard Widget
class KPIDashboard extends FluidWidget { render(data, fluidUser) { const container = document.getElementById('widget-container'); container.innerHTML = ` <div class="kpi-dashboard"> <div class="user-header"> <h2>${fluidUser.username}</h2> <p>Customer ID: ${fluidUser.externalId}</p> </div> <div class="kpi-grid"> <div class="kpi-card"> <div class="kpi-value">$${data.personalVolume.toLocaleString()}</div> <div class="kpi-label">Personal Volume</div> <div class="kpi-progress"> <div class="progress-bar" style="width: ${data.personalProgress}%"></div> </div> </div> <div class="kpi-card"> <div class="kpi-value">$${data.teamVolume.toLocaleString()}</div> <div class="kpi-label">Team Volume</div> </div> <div class="kpi-card"> <div class="kpi-value">${data.newEnrollments}</div> <div class="kpi-label">New Enrollments</div> </div> <div class="kpi-card"> <div class="kpi-value">${data.rank}</div> <div class="kpi-label">Current Rank</div> </div> </div> </div> `; } }
2. Team Performance Widget
class TeamPerformance extends FluidWidget { async fetchWidgetData(fluidUser) { const response = await fetch( `${this.backendApiUrl}/widgets/team-performance?external_id=${fluidUser.externalId}`, { headers: { 'Authorization': `Bearer ${this.authToken}`, 'X-Company-Id': fluidUser.companyId } } ); if (!response.ok) { throw new Error('Failed to fetch team data'); } return await response.json(); } render(data, fluidUser) { const container = document.getElementById('widget-container'); const teamHtml = data.topPerformers.map(member => ` <div class="team-member"> <div class="member-info"> <span class="member-name">${member.name}</span> <span class="member-rank">${member.rank}</span> </div> <span class="member-volume">$${member.volume.toLocaleString()}</span> </div> `).join(''); container.innerHTML = ` <div class="team-widget"> <div class="team-summary"> <h3>Team Overview</h3> <div class="stats-grid"> <div class="stat"> <span class="stat-value">${data.directTeamCount}</span> <span class="stat-label">Direct Team</span> </div> <div class="stat"> <span class="stat-value">${data.totalTeamSize}</span> <span class="stat-label">Total Team</span> </div> <div class="stat"> <span class="stat-value">${data.activeMembers}</span> <span class="stat-label">Active</span> </div> </div> </div> <div class="top-performers"> <h3>Top Performers</h3> ${teamHtml} </div> </div> `; } }
Best Practices
Authentication & Security
- Always validate tokens server-side: Never trust client-side validation alone
- Use HTTPS everywhere: All communications must be encrypted
- Handle token expiration: Gracefully handle expired tokens with clear user feedback
- Validate user fields: Always check for required fields like
external_id
- Use external_id consistently: This should be your primary user identifier for business logic
Performance
- Implement caching: Cache frequently accessed data appropriately
- Lazy loading: Load data progressively for better perceived performance
- Batch API calls: Combine multiple requests where possible
- Optimize payloads: Keep data transfers minimal for mobile networks
- Error retry logic: Implement smart retry for failed requests
User Experience
- Responsive design: Test on all device sizes
- Loading states: Always show loading indicators
- Error messages: Provide clear, actionable error messages
- Offline handling: Consider offline scenarios for mobile users
- Touch-friendly: Ensure all interactive elements are easily tappable
Development Workflow
Local Development
- Use ngrok or similar for testing with real tokens
- Mock Fluid API responses for faster development
Testing
- Test with real Fluid tokens
- Verify external_id mapping
- Test token expiration scenarios
- Test with users missing required fields
Deployment
- Ensure all endpoints use HTTPS
- Set up proper logging and monitoring
- Configure CORS if needed
- Document API endpoints for mobile team
Configuration in Fluid
Widget Registration
Register widgets in Fluid through the admin interface or programmatically:
# Create a mobile widget in Fluid mobile_widget = MobileWidget.create!( company: company, name: "KPI Dashboard", description: "Real-time performance metrics", embed_url: "https://your-backend.com/widgets/kpi-dashboard", height: 400, # Height in pixels (100-1000) cover_image_url: "https://example.com/widget-cover.jpg", page_scope: { home: true, explore: false }, active: true )
Mobile Widget Management API
Fluid provides comprehensive API endpoints for managing mobile widgets including resizing and configuration updates.
Available Endpoints
1. List Mobile Widgets
GET /api/company/mobile_widgets
Authorization: Bearer {company_admin_token}
2. Get Mobile Widget Details
GET /api/company/mobile_widgets/{widget_id}
Authorization: Bearer {company_admin_token}
3. Create Mobile Widget
POST /api/company/mobile_widgets Authorization: Bearer {company_admin_token} Content-Type: application/json { "mobile_widget": { "name": "KPI Dashboard", "embed_url": "https://your-backend.com/widgets/kpi-dashboard", "height": 400, "cover_image_url": "https://example.com/cover.jpg", "page_scope": { "home": true, "explore": false }, "droplet_installation_id": null } }
4. Update Mobile Widget (Including Resizing)
PUT /api/company/mobile_widgets/{widget_id} Authorization: Bearer {company_admin_token} Content-Type: application/json { "mobile_widget": { "name": "Updated Widget Name", "embed_url": "https://your-backend.com/widgets/updated-url", "height": 600, // Resize widget (100-1000 pixels) "cover_image_url": "https://example.com/new-cover.jpg", "page_scope": { "home": true, "explore": true } } }
5. Delete Mobile Widget
DELETE /api/company/mobile_widgets/{widget_id}
Authorization: Bearer {company_admin_token}
Widget Properties
Based on the codebase analysis, mobile widgets support the following configurable properties:
Required Properties (for creation)
name
(string): Display name for the widgetembed_url
(string): URL of your widget applicationheight
(integer): Widget height in pixels (100-1000)cover_image_url
(string): Preview image URL for the widget
Optional Properties
page_scope
(object): Controls where the widget appearshome
(boolean): Show on home pageexplore
(boolean): Show on explore page
droplet_installation_id
(integer, nullable): Link to droplet installation if applicable
Widget Sizing and Dimensions
Mobile widgets have flexible sizing options:
Height Constraints
- Minimum height: 100 pixels
- Maximum height: 1000 pixels
- Default behavior: Widget width is responsive to mobile screen size
- Height control: Explicitly set via the
height
property
Responsive Behavior
The mobile app handles widget display with these characteristics:
- Width: Automatically adapts to mobile screen width
- Height: Fixed based on the
height
property you specify - Overflow: Scrollable if content exceeds specified height
- Aspect ratio: Maintained within the specified height constraint
Dynamic Widget Resizing
You can programmatically resize widgets using the update API:
// Example: Resize a widget based on content async function resizeWidget(widgetId, newHeight) { const response = await fetch(`/api/company/mobile_widgets/${widgetId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ mobile_widget: { height: Math.max(100, Math.min(1000, newHeight)) // Ensure within bounds } }) }); if (response.ok) { console.log('Widget resized successfully'); } }
Page Scope Configuration
Control where your widgets appear in the mobile app:
// Show widget on both home and explore pages { "page_scope": { "home": true, "explore": true } } // Show widget only on home page { "page_scope": { "home": true, "explore": false } } // Show widget only on explore page { "page_scope": { "home": false, "explore": true } }
Troubleshooting
Common Issues and Solutions
Authentication Failures
// Check token format console.log('Token:', authToken?.substring(0, 10) + '...'); // Verify API endpoint console.log('Calling:', `${FLUID_API_URL}/api/me`); // Log response status console.log('Response status:', response.status);
Missing External ID
// Always validate external_id before business operations if (!fluidUser.externalId) { console.error('User missing external_id'); // Handle appropriately - show error or use fallback }
CORS Issues
// Backend CORS configuration app.use(cors({ origin: '*', // Configure appropriately for production credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Company-Id'] }));
Performance Issues
// Implement caching const cache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getCachedData(key, fetcher) { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const data = await fetcher(); cache.set(key, { data, timestamp: Date.now() }); return data; }
Deployment Checklist
- All URLs use HTTPS
- Fluid API endpoint correctly configured
- Authentication flow thoroughly tested
- External ID validation implemented
- Error handling covers all scenarios
- Mobile-responsive design verified
- Performance tested with production data volumes
- CORS properly configured (if needed)
- Logging and monitoring configured
- API documentation provided to mobile team
- Widget registered in Fluid admin
- Tested in actual mobile app environment
- Fallback behavior for missing user fields
- Token expiration handling implemented
- Security review completed
Support and Resources
- Fluid API Documentation: Contact Fluid support for API documentation
- Mobile Integration: Coordinate with mobile app development team
- Widget Examples: Reference implementations in shared repository
- Support: Contact Fluid support for platform-specific questions
Key Implementation Notes
- Authentication Flow: Fluid automatically appends auth tokens to widget URLs when loaded in the mobile app
- Token Validation: Use standard HTTP Bearer token authentication to validate with Fluid's API
- External ID Usage: The
external_id
field inUserCompany
is your primary business identifier - Mobile Widget Model: Widgets are configured through Fluid's
MobileWidget
model - Auth Token Generation: Tokens are generated via
UserCompany#generate_auth_token
and stored inauth_tokens
table
This guide provides a complete foundation for creating mobile widgets on the Fluid platform, validated against the actual codebase implementation.