ketch_flutter 0.1.0
ketch_flutter: ^0.1.0 copied to clipboard
Flutter wrapper around a WebView to load the Ketch Mobile SDK.
example/lib/main.dart
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:ketch_flutter/ketch_flutter.dart';
import 'dev_url_overrides.dart';
import 'logging.dart';
void main() => runApp(const MaterialApp(home: HomePage()));
/// Single source of truth for SDK configuration. Drives both the
/// [KetchController] / [KetchOptions] and the read-only Info panel.
class SampleConfig {
final String organizationCode;
final String propertyCode;
final String environmentName;
final String languageCode;
final String jurisdictionCode;
final String regionCode;
final Map<String, String> identities;
final KetchDataCenter dataCenter;
const SampleConfig({
required this.organizationCode,
required this.propertyCode,
required this.environmentName,
required this.languageCode,
required this.jurisdictionCode,
required this.regionCode,
required this.identities,
required this.dataCenter,
});
}
const sampleConfig = SampleConfig(
organizationCode: 'ketch_samples',
propertyCode: 'ios',
environmentName: 'production',
languageCode: 'en',
jurisdictionCode: '',
regionCode: '',
identities: {'email': 'test@example.com'},
dataCenter: KetchDataCenter.us,
);
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Live Info state: starts from config, updated by SDK callbacks.
String _jurisdictionText =
sampleConfig.jurisdictionCode.isEmpty ? '—' : sampleConfig.jurisdictionCode;
String _regionText =
sampleConfig.regionCode.isEmpty ? '—' : sampleConfig.regionCode;
bool _tabOverview = true;
bool _tabConsent = true;
bool _tabSubscriptions = true;
bool _tabRights = true;
PreferenceTab _initialTab = PreferenceTab.overviewTab;
late final KetchController _controller = KetchController(
organizationCode: sampleConfig.organizationCode,
propertyCode: sampleConfig.propertyCode,
languageCode: sampleConfig.languageCode,
identities: sampleConfig.identities,
dataCenter: sampleConfig.dataCenter,
environmentName: sampleConfig.environmentName,
);
late KetchOptions _options = _buildOptions();
@override
void initState() {
super.initState();
_controller.setOptions(_options);
}
Future<void> _requestAtt() async {
if (!Platform.isIOS) return;
final prev = await TrackingAuthorization.lastStatusString();
final status = await TrackingAuthorization.requestTrackingAuthorization();
debugPrint(
'[KetchSample] ATT requested: ${formatAttState(status ?? 'unknown', prev)}',
);
// Rebuild options so the WebView picks up the refreshed ketch_att value.
_applyOptions();
}
// Build options from the config constant + current Preference selections.
KetchOptions _buildOptions() {
final pref = PreferenceExperienceOptions(
tab: _initialTab,
showOverviewTab: _tabOverview,
showConsentsTab: _tabConsent,
showSubscriptionsTab: _tabSubscriptions,
showRightsTab: _tabRights,
);
return KetchOptions(
organizationCode: sampleConfig.organizationCode,
propertyCode: sampleConfig.propertyCode,
identities: sampleConfig.identities,
languageCode: sampleConfig.languageCode,
regionCode:
sampleConfig.regionCode.isEmpty ? null : sampleConfig.regionCode,
jurisdictionCode: sampleConfig.jurisdictionCode.isEmpty
? null
: sampleConfig.jurisdictionCode,
environmentName: sampleConfig.environmentName,
dataCenter: sampleConfig.dataCenter,
preferenceExperienceOptions: pref,
webResourceUrlOverrides: DevUrlOverrides.enabled
? (Platform.isAndroid
? DevUrlOverrides.forAndroidEmulator
: DevUrlOverrides.forSimulator)
: null,
onEnvironmentUpdated: (env) =>
debugPrint('[KetchSample] onEnvironmentUpdated: $env'),
onRegionUpdated: (r) {
setState(() => _regionText = r);
debugPrint('[KetchSample] onRegionUpdated: $r');
},
onJurisdictionUpdated: (j) {
setState(() => _jurisdictionText = j);
debugPrint('[KetchSample] onJurisdictionUpdated: $j');
},
onIdentitiesUpdated: (ids) =>
debugPrint('[KetchSample] onIdentitiesUpdated: $ids'),
onConsentUpdated: (c) =>
debugPrint('[KetchSample] onConsentUpdated: ${formatConsent(c)}'),
onPrivacyProtocolUpdated: (p, v) =>
debugPrint('[KetchSample] onPrivacyProtocolUpdated: $p=$v'),
onHasShownExperience: () =>
debugPrint('[KetchSample] onHasShownExperience'),
onHideExperience: (why) =>
debugPrint('[KetchSample] onHideExperience: $why'),
onNativeStoragePut: (key, value) =>
debugPrint('[KetchSample] onNativeStoragePut: $key=$value'),
onError: (msg) => debugPrint('[KetchSample] onError: $msg'),
);
}
void _applyOptions() {
final next = _buildOptions();
setState(() => _options = next);
_controller.updateParameters(next);
}
// ---- UI ----
Widget _infoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 110,
child: Text('$label:', style: const TextStyle(fontSize: 12)),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
],
),
);
}
Widget _infoPanel() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Info',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)),
const SizedBox(height: 8),
_infoRow('Org Code', sampleConfig.organizationCode),
_infoRow('Property', sampleConfig.propertyCode),
_infoRow('Environment', sampleConfig.environmentName),
_infoRow('Language', sampleConfig.languageCode),
_infoRow('Jurisdiction', _jurisdictionText),
_infoRow('Region', _regionText),
],
);
}
Widget _allowedTabsChecks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Allowed Tabs', style: TextStyle(fontWeight: FontWeight.w600)),
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('Overview'),
selected: _tabOverview,
onSelected: (v) {
_tabOverview = v;
_applyOptions();
},
),
FilterChip(
label: const Text('Consent'),
selected: _tabConsent,
onSelected: (v) {
_tabConsent = v;
_applyOptions();
},
),
FilterChip(
label: const Text('Subscriptions'),
selected: _tabSubscriptions,
onSelected: (v) {
_tabSubscriptions = v;
_applyOptions();
},
),
FilterChip(
label: const Text('Rights'),
selected: _tabRights,
onSelected: (v) {
_tabRights = v;
_applyOptions();
},
),
],
),
],
);
}
Widget _initialTabRadios() {
Widget tile(String label, PreferenceTab value) {
return RadioListTile<PreferenceTab>(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(label),
value: value,
groupValue: _initialTab,
onChanged: (v) {
_initialTab = v!;
_applyOptions();
},
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Initial Tab', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
GridView.count(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 12,
childAspectRatio: 3.8,
children: [
tile('Overview', PreferenceTab.overviewTab),
tile('Consent', PreferenceTab.consentsTab),
tile('Subscriptions', PreferenceTab.subscriptionsTab),
tile('Rights', PreferenceTab.rightsTab),
],
),
],
);
}
Widget _actions() {
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton(
onPressed: () => _controller.load(),
child: const Text('Reload'),
),
OutlinedButton(
onPressed: () => _controller.showConsentExperience(),
child: const Text('Consent'),
),
OutlinedButton(
onPressed: () => _controller.showPreferenceExperience(
options: PreferenceExperienceOptions(
tab: _initialTab,
showOverviewTab: _tabOverview,
showConsentsTab: _tabConsent,
showSubscriptionsTab: _tabSubscriptions,
showRightsTab: _tabRights,
),
),
child: const Text('Preferences'),
),
OutlinedButton(
onPressed: () async {
final protocols = await _controller.getProtocols();
debugPrint('[KetchSample] Privacy Strings: $protocols');
},
child: const Text('Privacy Strings'),
),
OutlinedButton(
onPressed: () => _controller.setCssOverride(
'#ketch-banner-button-primary { display: none !important; }',
),
child: const Text('Apply CSS'),
),
if (Platform.isIOS)
OutlinedButton(
onPressed: _requestAtt,
child: const Text('Request ATT'),
),
],
);
}
Widget _buildMainContent() {
return SafeArea(
child: ListView(
padding: const EdgeInsets.all(12),
children: [
_infoPanel(),
const SizedBox(height: 22),
const Text('Preference Options',
style: TextStyle(fontWeight: FontWeight.w600)),
const Text(
'Options that only apply to the preference experience',
style: TextStyle(fontWeight: FontWeight.w400),
),
const SizedBox(height: 12),
_allowedTabsChecks(),
const SizedBox(height: 8),
_initialTabRadios(),
const SizedBox(height: 12),
const Text('Actions',
style: TextStyle(fontWeight: FontWeight.w600)),
const Text(
'Trigger some SDK functionality',
style: TextStyle(fontWeight: FontWeight.w400),
),
const SizedBox(height: 12),
_actions(),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Ketch Flutter Example')),
body: Stack(
children: [
Positioned.fill(child: _buildMainContent()),
// Full-screen overlay (RN parity) so preference modals can scroll.
Positioned.fill(
child: KetchView(
options: _options,
controller: _controller,
),
),
],
),
);
}
}