Skip to main content

novvy_ads 1.2.1 copy "novvy_ads: ^1.2.1" to clipboard
novvy_ads: ^1.2.1 copied to clipboard

Flutter plugin for Novvy Ads SDK.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';

import 'package:novvy_ads/novvy_ads.dart';

import 'story_demo_page.dart';

void main() {
  runApp(const NovvyAdsExampleApp());
}

class NovvyAdsExampleApp extends StatelessWidget {
  const NovvyAdsExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Novvy Ads Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF6C5CE7),
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: const Color(0xFF6C5CE7),
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _initialized = false;
  bool _initializing = false;

  final _appIdController = TextEditingController(text: 'your_app_id');
  final _endpointController = TextEditingController(text: 'your_end_point');
  final _apiKeyController = TextEditingController(text: 'your_api_key');
  final _interstitialAdUnitController =
      TextEditingController(text: 'your_ad_unit_id');
  final _rewardedAdUnitController =
      TextEditingController(text: 'your_ad_unit_id');
  final _midAdUnitController = TextEditingController(text: 'your_ad_unit_id');
  final _endAdUnitController = TextEditingController(text: 'your_ad_unit_id');
  final _pauseAdUnitController = TextEditingController(text: 'your_ad_unit_id');
  final _scrollAdUnitController = TextEditingController(text: 'your_ad_unit_id');
  final _insertAdUnitController = TextEditingController(text: 'your_ad_unit_id');
  final _bannerAdUnitController = TextEditingController(text: 'your_ad_unit_id');

  bool _showPause = false;
  _DemoPlayerAdapter _pausePlayer = _DemoPlayerAdapter(playing: false);
  NovvyPauseAd? _pauseAd;

  NovvyScrollAd? _scrollAd;
  bool _scrollReady = false;

  NovvyBannerAd? _bannerAd;
  NovvyBannerSize _bannerSize = NovvyBannerSize.standard;
  int _bannerRotations = 0;

  final _logs = <_LogEntry>[];

  void _addLog(String message, {_LogLevel level = _LogLevel.info}) {
    setState(() {
      _logs.insert(0, _LogEntry(message: message, level: level));
      if (_logs.length > 200) _logs.removeLast();
    });
  }

  Future<void> _initialize() async {
    if (_initializing) return;
    setState(() => _initializing = true);

    _addLog('Initializing Novvy SDK...');
    NovvyAds.initialize(
      appId: _appIdController.text.trim(),
      endpoint: _endpointController.text.trim(),
      apiKey: _apiKeyController.text.trim(),
      onComplete: (success) {
        if (!mounted) return;
        if (success) {
          _addLog('SDK initialized successfully', level: _LogLevel.success);
          setState(() {
            _initialized = true;
            _initializing = false;
          });
        } else {
          _addLog('SDK initialization failed', level: _LogLevel.error);
          setState(() => _initializing = false);
        }
      },
    );
  }

  void _setContentContext() {
    NovvyAds.setContentContext(
      NovvyContentContext(
        seriesName: 'your_series_name',
        episodeNumber: 1,
        url: 'your_content_url',
      ),
    );
    _addLog('Content context updated', level: _LogLevel.success);
  }

  // ── Full-screen ads (Interstitial / Mid / End) ──

  void _showFullScreen(String label, NovvyAd ad) {
    _addLog('Requesting $label: ${ad.adUnitId}');
    ad.onAdFailedToLoad = (e) {
      _addLog('$label failed [${ad.adUnitId}]: $e', level: _LogLevel.error);
      ad.destroy();
    };
    if (ad is NovvyInterstitialAd) {
      ad.onAdLoaded = ad.show;
      ad.onAdDismissed =
          () => _addLog('$label dismissed', level: _LogLevel.success);
    } else if (ad is NovvyMidAd) {
      ad.onAdLoaded = ad.show;
      ad.onAdDismissed =
          () => _addLog('$label dismissed', level: _LogLevel.success);
    } else if (ad is NovvyEndAd) {
      ad.onAdLoaded = ad.show;
      ad.onAdDismissed =
          () => _addLog('$label dismissed', level: _LogLevel.success);
    }
    ad.load();
  }

  void _showInterstitial() => _showFullScreen(
      'Interstitial', NovvyInterstitialAd(adUnitId: _interstitialAdUnitController.text.trim()));

  void _showMid() =>
      _showFullScreen('Mid', NovvyMidAd(adUnitId: _midAdUnitController.text.trim()));

  void _showEnd() =>
      _showFullScreen('End', NovvyEndAd(adUnitId: _endAdUnitController.text.trim()));

  // ── Rewarded ──

  void _showRewarded() {
    final ad = NovvyRewardedAd(adUnitId: _rewardedAdUnitController.text.trim());
    _addLog('Requesting rewarded: ${ad.adUnitId}');
    var earned = false;
    ad.onAdLoaded = ad.show;
    ad.onUserEarnedReward = () => earned = true;
    ad.onAdDismissed = () {
      if (earned) {
        _addLog('Rewarded dismissed: reward EARNED — grant it',
            level: _LogLevel.success);
      } else {
        _addLog('Rewarded dismissed: no reward (skipped early)',
            level: _LogLevel.warning);
      }
      ad.destroy();
    };
    ad.onAdFailedToLoad = (e) {
      _addLog('Rewarded failed: $e', level: _LogLevel.error);
      ad.destroy();
    };
    ad.load();
  }

  // ── Pause card ──

  void _togglePause() {
    setState(() {
      _showPause = !_showPause;
      if (_showPause) {
        // Fresh paused player for the new pause-card session.
        _pausePlayer = _DemoPlayerAdapter(playing: false);
        final ad = NovvyPauseAd(adUnitId: _pauseAdUnitController.text.trim());
        ad.onAdShowed = () => _addLog('Pause card shown', level: _LogLevel.success);
        ad.onAdHidden = () => _addLog('Pause card hidden (resumed)');
        ad.onAdFailedToLoad =
            (e) => _addLog('Pause failed: $e', level: _LogLevel.error);
        _pauseAd = ad;
      } else {
        _pauseAd?.destroy();
        _pauseAd = null;
      }
    });
    _addLog(_showPause ? 'Pause card added' : 'Pause card removed');
  }

  // ── Scroll (inline feed) ──

  void _loadScroll() {
    _scrollAd?.destroy();
    final ad = NovvyScrollAd(adUnitId: _scrollAdUnitController.text.trim());
    _addLog('Loading scroll ad: ${ad.adUnitId}');
    ad.onAdLoaded = () {
      _addLog('Scroll ad ready', level: _LogLevel.success);
      setState(() => _scrollReady = true);
      // Simulate the item being visible in the viewport.
      ad.setPlaying(true);
    };
    ad.onAdFailedToLoad = (e) {
      _addLog('Scroll ad failed: $e', level: _LogLevel.error);
      ad.destroy();
    };
    ad.onAdHidden = () => _addLog('Scroll ad hidden');
    ad.onAdPlaybackTick =
        (remaining) => _addLog('Scroll tick: ${remaining}s remaining');
    setState(() {
      _scrollReady = false;
      _scrollAd = ad;
    });
    ad.load();
  }

  void _disposeScroll() {
    _scrollAd?.destroy();
    setState(() {
      _scrollAd = null;
      _scrollReady = false;
    });
    _addLog('Scroll ad disposed');
  }

  // ── Banner (persistent rotating slot) ──

  /// Mounting [NovvyBannerAdView] is what issues the request, so there is no
  /// "load" button here — building the ad object and letting the view appear
  /// is the whole flow.
  void _toggleBanner() {
    if (_bannerAd != null) {
      _removeBanner();
      return;
    }
    final ad = NovvyBannerAd(
      adUnitId: _bannerAdUnitController.text.trim(),
      size: _bannerSize,
      // The floor the SDK clamps to; keeps the demo rotating often enough to
      // watch the per-rotation callbacks arrive.
      refreshIntervalSeconds: 30,
    );
    ad.onAdLoaded = () => _addLog('Banner creative loaded');
    ad.onAdShowed = () {
      // Fires once per rotation, not once per ad object.
      setState(() => _bannerRotations++);
      _addLog('Banner shown (rotation $_bannerRotations)',
          level: _LogLevel.success);
    };
    ad.onAdClicked = () =>
        _addLog('Banner clicked — SDK already opened the destination');
    ad.onAdHidden = () => _addLog('Banner slot emptied');
    // Not terminal: the SDK retries with backoff and any creative already on
    // screen stays up, so the slot is deliberately left mounted.
    ad.onAdFailedToLoad = (e) =>
        _addLog('Banner refresh failed (will retry): $e',
            level: _LogLevel.warning);
    setState(() {
      _bannerRotations = 0;
      _bannerAd = ad;
    });
    _addLog('Banner slot mounted: ${_bannerSize.name}');
  }

  void _removeBanner() {
    _bannerAd?.destroy();
    setState(() {
      _bannerAd = null;
      _bannerRotations = 0;
    });
    _addLog('Banner slot removed');
  }

  /// The tier is fixed for the life of an ad object (the native banner takes it
  /// in its constructor), so switching size rebuilds the slot.
  void _setBannerSize(NovvyBannerSize size) {
    if (size == _bannerSize) return;
    final wasMounted = _bannerAd != null;
    _removeBanner();
    setState(() => _bannerSize = size);
    if (wasMounted) _toggleBanner();
  }

  @override
  void dispose() {
    _appIdController.dispose();
    _endpointController.dispose();
    _apiKeyController.dispose();
    _interstitialAdUnitController.dispose();
    _rewardedAdUnitController.dispose();
    _midAdUnitController.dispose();
    _endAdUnitController.dispose();
    _pauseAdUnitController.dispose();
    _scrollAdUnitController.dispose();
    _insertAdUnitController.dispose();
    _bannerAdUnitController.dispose();
    _pauseAd?.destroy();
    _scrollAd?.destroy();
    _bannerAd?.destroy();
    super.dispose();
  }

  /// STORY lives inside the host's own player view, so the demo for it is a
  /// separate full-page player feed rather than a card on this page.
  void _openStoryDemo() {
    final adUnitId = _insertAdUnitController.text.trim();
    _addLog('Opening STORY player feed: $adUnitId');
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => StoryDemoPage(adUnitId: adUnitId),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Novvy Ads Demo'),
        centerTitle: true,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Center(
            child: Chip(
              avatar: Icon(
                Platform.isAndroid ? Icons.android : Icons.apple,
                size: 18,
              ),
              label: Text(Platform.isAndroid ? 'Android' : 'iOS'),
            ),
          ),
          const SizedBox(height: 16),

          // Init config
          _SectionCard(
            title: 'SDK Configuration',
            icon: Icons.settings,
            children: [
              _ConfigField(
                label: 'App ID',
                controller: _appIdController,
                enabled: !_initialized,
              ),
              _ConfigField(
                label: 'Endpoint',
                controller: _endpointController,
                enabled: !_initialized,
              ),
              _ConfigField(
                label: 'API Key',
                controller: _apiKeyController,
                enabled: !_initialized,
                obscure: true,
              ),
              const SizedBox(height: 12),
              FilledButton.icon(
                onPressed: _initialized || _initializing ? null : _initialize,
                icon: _initializing
                    ? SizedBox.square(
                        dimension: 18,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          color: colorScheme.onPrimary,
                        ),
                      )
                    : Icon(_initialized ? Icons.check : Icons.rocket_launch),
                label: Text(
                  _initialized
                      ? 'Initialized'
                      : _initializing
                          ? 'Initializing...'
                          : 'Initialize SDK',
                ),
              ),
              const SizedBox(height: 8),
              OutlinedButton.icon(
                onPressed: _initialized ? _setContentContext : null,
                icon: const Icon(Icons.tune),
                label: const Text('Set Content Context'),
              ),
            ],
          ),
          const SizedBox(height: 16),

          // Full-screen ads
          _SectionCard(
            title: 'Full-screen Ads',
            icon: Icons.fullscreen,
            children: [
              _ConfigField(
                label: 'Interstitial Ad Unit ID',
                controller: _interstitialAdUnitController,
              ),
              const SizedBox(height: 4),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _showInterstitial : null,
                icon: const Icon(Icons.fullscreen),
                label: const Text('Show Interstitial'),
              ),
              const SizedBox(height: 16),
              _ConfigField(
                label: 'Rewarded Ad Unit ID',
                controller: _rewardedAdUnitController,
              ),
              const SizedBox(height: 4),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _showRewarded : null,
                icon: const Icon(Icons.card_giftcard),
                label: const Text('Show Rewarded'),
              ),
              const SizedBox(height: 16),
              _ConfigField(
                label: 'Mid-roll Ad Unit ID',
                controller: _midAdUnitController,
              ),
              const SizedBox(height: 4),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _showMid : null,
                icon: const Icon(Icons.skip_next),
                label: const Text('Show Mid-roll'),
              ),
              const SizedBox(height: 16),
              _ConfigField(
                label: 'End-roll Ad Unit ID',
                controller: _endAdUnitController,
              ),
              const SizedBox(height: 4),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _showEnd : null,
                icon: const Icon(Icons.stop_circle_outlined),
                label: const Text('Show End-roll'),
              ),
            ],
          ),
          const SizedBox(height: 16),

          // Pause card
          _SectionCard(
            title: 'Pause Card',
            icon: Icons.pause_circle_outline,
            children: [
              _ConfigField(
                label: 'Pause Ad Unit ID',
                controller: _pauseAdUnitController,
              ),
              const SizedBox(height: 8),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _togglePause : null,
                icon: Icon(_showPause ? Icons.visibility_off : Icons.visibility),
                label: Text(_showPause ? 'Remove Pause Card' : 'Show Pause Card'),
              ),
              const SizedBox(height: 12),
              // Simulated player area — pause card sits inside it.
              Container(
                width: 280,
                height: 200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  border: Border.all(color: colorScheme.outlineVariant),
                  borderRadius: BorderRadius.circular(8),
                ),
                clipBehavior: Clip.antiAlias,
                child: (_showPause && _pauseAd != null)
                    ? NovvyPauseAdView(ad: _pauseAd!, player: _pausePlayer)
                    : Text('Player (paused)',
                        style: TextStyle(color: colorScheme.onSurfaceVariant)),
              ),
            ],
          ),
          const SizedBox(height: 16),

          // Scroll ad
          _SectionCard(
            title: 'Scroll Ad',
            icon: Icons.view_stream_outlined,
            children: [
              _ConfigField(
                label: 'Scroll Ad Unit ID',
                controller: _scrollAdUnitController,
              ),
              const SizedBox(height: 8),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _loadScroll : null,
                icon: const Icon(Icons.article_outlined),
                label: const Text('Load Scroll Ad'),
              ),
              if (_scrollReady && _scrollAd != null) ...[
                const SizedBox(height: 12),
                SizedBox(
                  height: 200,
                  child: NovvyScrollAdView(ad: _scrollAd!),
                ),
                const SizedBox(height: 8),
                OutlinedButton.icon(
                  onPressed: _disposeScroll,
                  icon: const Icon(Icons.close),
                  label: const Text('Dispose Scroll Ad'),
                ),
              ],
            ],
          ),
          const SizedBox(height: 16),

          // Banner — a persistent slot that rotates creatives on a timer.
          _SectionCard(
            title: 'Banner Ad',
            icon: Icons.crop_16_9_outlined,
            children: [
              _ConfigField(
                label: 'Banner Ad Unit ID',
                controller: _bannerAdUnitController,
              ),
              const SizedBox(height: 8),
              // The tiers have mutually distinct aspect ratios — the server
              // matches a creative to the slot by ratio, so the wrong tier
              // gets no fill rather than a stretched ad.
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: NovvyBannerSize.values.map((size) {
                  final px = NovvyBannerAd.sizesInLogicalPixels[size]!;
                  return ChoiceChip(
                    label: Text(
                      '${size.name} '
                      '(${px.width.toInt()}×${px.height.toInt()})',
                    ),
                    selected: _bannerSize == size,
                    onSelected:
                        _initialized ? (_) => _setBannerSize(size) : null,
                  );
                }).toList(),
              ),
              const SizedBox(height: 8),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _toggleBanner : null,
                icon: Icon(_bannerAd != null
                    ? Icons.visibility_off
                    : Icons.visibility),
                label: Text(_bannerAd != null
                    ? 'Remove Banner'
                    : 'Show Banner (30s rotation)'),
              ),
              if (_bannerAd != null) ...[
                const SizedBox(height: 12),
                // The leaderboard tier is 728dp wide — wider than any phone —
                // so the demo scrolls it rather than letting it overflow. A
                // real host would use FittedBox or pick a narrower tier.
                SingleChildScrollView(
                  scrollDirection: Axis.horizontal,
                  child: NovvyBannerAdView(ad: _bannerAd!),
                ),
                const SizedBox(height: 8),
                Text(
                  'Rotations shown: $_bannerRotations',
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ],
          ),
          const SizedBox(height: 16),

          // Insert (STORY) ad — opens a full-page player feed, since this
          // placement lives inside the host's own player view.
          _SectionCard(
            title: 'Insert Ad (STORY)',
            icon: Icons.auto_stories_outlined,
            children: [
              _ConfigField(
                label: 'Insert Ad Unit ID',
                controller: _insertAdUnitController,
              ),
              const SizedBox(height: 8),
              FilledButton.tonalIcon(
                onPressed: _initialized ? _openStoryDemo : null,
                icon: const Icon(Icons.play_circle_outline),
                label: const Text('Open Player Feed'),
              ),
            ],
          ),
          const SizedBox(height: 16),

          // Event log
          _SectionCard(
            title: 'Event Log',
            icon: Icons.receipt_long,
            trailing: _logs.isNotEmpty
                ? TextButton(
                    onPressed: () => setState(_logs.clear),
                    child: const Text('Clear'),
                  )
                : null,
            children: [
              if (_logs.isEmpty)
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 24),
                  child: Center(
                    child: Text(
                      'No events yet.\nInitialize the SDK and trigger an ad.',
                      textAlign: TextAlign.center,
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ),
                )
              else
                ...List.generate(
                  _logs.length.clamp(0, 50),
                  (i) => _LogTile(entry: _logs[i]),
                ),
            ],
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

