Skip to main content

gantry 0.2.0 copy "gantry: ^0.2.0" to clipboard
gantry: ^0.2.0 copied to clipboard

Client SDK for Gantry. Fetch interstitial campaigns, submit leads and load content cards and pages in Flutter apps, with caching and retries built in.

example/lib/main.dart

// Runs every call of the gantry package against a Gantry server.
//
// With a local Gantry (bin/rails db:seed && bin/dev):
//   iOS simulator:     flutter run
//   Android emulator:  flutter run --dart-define=GANTRY_BASE_URL=http://10.0.2.2:3000
//
// Against another server:
//   flutter run --dart-define=GANTRY_BASE_URL=https://app.gantryhq.net \
//               --dart-define=GANTRY_API_KEY=gk_dev_...

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:gantry/gantry.dart';

const _baseUrl = String.fromEnvironment(
  'GANTRY_BASE_URL',
  defaultValue: 'http://localhost:3000',
);
const _apiKey = String.fromEnvironment(
  'GANTRY_API_KEY',
  defaultValue: 'gk_dev_seedseedseedseedseedseedseedseed',
);

void main() =>
    runApp(const MaterialApp(title: 'Gantry example', home: ExampleScreen()));

/// Exercises every call of the SDK against a Gantry server.
class ExampleScreen extends StatefulWidget {
  const ExampleScreen({super.key});

  @override
  State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
  final _customerId = TextEditingController(text: 'cust-1001');
  final _log = <String>[];
  late final Gantry _gantry;

  // Stands in for Firebase Remote Config. A real app passes
  // (key) => FirebaseRemoteConfig.instance.getString(key) instead.
  bool _targetedFlag = false;
  String _frequency = 'everyLaunch';

  List<ContentCard> _cards = const [];
  ContentPage? _page;

  @override
  void initState() {
    super.initState();
    _gantry = Gantry(
      apiKey: _apiKey,
      appVersion: '2.3.1',
      baseUrl: Uri.parse(_baseUrl),
      remoteConfig: _readRemoteConfig,
      // A real app passes FirebaseMessaging.instance.subscribeToTopic.
      subscribeToTopic: (topic) async => _say('Would subscribe to $topic'),
      onLog: (event) => _say(event.toString()),
    );
  }

  @override
  void dispose() {
    _gantry.close();
    _customerId.dispose();
    super.dispose();
  }

  String? _readRemoteConfig(String key) {
    return switch (key) {
      'bo_interstitial_targeted' => '$_targetedFlag',
      'bo_interstitial' => jsonEncode({
        'id': 'coaching-lead',
        'schemaVersion': 1,
        'enabled': true,
        'startAt': '2026-01-01T00:00:00.000Z',
        'endAt': '2030-01-01T00:00:00.000Z',
        'platform': ['all'],
        'minAppVersion': '1.0.0',
        'frequency': {'type': _frequency},
        'audience': {'type': 'all'},
        'priority': 100,
        'content': {
          'title': {
            'tr': 'Sağlık koçunuzla tanışın',
            'en': 'Meet your health coach',
          },
          'description': {
            'tr': 'Sizi arayalım, birlikte planlayalım.',
            'en': 'Let us call you and plan together.',
          },
          'imageUrl': 'https://picsum.photos/seed/gantry/600/400',
          'a11yLabel': {'tr': 'Kampanya görseli', 'en': 'Campaign image'},
          'primaryButtonLabel': {'tr': 'Beni arayın', 'en': 'Call me'},
          'primaryAction': {
            'type': 'lead',
            'trackingEvent': 'coaching_lead_click',
          },
          'secondaryButtonLabel': {'tr': 'Kapat', 'en': 'Close'},
          'secondaryAction': {'type': 'dismiss'},
        },
      }),
      _ => null,
    };
  }

  void _say(String message) {
    if (!mounted) return;
    setState(() => _log.insert(0, message));
  }

  void _identify() {
    try {
      _gantry.identify(_customerId.text.trim());
      _say('Identified as ${_customerId.text.trim()}');
    } on ArgumentError catch (error) {
      _say('Invalid customer id: ${error.message}');
    }
  }

  void _reset() {
    _gantry.reset();
    _say('Signed out');
  }

