octopus_sdk_flutter 1.7.1
octopus_sdk_flutter: ^1.7.1 copied to clipboard
Flutter plugin for Octopus Community SDK
Octopus SDK Flutter #
A Flutter plugin for integrating Octopus Community into your Flutter applications.
📖 Official Documentation: https://doc.octopuscommunity.com/
Note: This Flutter SDK is based on the official Octopus Community SDKs. For detailed information about Octopus features, authentication methods, and integration concepts, please refer to the official documentation. Some features of the native SDKs are not yet available.
Features #
- 🎨 Theme Customization: Colors, font sizes, and logos
- 📱 Cross-Platform: iOS and Android support
- 🖼️ Flexible UI: Embedded widget
- 🔧 Easy Integration: Simple API with comprehensive documentation
- 👤 User Management: Connect, disconnect, and manage user sessions and profile information
- 🎯 Complete SDK Lifecycle: Initialize, authenticate, display UI, and cleanup
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
octopus_sdk_flutter: ^1.7.1
Then run:
flutter pub get
Testing the Example Project #
To test the example project, run the example app and follow the integration examples provided in the /example folder. You will need a sandbox community, an API Key and an SSO jwt secret.
Platform-Specific Configuration #
Android Configuration #
1. Update android/app/build.gradle
android {
compileSdk 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
2. Update MainActivity.kt
Important: You must update your android/app/src/main/kotlin/your/package/name/MainActivity.kt file to extend FlutterFragmentActivity instead of FlutterActivity:
package com.yourpackage.yourapp
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()
This is required for the Octopus SDK to work properly with the embedded widget functionality.
3. Add Internet Permission
In android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application>
<!-- Your existing configuration -->
</application>
</manifest>
iOS Configuration #
1. Update ios/Podfile
# Minimum iOS version
platform :ios, '14.0'
# CocoaPods configuration
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Required for Octopus SDK
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
end
end
end
2. Update ios/Runner/Info.plist
Add network permissions for API calls:
<dict>
<!-- Your existing configuration -->
<!-- Allow arbitrary loads for development -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
⚠️ gRPC-Swift / gRPC-Core naming conflict
If you depend on gRPC-Core, you may hit a naming conflict at build time because both pods resolve to gRPC. To fix:
- Copy gRPC-Swift.podspec.json from the native SDK repo into your project
ios/folder. - In
ios/Podfile, add:pod 'gRPC-Swift', :podspec => 'gRPC-Swift.podspec.json'.
Core SDK Functions #
The Octopus SDK Flutter provides four main functions to manage the complete lifecycle:
1. Initialize SDK #
Set up the SDK with your API key
2. Connect User #
Authenticate and connect a user to the Octopus Community
3. Show Octopus UI #
Display the Octopus Community interface (embedded)
4. Disconnect User #
Log out the current user and clean up the session
Quick Start Guide #
Step 1: Basic Setup #
import 'package:octopus_sdk_flutter/octopus_sdk_flutter.dart';
import 'package:octopus_sdk_flutter/octopus_theme.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final octopus = OctopusSdkFlutter();
bool isInitialized = false;
bool isUserConnected = false;
}
Step 2: Initialize SDK #
// Initialize SDK
await octopus.initializeOctopusSDK(
apiKey: 'your_api_key',
appManagedFields: [], // Optional: fields managed by your app (NICKNAME, AVATAR, BIO)
);
Step 3 (optional): Connect User #
⚠️ Note: For security reasons, the jwt has to be generated on your backend. Never leave the sso secret inside your app code/build!
// Connect User
await octopus.connectUser(
userId: 'unique_user_id',
token: 'your_jwt_token_here', // Replace with actual token from your backend
nickname: 'John Doe', // Optional
bio: 'Flutter developer', // Optional
picture: 'https://example.com/avatar.jpg' // Optional: URL or base64
);
Step 4: Show Octopus UI #
Once the SDK is initialized, you can display the Octopus Community interface.
// Show Octopus UI - Embedded Widget
Container(
width: double.infinity,
height: 400,
child: OctopusView(
navBarTitle: 'Embedded Community',
navBarPrimaryColor: true, // Use primary color for nav bar background
theme: OctopusTheme(
primaryMain: Colors.purple,
fontSizeTitle1: 24,
fontSizeBody1: 16,
),
// see sample app in /example for examples on how to use the callback features
onNavigateToLogin: () {
// Handle navigation to login
print('Navigate to login');
},
onModifyUser: (fieldToEdit) {
// Handle user profile modification
print('Modify user field: $fieldToEdit');
},
),
)
Step 5: Disconnect User #
// Disconnect User
await octopus.disconnectUser();
Complete Example #
Here's a complete working example that demonstrates all functions:
import 'package:flutter/material.dart';
import 'main_embedded.dart';
import 'main_modal.dart';
import 'package:octopus_sdk_flutter/octopus_sdk_flutter.dart';
import 'package:octopus_sdk_flutter/octopus_theme.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Octopus SDK - Embedded Mode',
home: const MyApp(),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final octopus = OctopusSdkFlutter();
bool isInitialized = false;
bool isUserConnected = false;
String status = 'Not initialized';
@override
void initState() {
super.initState();
initializeSDK();
}
// Initialize SDK
Future<void> initializeSDK() async {
setState(() => status = 'Initializing...');
try {
await octopus.initializeOctopusSDK(
apiKey: 'your_api_key',
appManagedFields: ['NICKNAME', 'AVATAR'], // Valid values: NICKNAME, AVATAR, BIO
);
setState(() {
isInitialized = true;
status = 'SDK initialized - Ready to connect user';
});
} catch (e) {
setState(() => status = 'Initialization error: $e');
}
}
// Connect User
Future<void> connectUser() async {
setState(() => status = 'Connecting user...');
try {
final token = 'your_jwt_token_here'; // Replace with actual token from your backend
await octopus.connectUser(
userId: 'user123',
token: token,
nickname: 'John Doe'
);
setState(() {
isUserConnected = true;
status = 'User connected - Ready to show UI';
});
} catch (e) {
setState(() => status = 'Connection error: $e');
}
}
// Show UI
Future<void> showOctopusUI() async {
octopus.showOctopusView(
context,
navBarTitle: 'Community',
navBarPrimaryColor: true,
theme: OctopusTheme(
primaryMain: Colors.blue,
fontSizeTitle1: 26,
),
onNavigateToLogin: () {
print('Navigate to login');
// After user completes login, call octopus.connectUser() with the user's credentials
},
onModifyUser: (fieldToEdit) {
print('Modify user field: $fieldToEdit');
},
);
}
// Disconnect User
Future<void> disconnectUser() async {
await octopus.disconnectUser();
setState(() {
isUserConnected = false;
status = 'User disconnected';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Octopus SDK Demo')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Status: $status'),
SizedBox(height: 20),
ElevatedButton(
onPressed: isInitialized && !isUserConnected ? connectUser : null,
child: Text('2. Connect User'),
),
ElevatedButton(
onPressed: isUserConnected ? showOctopusUI : null,
child: Text('3. Show Octopus UI'),
),
ElevatedButton(
onPressed: isUserConnected ? disconnectUser : null,
child: Text('4. Disconnect User'),
),
],
),
),
);
}
}
Theme Customization #
The OctopusTheme class allows complete customization of the Octopus Community interface:
Colors #
final theme = OctopusTheme(
primaryMain: Color(0xFF6200EA), // Main brand color
primaryLowContrast: Color(0x336200EA), // Low contrast variant
primaryHighContrast: Colors.white, // High contrast variant
onPrimary: Colors.white, // Content color on primary
);
Font Sizes #
final theme = OctopusTheme(
fontSizeTitle1: 26, // Main titles (default: 26)
fontSizeTitle2: 20, // Secondary titles (default: 20)
fontSizeBody1: 17, // Main body text (default: 17)
fontSizeBody2: 14, // Secondary body text (default: 14)
fontSizeCaption1: 12, // Main captions (default: 12)
fontSizeCaption2: 10, // Secondary captions (default: 10)
);
Custom Logo #
final theme = OctopusTheme(
logoBase64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...', // Full base64
// or
logoBase64: 'iVBORw0KGgoAAAANSUhEUgAA...', // Base64 without prefix
);
Complete Theme Example #
final customTheme = OctopusTheme(
// Brand colors
primaryMain: const Color(0xFF1976D2),
primaryLowContrast: const Color(0xFF1976D2).withOpacity(0.12),
primaryHighContrast: Colors.white,
onPrimary: Colors.white,
// Typography scale
fontSizeTitle1: 32,
fontSizeTitle2: 24,
fontSizeBody1: 18,
fontSizeBody2: 16,
fontSizeCaption1: 14,
fontSizeCaption2: 12,
// Branding
logoBase64: 'your_logo_base64_here',
);
API Reference #
OctopusSdkFlutter Methods #
initializeOctopusSDK
Future<void> initializeOctopusSDK({
required String apiKey,
List<String>? appManagedFields,
})
Parameters:
apiKey(required): Your Octopus Community API keyappManagedFields(optional): List of profile fields managed by your app instead of Octopus Community
appManagedFields Details:
This parameter allows you to specify which user profile fields should be managed by your application rather than by Octopus Community. When a field is in this list, users will not be able to edit it directly in the Octopus Community interface. Instead, your app should handle editing these fields through the onModifyUser callback.
Valid field names (only these 3 values are supported):
'NICKNAME': User's display name'AVATAR': User's profile picture'BIO': User's biography/description
Example:
await octopus.initializeOctopusSDK(
apiKey: 'your-api-key',
appManagedFields: ['NICKNAME', 'AVATAR', 'BIO'],
);
If appManagedFields is empty or null, all fields will be managed by Octopus Community (default behavior).
connectUser
Future<void> connectUser({
required String userId,
required String token,
String? nickname,
String? bio,
String? picture
})
OctopusView Widget
OctopusView({
String? navBarTitle, // Navigation bar title
bool navBarPrimaryColor = false, // Use primary color for nav bar
bool showBackButton = true, // Show back button (Android only)
OctopusTheme? theme, // Custom theme
required VoidCallback onNavigateToLogin, // Login navigation callback
Function(String?)? onModifyUser, // User modification callback
Function(String)? onCurrentPageChanged, // Page change callback
})
Parameters:
navBarTitle: Title text to display in the embedded view's navigation barnavBarPrimaryColor: Whether to use the primary theme color for the navigation bar backgroundshowBackButton: Whether to show the back button in the navigation bar (Android only)theme: Optional custom theme for colors, fonts, and logoonNavigateToLogin: Required callback for login navigationonModifyUser: Optional callback for user profile modificationonCurrentPageChanged: Optional callback for page change events
Features:
- Embedded Widget: Integrates seamlessly into your Flutter UI
- Callback Support: Full callback support for navigation and user actions
- Cross-platform: Consistent behavior on iOS and Android
OctopusTheme Properties #
| Property | Type | Default | Description |
|---|---|---|---|
primaryMain |
Color? |
null |
Main brand color |
primaryLowContrast |
Color? |
null |
Low contrast variant |
primaryHighContrast |
Color? |
null |
High contrast variant |
onPrimary |
Color? |
null |
Content color on primary |
fontSizeTitle1 |
int? |
26 |
Main titles font size |
fontSizeTitle2 |
int? |
20 |
Secondary titles font size |
fontSizeBody1 |
int? |
17 |
Main body text font size |
fontSizeBody2 |
int? |
14 |
Secondary body text font size |
fontSizeCaption1 |
int? |
12 |
Main captions font size |
fontSizeCaption2 |
int? |
10 |
Secondary captions font size |
logoBase64 |
String? |
null |
Custom logo in base64 format |
themeMode |
OctopusThemeMode? |
null |
Theme mode (light/dark) |
OctopusThemeMode Enum #
The OctopusThemeMode enum allows you to control the overall appearance of the Octopus interface:
enum OctopusThemeMode {
/// Light theme mode - bright interface with dark text
light,
/// Dark theme mode - dark interface with light text
dark,
}
Usage Examples:
// Light theme
final lightTheme = OctopusTheme(
primaryMain: Colors.blue,
themeMode: OctopusThemeMode.light,
);
// Dark theme
final darkTheme = OctopusTheme(
primaryMain: Colors.blue,
themeMode: OctopusThemeMode.dark,
);
// Auto theme (follows system preference)
final autoTheme = OctopusTheme(
primaryMain: Colors.blue,
// themeMode: null (default) - follows system theme
);
Theme Mode Behavior:
OctopusThemeMode.light: Forces light theme regardless of system settingOctopusThemeMode.dark: Forces dark theme regardless of system settingnull(default): Follows the system theme preference automatically
Available Methods #
| Method | Description | Required Parameters |
|---|---|---|
initializeOctopusSDK() |
Initialize SDK in SSO mode | apiKey |
connectUser() |
Connect user (SSO mode) | userId, token |
disconnectUser() |
Disconnect current user | None |
Available Widgets #
| Widget | Type | Description |
|---|---|---|
OctopusView |
StatelessWidget | Simple embedded Octopus UI widget with callbacks |
Converting Image to Base64 #
To use a custom logo, you need to convert your image to base64 format. Here's how to do it:
From Assets
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
String? logoBase64;
@override
void initState() {
super.initState();
loadLogo();
}
Future<void> loadLogo() async {
final bytes = await rootBundle.load('assets/logo.png');
setState(() {
logoBase64 = base64Encode(bytes.buffer.asUint8List());
});
}
@override
Widget build(BuildContext context) {
if (logoBase64 == null) {
return CircularProgressIndicator();
}
final theme = OctopusTheme(
logoBase64: logoBase64,
primaryMain: Colors.blue,
// ... other theme properties
);
return OctopusView(theme: theme);
}
}
From Network Image
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
Future<String> convertNetworkImageToBase64(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final bytes = response.bodyBytes;
return base64Encode(bytes);
} else {
throw Exception('Failed to load image');
}
}
// Usage
final logoBase64 = await convertNetworkImageToBase64('https://example.com/logo.png');
final theme = OctopusTheme(logoBase64: logoBase64);
From File
import 'dart:convert';
import 'dart:io';
Future<String> convertFileToBase64(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
return base64Encode(bytes);
}
// Usage
final logoBase64 = await convertFileToBase64('/path/to/logo.png');
final theme = OctopusTheme(logoBase64: logoBase64);
Troubleshooting #
Common Issues #
iOS Build Issues
- Ensure iOS deployment target is 14.0 or higher
- Check that
ENABLE_USER_SCRIPT_SANDBOXINGis set toNO - Run
cd ios && pod install --repo-update
Android Build Issues
- Ensure
minSdkVersionis 21 or higher - Check that internet permission is added
- Clean and rebuild:
flutter clean && flutter pub get
Authentication Issues
- Verify your API key is correct
- Check that JWT tokens are properly formatted
Network Issues
- Check internet connectivity
- Verify API endpoints are reachable
- Review network security settings (iOS ATS)
OctopusView Widgets #
The OctopusView widgets provide a simplified way to integrate the native Octopus Community UI into your Flutter application.
OctopusView (StatelessWidget) #
The simplest widget to display the embedded Octopus UI. This widget is lightweight and suitable for most use cases:
Key Features:
- Lightweight and fast
- No state management overhead
- Suitable for simple integrations
- Minimal memory footprint
import 'package:octopus_sdk_flutter/octopus_sdk_flutter.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return OctopusView(
navBarTitle: 'My Community',
showBackButton: false,
);
}
}
OctopusViewStateful (StatefulWidget) #
For more advanced state management with error handling and loading states. This widget provides additional callbacks and state management:
Key Features:
- Built-in error handling with retry functionality
- Loading state management
- Callback support for view lifecycle events
- Automatic error recovery
- Custom loading and error widgets
The errorWidget parameter allows you to create fully customized error displays:
- Interactive elements: Buttons, links, custom actions
- Branding integration: Your app's colors, logos, fonts
- Multiple actions: Retry, help, contact support, etc.
- Responsive design: Adapt to different screen sizes
- Accessibility: Include proper labels and descriptions
Available Parameters #
OctopusView
| Parameter | Type | Default | Description |
|---|---|---|---|
navBarTitle |
String? |
null |
Title displayed in the navigation bar |
navBarPrimaryColor |
bool |
false |
Uses primary color for navigation bar background |
showBackButton |
bool |
true |
Shows back button (Android only) |
theme |
OctopusTheme? |
null |
Custom theme for the interface |
showLoadingIndicator |
bool |
true |
Shows loading indicator |
loadingWidget |
Widget? |
null |
Custom widget for loading state |
errorWidget |
Widget? |
null |
Custom widget for error state |
showError |
bool |
true |
Whether to show error states |
enabled |
bool |
true |
Enables/disables the widget |
OctopusViewStateful
All parameters from OctopusView plus:
| Parameter | Type | Default | Description |
|---|---|---|---|
onBackPressed |
VoidCallback? |
null |
Callback called when user presses back |
onViewReady |
VoidCallback? |
null |
Callback called when view is ready |
onError |
Function(String error)? |
null |
Callback called on error |
showError |
bool |
true |
Whether to show error states |
Usage Examples #
Example with Custom Theme
final customTheme = OctopusTheme(
primaryMain: Colors.blue,
primaryLowContrast: Colors.blue.withOpacity(0.2),
primaryHighContrast: Colors.white,
onPrimary: Colors.white,
fontSizeTitle1: 24,
fontSizeBody1: 16,
logoBase64: myLogoBase64,
);
OctopusView(
navBarTitle: 'Community',
theme: customTheme,
navBarPrimaryColor: true,
showBackButton: false,
)
Example with State Management
OctopusViewStateful(
navBarTitle: 'Community',
showBackButton: true,
onBackPressed: () {
Navigator.of(context).pop();
},
onViewReady: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Community loaded!')),
);
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $error'),
backgroundColor: Colors.red,
),
);
},
loadingWidget: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading your community...'),
],
),
),
)
Example with Error Control
// Hide error display but still handle errors via callback
OctopusViewStateful(
navBarTitle: 'Community',
showError: false, // Don't show error widget
onError: (error) {
// Handle error silently or show custom notification
print('Error occurred: $error');
},
)
// Or show custom error widget
OctopusViewStateful(
navBarTitle: 'Community',
showError: true,
errorWidget: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.wifi_off, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No connection'),
SizedBox(height: 8),
Text('Please check your internet connection'),
],
),
),
)
Advanced Error Widget Customization
You can create highly customized error widgets with interactive elements:
OctopusViewStateful(
navBarTitle: 'Community',
errorWidget: Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Colors.orange,
),
SizedBox(height: 16),
Text(
'Something went wrong',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'We couldn\'t load the community right now.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: () {
// Custom retry logic
print('Retry pressed');
},
icon: Icon(Icons.refresh),
label: Text('Retry'),
),
TextButton.icon(
onPressed: () {
// Custom help logic
print('Help pressed');
},
icon: Icon(Icons.help_outline),
label: Text('Help'),
),
],
),
],
),
),
)
Branded Error Widget
Create error widgets that match your app's branding:
OctopusViewStateful(
navBarTitle: 'Community',
theme: OctopusTheme(
primaryMain: Colors.blue,
// ... other theme properties
),
errorWidget: Container(
padding: EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Your app logo
Image.asset(
'assets/logo.png',
height: 64,
width: 64,
),
SizedBox(height: 24),
Text(
'Connection Issue',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue, // Use your brand color
),
),
SizedBox(height: 12),
Text(
'Please check your internet connection and try again.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// Custom retry action
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // Your brand color
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
child: Text('Try Again'),
),
],
),
),
)
Example in TabBar
TabBarView(
children: [
// Other content...
OctopusView(
navBarTitle: 'Community',
showBackButton: false,
),
],
)
Example in BottomNavigationBar
IndexedStack(
index: _currentIndex,
children: [
// Other content...
OctopusView(
navBarTitle: 'Community',
showBackButton: false,
),
],
)
Error Handling #
The OctopusViewStateful widget automatically handles errors and displays a default error widget with a "Retry" button. You can customize this behavior:
OctopusViewStateful(
errorWidget: Center(
child: Column(
children: [
Icon(Icons.wifi_off, size: 64),
Text('No connection'),
ElevatedButton(
onPressed: () => setState(() {}),
child: Text('Retry'),
),
],
),
),
)
Important Notes #
- The widget works on Android and iOS
- On iOS,
navBarTitlecompletely replaces the logo - On Android,
navBarTitlecan be displayed alongside the logo - The
showBackButtonparameter only affects Android - Make sure the SDK is initialized before using the widget
Examples #
Check the /example folder for complete implementation examples:
- Basic Integration: Simple setup with default theme
- Custom Theme: Advanced theming with custom colors and fonts
- SSO Authentication: Complete SSO flow with JWT token integration
- Embedded Widgets: Usage of
OctopusViewwidgets with callbacks
Support #
For issues, questions, or contributions:
- 📧 Email: support@octopuscommunity.com
- 📖 Official Documentation: https://doc.octopuscommunity.com/
- 📖 Flutter SDK Documentation: This README
License #
This project is licensed under the Octopus Community Mobile SDK License Agreement - see the LICENSE file for details.