Skip to main content

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:

ParameterRequiredDescription
clientIdYesClient ID provided by the Hubble team
appSecretYesApp secret provided by the Hubble team
tokenConditionalSSO token for the current user. Required unless lazy login is enabled.
appVersionNoApp version string. Defaults to "10000".
deviceIdNoDevice 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));
JavaScript Channel Name Must Be "FlutterHost"

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:

ActionWhen It FiresWhat You Should Do
app_readySDK has finished loadingShow the WebView / iframe. Hide your loading spinner.
closeUser tapped the close or back button in the SDKDismiss the WebView / iframe. Navigate the user back.
errorSDK failed to load (invalid credentials, network error, SSO failure)Hide the WebView. Show a user-friendly error with a retry option.
Always Handle the close Event

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:

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's accept="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 no READ_MEDIA_IMAGES / READ_MEDIA_VIDEO manifest 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

  1. Build on a real Android device (test API 33+ and one API ≤ 32 if possible).
  2. Open the SDK, tap Contact support, pick any sub-issue that shows the "Add screenshots or recordings" section.
  3. Tap the dashed + tile — the system Photo Picker should open. Select up to 3 files (mix of images and videos).
  4. Submit and confirm attachments arrive on the ticket.
  5. 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 SafeArea to avoid content being hidden behind the notch or system UI.
  • Dispose properly: The WebViewController is automatically disposed when the widget is removed from the tree.
  • Show a loading state: Use a Stack to overlay a loading spinner until app_ready fires.
  • Test on both platforms: Flutter WebView behavior can differ between iOS and Android. Test on both physical devices.