ketch_flutter 0.3.0 copy "ketch_flutter: ^0.3.0" to clipboard
ketch_flutter: ^0.3.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);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  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,
            ),
          ),
        ],
      ),
    );
  }
}