ketch_flutter 0.0.2
ketch_flutter: ^0.0.2 copied to clipboard
Flutter wrapper around a WebView to load the Ketch Mobile SDK.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:ketch_flutter/ketch_flutter.dart';
void main() => runApp(const MaterialApp(home: HomePage()));
enum ApiRegion { prodUs, prodEu, uat }
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _orgCtrl = TextEditingController(text: 'ketch_samples');
final _propCtrl = TextEditingController(text: 'react_native_sample_app');
final _envCtrl = TextEditingController();
final _langCtrl = TextEditingController(text: 'en');
final _jurCtrl = TextEditingController();
final _regCtrl = TextEditingController();
final List<_IdentityRow> _identities = [
_IdentityRow(
keyCtrl: TextEditingController(text: 'email'),
valCtrl: TextEditingController(text: 'test-123-1@gmail.com'),
),
];
ApiRegion _apiRegion = ApiRegion.prodUs;
bool _tabOverview = true;
bool _tabConsent = true;
bool _tabSubscriptions = true;
bool _tabRights = true;
/// Use the SDK enum instead of a local one
PreferenceTab _initialTab = PreferenceTab.overviewTab;
late final KetchController _controller = KetchController(
organizationCode: _orgCtrl.text,
propertyCode: _propCtrl.text,
languageCode: _langCtrl.text.isEmpty ? null : _langCtrl.text,
identities: _collectIdentities(),
);
late KetchOptions _options = _buildOptions();
// Build options from current UI state
KetchOptions _buildOptions() {
// Map API Region -> KetchDataCenter (+ optional env default for UAT)
late final KetchDataCenter dataCenter;
String? envName = _envCtrl.text.trim().isEmpty ? null : _envCtrl.text.trim();
switch (_apiRegion) {
case ApiRegion.prodUs:
dataCenter = KetchDataCenter.us;
break;
case ApiRegion.prodEu:
dataCenter = KetchDataCenter.eu;
break;
case ApiRegion.uat:
dataCenter = KetchDataCenter.uat; // UAT mapping
envName ??= 'uat';
break;
}
// Modern preference options: tab + visibility flags
final pref = PreferenceExperienceOptions(
tab: _initialTab,
showOverviewTab: _tabOverview,
showConsentsTab: _tabConsent,
showSubscriptionsTab: _tabSubscriptions,
showRightsTab: _tabRights,
);
return KetchOptions(
organizationCode: _orgCtrl.text.trim(),
propertyCode: _propCtrl.text.trim(),
identities: _collectIdentities(),
languageCode: _langCtrl.text.trim().isEmpty ? null : _langCtrl.text.trim(),
regionCode: _regCtrl.text.trim().isEmpty ? null : _regCtrl.text.trim(),
jurisdictionCode: _jurCtrl.text.trim().isEmpty ? null : _jurCtrl.text.trim(),
environmentName: envName,
dataCenter: dataCenter,
preferenceExperienceOptions: pref,
onEnvironmentUpdated: (env) => debugPrint('env: $env'),
onRegionUpdated: (r) => debugPrint('region: $r'),
onJurisdictionUpdated: (j) => debugPrint('jurisdiction: $j'),
onIdentitiesUpdated: (ids) => debugPrint('identities: $ids'),
onConsentUpdated: (c) => debugPrint('consent: ${c.toJson()}'),
onPrivacyProtocolUpdated: (p, v) => debugPrint('protocol $p: $v'),
onHasShownExperience: () {
debugPrint('hasShownExperience');
},
onHideExperience: (why) {
debugPrint('hideExperience: $why');
},
onError: (msg) => debugPrint('error: $msg'),
);
}
Map<String, String> _collectIdentities() {
final map = <String, String>{};
for (final row in _identities) {
final k = row.keyCtrl.text.trim();
final v = row.valCtrl.text.trim();
if (k.isNotEmpty && v.isNotEmpty) map[k] = v;
}
return map;
}
void _applyOptions() {
final next = _buildOptions();
_controller.updateParameters(next);
setState(() {
_options = next;
});
}
// ---- UI helpers ----
Widget _tf(String label, TextEditingController c,
{double w = 240, String? hint}) {
return SizedBox(
width: w,
child: Focus(
onFocusChange: (hasFocus) {
if (!hasFocus) _applyOptions();
},
child: TextField(
controller: c,
decoration: InputDecoration(
labelText: label,
hintText: hint,
isDense: true,
border: const OutlineInputBorder(),
),
),
),
);
}
Widget _apiRegionRadios() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('API Region', style: TextStyle(fontWeight: FontWeight.w600)),
Wrap(
spacing: 8,
children: [
RadioChoice<ApiRegion>(
label: 'Prod – US',
groupValue: _apiRegion,
value: ApiRegion.prodUs,
onChanged: (v) {
_apiRegion = v!;
_applyOptions();
},
),
RadioChoice<ApiRegion>(
label: 'Prod – EU',
groupValue: _apiRegion,
value: ApiRegion.prodEu,
onChanged: (v) {
_apiRegion = v!;
_applyOptions();
},
),
RadioChoice<ApiRegion>(
label: 'UAT',
groupValue: _apiRegion,
value: ApiRegion.uat,
onChanged: (v) {
_apiRegion = v!;
_applyOptions();
},
),
],
),
],
);
}
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 _identitiesEditor() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Identities', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
..._identities.asMap().entries.map((e) {
final idx = e.key;
final row = e.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
SizedBox(
width: 160,
child: Focus(
onFocusChange: (hasFocus) {
if (!hasFocus) _applyOptions();
},
child: TextField(
controller: row.keyCtrl,
decoration: const InputDecoration(
labelText: 'Key',
isDense: true,
border: OutlineInputBorder(),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Focus(
onFocusChange: (hasFocus) {
if (!hasFocus) _applyOptions();
},
child: TextField(
controller: row.valCtrl,
decoration: const InputDecoration(
labelText: 'Value',
isDense: true,
border: OutlineInputBorder(),
),
),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Remove',
onPressed: _identities.length == 1
? null
: () {
_identities.removeAt(idx);
_applyOptions();
},
icon: const Icon(Icons.remove_circle_outline),
),
],
),
);
}),
Row(
children: [
OutlinedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Add Identity'),
onPressed: () {
setState(() {
_identities.add(
_IdentityRow(
keyCtrl: TextEditingController(),
valCtrl: TextEditingController(),
),
);
});
},
),
const SizedBox(width: 12),
TextButton(
onPressed: () {
_identities
..clear()
..add(
_IdentityRow(
keyCtrl: TextEditingController(text: 'email'),
valCtrl: TextEditingController(text: 'test-123-1@gmail.com'),
),
);
_applyOptions();
},
child: const Text('Reset'),
),
],
),
],
);
}
Widget _buttons() {
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton(
onPressed: () => _controller.showConsentExperience(),
child: const Text('Show Consent'),
),
OutlinedButton(
onPressed: () => _controller.showPreferenceExperience(),
child: const Text('Show Preferences'),
),
OutlinedButton(
onPressed: () => _controller.load(),
child: const Text('Load'),
),
OutlinedButton(
onPressed: () async {
final consent = await _controller.getConsent();
debugPrint('Example App (Log Consent): $consent');
},
child: const Text('Log Consent'),
),
OutlinedButton(
onPressed: () async {
final protocols = await _controller.getProtocols();
debugPrint('Example App (Log Protocols): $protocols');
},
child: const Text('Log Protocols'),
),
OutlinedButton(
onPressed: () => _controller.setCssOverride(
'#ketch-banner-button-primary { display: none !important; }',
),
child: const Text('Apply CSS'),
),
OutlinedButton(
onPressed: () => _controller.setCssOverride(null),
child: const Text('Clear CSS'),
),
],
);
}
Widget _buildMainContent() {
return SafeArea(
child: ListView(
padding: const EdgeInsets.all(12),
children: [
const Text('Global Options',
style: TextStyle(fontWeight: FontWeight.w600)),
const Text(
'Options that apply to both experiences',
style: TextStyle(fontWeight: FontWeight.w400),
),
const SizedBox(height: 22),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_tf('Organization', _orgCtrl, w: 180, hint: 'e.g. ketch_samples'),
_tf('Property', _propCtrl, w: 180,
hint: 'e.g. react_native_sample_app'),
_tf('Environment', _envCtrl, w: 180, hint: 'e.g. production'),
_tf('Language', _langCtrl, w: 180, hint: 'e.g. en'),
_tf('Jurisdiction', _jurCtrl, w: 180, hint: 'e.g. gdpr'),
_tf('Region', _regCtrl, w: 180, hint: 'e.g. PL'),
],
),
const SizedBox(height: 12),
_identitiesEditor(),
const SizedBox(height: 22),
_apiRegionRadios(),
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),
_buttons(),
],
),
);
}
@override
void dispose() {
_orgCtrl.dispose();
_propCtrl.dispose();
_envCtrl.dispose();
_langCtrl.dispose();
_jurCtrl.dispose();
_regCtrl.dispose();
for (final r in _identities) {
r.keyCtrl.dispose();
r.valCtrl.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final sheetHeight = MediaQuery.of(context).size.height * 0.9;
return Scaffold(
appBar: AppBar(title: const Text('Ketch Flutter Example')),
body: Stack(
children: [
Positioned.fill(child: _buildMainContent()),
Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: const Duration(milliseconds: 0),
curve: Curves.easeOut,
height: sheetHeight,
width: double.infinity,
child: ClipRect(
child: SizedBox.expand(
child: KetchView(
options: _options,
controller: _controller,
),
),
),
),
),
],
),
);
}
}
class _IdentityRow {
final TextEditingController keyCtrl;
final TextEditingController valCtrl;
_IdentityRow({required this.keyCtrl, required this.valCtrl});
}
class RadioChoice<T> extends StatelessWidget {
final String label;
final T value;
final T groupValue;
final ValueChanged<T?> onChanged;
const RadioChoice({
super.key,
required this.label,
required this.value,
required this.groupValue,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final selected = value == groupValue;
return ChoiceChip(
selected: selected,
label: Text(label),
onSelected: (_) => onChanged(value),
);
}
}