Flutter Integration
This guide covers how to integrate the Hubble SDK into a Flutter application using webview_flutter.
1. Setup the WebView
Add the required dependencies to your pubspec.yaml:
dependencies:
webview_flutter: ^4.0.0
url_launcher: ^6.0.0
Run flutter pub get after adding the dependencies.
2. Initialization
Build the SDK URL with your credentials:
| Parameter | Required | Description |
|---|---|---|
clientId | Yes | Client ID provided by the Hubble team |
appSecret | Yes | App secret provided by the Hubble team |
token | Conditional | SSO token for the current user. Required unless lazy login is enabled. |
appVersion | No | App version string. Defaults to "10000". |
deviceId | No | Device identifier for analytics. |
final params = {
'clientId': 'id_given_by_hubble',
'appSecret': 'secret_given_by_hubble',
'token': 'your_sso_token',
};
final baseUrl = 'https://sdk.dev.myhubble.money/';
// prod: https://sdk.myhubble.money/
final sourceUrl = '$baseUrl?clientId=${params['clientId']}&appSecret=${params['appSecret']}&token=${params['token']}';
3. Load the WebView
Initialize the WebViewController and load the URL:
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..setNavigationDelegate(NavigationDelegate(/* ... */))
..addJavaScriptChannel("FlutterHost",
onMessageReceived: (message) => _handleEvent(message.message),
)
..loadRequest(Uri.parse(sourceUrl));
The SDK sends messages to a channel named "FlutterHost". If you use a different name, events will not be received.
4. Handling Navigation
Use the NavigationDelegate to control URL handling:
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith(baseUrl)
|| request.url.startsWith('https://api.razorpay.com')) {
return NavigationDecision.navigate;
} else {
launchUrl(Uri.parse(request.url), mode: LaunchMode.externalApplication);
return NavigationDecision.prevent;
}
},
)
Back Navigation
Use PopScope to handle the Android back button:
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (await _controller.canGoBack()) {
await _controller.goBack();
} else {
if (context.mounted) Navigator.of(context).pop();
}
},
child: Scaffold(
body: SafeArea(child: WebViewWidget(controller: _controller)),
),
)
5. Handling Events
The SDK communicates with your application by sending events. There are two types:
Action Events
SDK lifecycle and navigation:
| Action | When It Fires | What You Should Do |
|---|---|---|
app_ready | SDK has finished loading | Show the WebView / iframe. Hide your loading spinner. |
close | User tapped the close or back button in the SDK | Dismiss the WebView / iframe. Navigate the user back. |
error | SDK failed to load (invalid credentials, network error, SSO failure) | Hide the WebView. Show a user-friendly error with a retry option. |
The close action is the only way the SDK tells your application that the user wants to leave. If you do not handle it, the user will be stuck inside the SDK with no way to navigate back. This is one of the most common integration issues.
Analytics Events
User interaction tracking:
{ "type": "analytics", "event": "payment_success", "properties": { "amount": 500 } }
Forward analytics events to your analytics provider (Mixpanel, CleverTap, Amplitude, etc.) to track SDK usage.
For a complete list of events, see the Full Events Reference.
Setting Up the Event Handler
Parse JSON messages from the FlutterHost channel:
void _handleEvent(String jsonString) {
try {
final data = jsonDecode(jsonString) as Map<String, dynamic>;
final type = data['type'] as String?;
if (type == 'action') {
final action = data['action'] as String?;
if (action == 'close') {
Navigator.of(context).pop();
} else if (action == 'app_ready') {
setState(() => _loading = false);
} else if (action == 'error') {
setState(() => _error = true);
}
} else if (type == 'analytics') {
final event = data['event'] as String?;
final properties = data['properties'] as Map<String, dynamic>?;
// Forward to your analytics provider
}
} catch (e) {
print('Error handling event: $e');
}
}
6. Payment Configuration
The Hubble SDK supports UPI and credit/debit card payments. Credit/debit card payments are not enabled by default - contact Hubble support to enable them.
UPI on Flutter
Flutter uses a WebView under the hood, so UPI configuration depends on the target platform:
- iOS: Add the
LSApplicationQueriesSchemesto yourInfo.plist(see the iOS Integration Guide for details). - Android: Add the
<queries>block to yourAndroidManifest.xml(see the Android Integration Guide for details).
In your NavigationDelegate, UPI scheme URLs should be launched externally using url_launcher.
7. Enable Attachments in Contact Support
The SDK's Contact Support flow lets users attach screenshots or short videos to a ticket via a standard HTML <input type="file">. Browsers, iOS WKWebView, Android system WebView, and React Native's react-native-webview all wire this up to the native gallery picker automatically. The official webview_flutter plugin does not — on Android, its default WebChromeClient never fires onShowFileChooser, so tapping "Add screenshot" appears to do nothing.
If you don't apply the setup below, users can still submit support tickets, but they won't be able to attach any media.
7.1 Add dependencies
Add image_picker and pin webview_flutter_android (needed because we import the Android platform interface directly):
dependencies:
webview_flutter: ^4.0.0
webview_flutter_android: ^3.16.6
image_picker: ^1.0.7
Run flutter pub get.
7.2 Wire the file selector on the Android platform controller
After you build the WebViewController (see Section 3), register a file-selector callback on the Android platform. It's a no-op on iOS.
import 'package:image_picker/image_picker.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
// ...after constructing _controller...
final platform = _controller.platform;
if (platform is AndroidWebViewController) {
platform.setOnShowFileSelector(_onShowFileSelector);
}
Future<List<String>> _onShowFileSelector(
FileSelectorParams params,
) async {
final picker = ImagePicker();
final List<XFile> files;
if (params.mode == FileSelectorMode.openMultiple) {
files = await picker.pickMultipleMedia();
} else {
final picked = await picker.pickMedia();
files = picked == null ? const <XFile>[] : <XFile>[picked];
}
return files.map((f) => Uri.file(f.path).toString()).toList();
}
Notes:
pickMultipleMedia()returns both images and videos in a single picker, matching the SDK'saccept="image/*,video/*".- The Contact Support flow caps attachments at 3 files and 20 MB per file — the SDK validates and trims whatever your callback returns, so no host-side enforcement is needed.
pickMedia()/pickMultipleMedia()route through the Android Photo Picker, so noREAD_MEDIA_IMAGES/READ_MEDIA_VIDEOmanifest entries or runtime permission prompts are required. This also keeps you compliant with Google Play's Photo and Video Permissions policy for apps targeting Android 13+ (API 33+).
7.3 iOS: no code changes, just Info.plist strings
iOS WKWebView handles <input type="file"> natively for gallery picking — the code above is Android-only and safely no-ops on iOS.
However, adding image_picker links Photos.framework and AVFoundation into your iOS binary. To pass App Store review, ios/Runner/Info.plist must declare usage descriptions for the frameworks it links (even if you never invoke the picker on iOS):
<key>NSPhotoLibraryUsageDescription</key>
<string>Attach screenshots to your support ticket.</string>
<key>NSCameraUsageDescription</key>
<string>Attach photos to your support ticket.</string>
If your app already declares these for another feature, you don't need to add them again.
7.4 Verify
- Build on a real Android device (test API 33+ and one API ≤ 32 if possible).
- Open the SDK, tap Contact support, pick any sub-issue that shows the "Add screenshots or recordings" section.
- Tap the dashed + tile — the system Photo Picker should open. Select up to 3 files (mix of images and videos).
- Submit and confirm attachments arrive on the ticket.
- Repeat on iOS — the same input opens the WKWebView-native picker; no additional wiring needed.
If the Android picker still doesn't open, check in this order: _controller.platform is not AndroidWebViewController (bump webview_flutter if very old), or image_picker didn't install (flutter pub get).
Best Practices
- Wrap in SafeArea: Always use
SafeAreato avoid content being hidden behind the notch or system UI. - Dispose properly: The
WebViewControlleris automatically disposed when the widget is removed from the tree. - Show a loading state: Use a
Stackto overlay a loading spinner untilapp_readyfires. - Test on both platforms: Flutter WebView behavior can differ between iOS and Android. Test on both physical devices.