ketch_flutter 0.0.2 copy "ketch_flutter: ^0.0.2" to clipboard
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),
    );
  }
}
0
likes
0
points
461
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter wrapper around a WebView to load the Ketch Mobile SDK.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, shared_preferences, url_launcher, webview_flutter

More

Packages that depend on ketch_flutter