  Future<void> _showInterstitial() async {
    final interstitial = await _gantry.interstitials.next();
    if (!mounted) return;
    if (interstitial == null) {
      _say('No interstitial to show');
      return;
    }

    await _gantry.interstitials.markShown(interstitial);
    if (!mounted) return;
    final secondaryLabel = interstitial.secondaryButtonLabel;
    final secondaryAction = interstitial.secondaryAction;
    final chosen = await showDialog<GantryAction>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(interstitial.title.resolve('en')),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Image.network(
              interstitial.imageUrl.toString(),
              semanticLabel: interstitial.a11yLabel.resolve('en'),
              errorBuilder: (context, error, stack) => const SizedBox.shrink(),
            ),
            const SizedBox(height: 12),
            Text(interstitial.description.resolve('en')),
            const SizedBox(height: 12),
            Text(
              '${interstitial.id} · ${interstitial.audience.name} · ${interstitial.frequency.name}',
            ),
          ],
        ),
        actions: [
          if (secondaryLabel != null && secondaryAction != null)
            TextButton(
              onPressed: () => Navigator.pop(context, secondaryAction),
              child: Text(secondaryLabel.resolve('en')),
            ),
          FilledButton(
            onPressed: () => Navigator.pop(context, interstitial.primaryAction),
            child: Text(interstitial.primaryButtonLabel.resolve('en')),
          ),
        ],
      ),
    );
    if (chosen != null) await _run(chosen, interstitial);
  }

  Future<void> _run(GantryAction action, Interstitial interstitial) async {
    switch (action) {
      case GantryRedirectAction(:final deeplink):
        _say('Would open the deeplink $deeplink');
      case GantryWebPageAction(:final url):
        _say('Would open the web page $url');
      case GantryDismissAction():
        _say('Dismissed');
      case GantryLeadAction():
        try {
          await _gantry.leads.submit(campaignId: interstitial.id);
          _say('Lead sent for ${interstitial.id}');
        } on GantryNotIdentifiedException {
          _say('Sign in before sending a lead');
        } on GantryException catch (error) {
          _say('Lead failed: $error');
        }
    }
  }

  Future<void> _loadCards() async {
    try {
      final cards = await _gantry.contents.list();
      if (!mounted) return;
      setState(() => _cards = cards);
      _say('Loaded ${cards.length} card(s)');
    } on GantryException catch (error) {
      _say('Cards failed: $error');
    }
  }

  Future<void> _syncPushBuckets() => _gantry.push.sync();

  Future<void> _loadPage() async {
    try {
      final page = await _gantry.pages.get('kvkk');
      if (!mounted) return;
      setState(() => _page = page);
      _say(
        page == null
            ? 'The page kvkk is not published'
            : 'Loaded the page kvkk',
      );
    } on GantryException catch (error) {
      _say('Page failed: $error');
    }
  }

  @override
  Widget build(BuildContext context) {
    final page = _page;
    return Scaffold(
      appBar: AppBar(title: const Text('Gantry example')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            'Server: $_baseUrl',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _customerId,
            decoration: const InputDecoration(
              labelText: 'Customer id',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              FilledButton(onPressed: _identify, child: const Text('Sign in')),
              OutlinedButton(onPressed: _reset, child: const Text('Sign out')),
              Text(_gantry.isIdentified ? 'Signed in' : 'Signed out'),
            ],
          ),
          const Divider(height: 32),
          SwitchListTile(
            contentPadding: EdgeInsets.zero,
            title: const Text('bo_interstitial_targeted'),
            subtitle: const Text(
              'Remote Config flag: ask the API for a targeted campaign',
            ),
            value: _targetedFlag,
            onChanged: (value) => setState(() => _targetedFlag = value),
          ),
          Row(
            children: [
              const Text('Frequency of the general campaign'),
              const SizedBox(width: 12),
              DropdownButton<String>(
                value: _frequency,
                items: const [
                  DropdownMenuItem(
                    value: 'everyLaunch',
                    child: Text('everyLaunch'),
                  ),
                  DropdownMenuItem(value: 'daily', child: Text('daily')),
                  DropdownMenuItem(value: 'once', child: Text('once')),
                ],
                onChanged: (value) =>
                    setState(() => _frequency = value ?? _frequency),
              ),
            ],
          ),
          FilledButton(
            onPressed: _showInterstitial,
            child: const Text('Show the next interstitial'),
          ),
          const Divider(height: 32),
          Wrap(
            spacing: 8,
            children: [
              FilledButton.tonal(
                onPressed: _loadCards,
                child: const Text('Load cards'),
              ),
              FilledButton.tonal(
                onPressed: _loadPage,
                child: const Text('Load page kvkk'),
              ),
              FilledButton.tonal(
                onPressed: _syncPushBuckets,
                child: const Text('Sync push buckets'),
              ),
            ],
          ),
          for (final card in _cards)
            ListTile(
              contentPadding: EdgeInsets.zero,
              title: Text(card.title.resolve('en')),
              subtitle: Text(
                '${card.category} · ${card.summary?.resolve('en') ?? 'no summary'}',
              ),
            ),
          if (page != null) ...[
            const SizedBox(height: 8),
            Text(
              page.title.resolve('en'),
              style: Theme.of(context).textTheme.titleMedium,
            ),
            Text(page.body.markdown.resolve('en')),
          ],
          const Divider(height: 32),
          Text('Log', style: Theme.of(context).textTheme.titleMedium),
          for (final line in _log)
            Text(line, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}
1
likes
160
points
204
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Client SDK for Gantry. Fetch interstitial campaigns, submit leads and load content cards and pages in Flutter apps, with caching and retries built in.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, meta, shared_preferences

More

Packages that depend on gantry