// ── Helper widgets ──

class _SectionCard extends StatelessWidget {
  final String title;
  final IconData icon;
  final Widget? trailing;
  final List<Widget> children;

  const _SectionCard({
    required this.title,
    required this.icon,
    this.trailing,
    required this.children,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: theme.colorScheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Icon(icon, size: 20, color: theme.colorScheme.primary),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(title, style: theme.textTheme.titleMedium),
                ),
                ?trailing,
              ],
            ),
            const SizedBox(height: 12),
            ...children,
          ],
        ),
      ),
    );
  }
}

class _ConfigField extends StatelessWidget {
  final String label;
  final TextEditingController controller;
  final bool enabled;
  final bool obscure;

  const _ConfigField({
    required this.label,
    required this.controller,
    this.enabled = true,
    this.obscure = false,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: TextField(
        controller: controller,
        enabled: enabled,
        obscureText: obscure,
        style: const TextStyle(fontSize: 13),
        decoration: InputDecoration(
          labelText: label,
          isDense: true,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }
}

class _LogTile extends StatelessWidget {
  final _LogEntry entry;

  const _LogTile({required this.entry});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final (icon, color) = switch (entry.level) {
      _LogLevel.info => (Icons.info_outline, theme.colorScheme.onSurface),
      _LogLevel.success => (Icons.check_circle_outline, Colors.green),
      _LogLevel.warning => (Icons.warning_amber, Colors.orange),
      _LogLevel.error => (Icons.error_outline, Colors.redAccent),
    };

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 3),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 16, color: color),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              entry.message,
              style: theme.textTheme.bodySmall?.copyWith(color: color),
            ),
          ),
        ],
      ),
    );
  }
}

// ── Models ──

enum _LogLevel { info, success, warning, error }

class _LogEntry {
  final String message;
  final _LogLevel level;
  final DateTime timestamp;

  _LogEntry({
    required this.message,
    this.level = _LogLevel.info,
  }) : timestamp = DateTime.now();
}

/// A simple player adapter for demo purposes.
class _DemoPlayerAdapter extends NovvyPlayerAdapter {
  _DemoPlayerAdapter({required bool playing}) : _playing = playing;

  final DateTime _start = DateTime.now();
  final bool _playing;

  @override
  int get currentPositionMs =>
      DateTime.now().difference(_start).inMilliseconds;

  @override
  bool get isPlaying => _playing;
}
0
likes
140
points
563
downloads

Documentation

API reference

Publisher

verified publishernovvy.ai

Weekly Downloads

Flutter plugin for Novvy Ads SDK.

Homepage

License

MIT (license)

Dependencies

crypto, flutter, meta, plugin_platform_interface

More

Packages that depend on novvy_ads

Packages that implement novvy_ads