promo_carousel 1.2.0 copy "promo_carousel: ^1.2.0" to clipboard
promo_carousel: ^1.2.0 copied to clipboard

A flexible, customizable promotional carousel package for Flutter.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:promo_carousel/promo_carousel.dart';

import 'l10n/app_localizations.dart';
import 'video_examples_page.dart';

void main() {
  // Enable debug mode for testing
  PromoCarousel.debugMode = true;

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Promo Carousel Demo',
      localizationsDelegates: const [
        PromoLocalizationsDelegate(),
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [Locale('en'), Locale('my')],
      locale: const Locale('my'),
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const DemoPage(),
    );
  }
}

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

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  String _lastAction = 'None';
  int _slidesViewed = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Promo Carousel Demo'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () async {
              await PromoCarousel.resetAll();
              if (context.mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('All slides reset')),
                );
              }
            },
            tooltip: 'Reset all slides',
          ),
          IconButton(
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) {
                    return const VideoExamplesPage();
                  },
                ),
              );
            },
            icon: const Icon(Icons.video_call),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            'Last Action: $_lastAction',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          Text('Slides Viewed: $_slidesViewed'),
          const SizedBox(height: 16),
          // Hero Animation Target
          Center(
            child: Hero(
              tag: 'promo_image_hero',
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.deepPurple.withValues(alpha: 0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: const Icon(
                  Icons.image,
                  size: 40,
                  color: Colors.deepPurple,
                ),
              ),
            ),
          ),
          const SizedBox(height: 24),

          _buildButton(
            context,
            'Hero Animation Demo',
            () => _showHeroAnimationCarousel(context),
          ),

          _buildButton(
            context,
            'Show Standard Carousel',
            () => _showStandardCarousel(context),
          ),

          _buildButton(
            context,
            'Show with Auto-Advance',
            () => _showAutoAdvanceCarousel(context),
          ),

          _buildButton(
            context,
            'Show with Custom Widgets',
            () => _showCustomCarousel(context),
          ),

          _buildButton(
            context,
            'Show Bottom Sheet',
            () => _showBottomSheetCarousel(context),
          ),

          _buildButton(
            context,
            'Show Fullscreen',
            () => _showFullscreenCarousel(context),
          ),

          _buildButton(
            context,
            'Show with Analytics',
            () => _showAnalyticsCarousel(context),
          ),

          _buildButton(
            context,
            'Show from JSON (Remote Config)',
            () => _showJsonCarousel(context),
          ),

          _buildButton(
            context,
            'Preview Mode (All Slides)',
            () => _showPreviewCarousel(context),
          ),
          const Divider(),
          _buildButton(
            context,
            '3D Transitions (Cube)',
            () => _show3DTransitionCarousel(context, TransitionType.cube),
          ),
          _buildButton(
            context,
            '3D Transitions (Flip)',
            () => _show3DTransitionCarousel(context, TransitionType.flip3D),
          ),
          _buildButton(
            context,
            'Audio Feedback',
            () => _showAudioCarousel(context),
          ),
          _buildButton(
            context,
            'Geolocation Rules (US Only)',
            () => _showGeolocationCarousel(context, 'US'),
          ),
        ],
      ),
    );
  }

  Widget _buildButton(
    BuildContext context,
    String text,
    VoidCallback onPressed,
  ) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16)),
        child: Text(text),
      ),
    );
  }

  void _showStandardCarousel(BuildContext context) {
    PromoCarousel.show(
      context: context,
      slides: _getStandardSlides(),
      config: const PromoCarouselConfig(
        showCloseButton: true,
        showDontShowAgain: true,
        enableHaptics: true,
      ),
      onAction: _handleAction,
      onDismiss: () => print('Carousel dismissed'),
    );
  }

  void _showAutoAdvanceCarousel(BuildContext context) {
    PromoCarousel.show(
      context: context,
      slides: _getStandardSlides(),
      config: const PromoCarouselConfig(
        autoAdvance: true,
        autoAdvanceDuration: Duration(seconds: 3),
        pauseOnInteraction: true,
        showProgressBar: true,
        progressBarPosition: ProgressPosition.top,
      ),
      onAction: _handleAction,
    );
  }

  void _showCustomCarousel(BuildContext context) {
    // Simulated user data
    final userZodiacSign = 'β™‹ Cancer';
    final userTopSearch = 'why am I crying at 3am';

    PromoCarousel.show(
      context: context,
      slides: [
        PromoSlide(
          id: 'zodiac_preview',
          title: 'Late-Night Most\nGoogled\nSecrets',
          visualType: PromoVisualType.custom,
          cta: PromoCTA(
            text: 'Face your secrets',
            action: PromoAction.openFeature,
            target: 'zodiac_detail',
          ),
          rules: const PromoRules(showOnce: true),
          customContentBuilder: (context) {
            return _buildZodiacWidget(userZodiacSign, userTopSearch);
          },
        ),
        ..._getStandardSlides().skip(1),
      ],
      config: PromoCarouselConfig.announcement(),
      onAction: _handleAction,
    );
  }

  void _showBottomSheetCarousel(BuildContext context) {
    PromoCarousel.showBottomSheet(
      context: context,
      slides: _getStandardSlides(),
      config: const PromoCarouselConfig(
        showSkipButton: true,
        backdropBlur: 5.0,
      ),
      onAction: _handleAction,
    );
  }

  void _showFullscreenCarousel(BuildContext context) {
    PromoCarousel.showFullscreen(
      context: context,
      slides: _getStandardSlides(),
      config: PromoCarouselConfig.onboarding(),
      onAction: _handleAction,
    );
  }

  void _showAnalyticsCarousel(BuildContext context) {
    PromoCarousel.show(
      context: context,
      slides: _getStandardSlides(),
      analytics: PromoCarouselAnalytics(
        onSlideViewed: (slideId, index) async {
          print('πŸ“Š Viewed: $slideId at index $index');
          await Future.delayed(const Duration(milliseconds: 500));
          if (context.mounted) {
            setState(() => _slidesViewed++);
          }
        },
        onSlideSkipped: (slideId, index) {
          print('⏭️ Skipped: $slideId');
        },
        onCTAClicked: (slideId, action, target) {
          print('πŸ”˜ CTA Clicked: $slideId - $action -> $target');
        },
        onCarouselCompleted: (viewedSlides) {
          print('βœ… Completed! Viewed: $viewedSlides');
        },
        onCarouselDismissed: (lastIndex, viewedSlides) {
          print('❌ Dismissed at $lastIndex. Viewed: $viewedSlides');
        },
        onSkipAll: () {
          print('⏩ Skip all clicked');
        },
      ),
      config: const PromoCarouselConfig(showSkipButton: true),
      onAction: _handleAction,
    );
  }

  void _showJsonCarousel(BuildContext context) {
    // Simulate loading from remote config
    final jsonData = [
      {
        'id': 'remote_promo_1',
        'title': 'New Feature Alert!',
        'subtitle': 'Loaded from remote config',
        'visualType': 'featureHighlight',
        'cta': {
          'text': 'Try Now',
          'action': 'openFeature',
          'target': 'new_feature',
        },
        'rules': {'showOnce': true, 'minAppVersion': '1.0.0'},
      },
    ];

    final slides = PromoCarousel.fromJson(jsonData);

    PromoCarousel.show(
      context: context,
      slides: [...slides, ..._getStandardSlides()],
      onAction: _handleAction,
    );
  }

  void _showPreviewCarousel(BuildContext context) {
    PromoCarousel.showPreview(
      context: context,
      slides: _getStandardSlides(),
      config: const PromoCarouselConfig(showProgressBar: true),
      onAction: _handleAction,
    );
  }

  void _handleAction(PromoAction action, String? target) {
    setState(() {
      _lastAction = '$action -> $target';
    });

    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text('Action: $action β†’ $target')));
  }

  void _show3DTransitionCarousel(BuildContext context, TransitionType type) {
    PromoCarousel.show(
      context: context,
      slides: _getStandardSlides(),
      config: PromoCarouselConfig(
        transitionType: type,
        transitionDuration: const Duration(milliseconds: 800),
        showCloseButton: true,
      ),
      onAction: _handleAction,
    );
  }

  void _showAudioCarousel(BuildContext context) {
    PromoCarousel.show(
      context: context,
      slides: [
        PromoSlide(
          id: 'audio_slide',
          title: 'Per-Slide Sound',
          subtitle: 'This slide plays a unique sound when shown',
          visualType: PromoVisualType.featureHighlight,
          audioAsset: 'sounds/alert.mp3',
          cta: PromoCTA(text: 'Nice!', action: PromoAction.close),
          rules: const PromoRules(),
        ),
        ..._getStandardSlides(),
      ],
      config: const PromoCarouselConfig(
        ctaClickSound: 'sounds/cta_click.mp3',
        slideChangeSound: 'sounds/page_turn.mp3',
        showCloseButton: true,
      ),
      onAction: _handleAction,
    );
  }

  void _showGeolocationCarousel(BuildContext context, String country) {
    final slides = [
      PromoSlide(
        id: 'us_only_offer',
        title: 'πŸ‡ΊπŸ‡Έ US Exclusive Offer',
        subtitle: 'Only visible for users in the United States',
        visualType: PromoVisualType.featureHighlight,
        cta: PromoCTA(text: 'Claim Offer', action: PromoAction.close),
        rules: const PromoRules(countries: ['US']),
      ),
      PromoSlide(
        id: 'global_offer',
        title: 'Global Feature',
        subtitle: 'Visible to everyone everywhere',
        visualType: PromoVisualType.featureHighlight,
        cta: PromoCTA(text: 'Got it', action: PromoAction.close),
        rules: const PromoRules(),
      ),
    ];

    PromoCarousel.show(
      context: context,
      slides: slides,
      currentCountry: country,
      onAction: _handleAction,
    );
  }

  void _showHeroAnimationCarousel(BuildContext context) {
    PromoCarousel.show(
      context: context,
      slides: [
        PromoSlide(
          id: 'hero_demo',
          title: 'Hero Animation',
          subtitle: 'The image will animate back to the page!',
          visualType: PromoVisualType.image,
          imageAsset:
              'https://image.shutterstock.com/image-vector/super-promo-3d-editable-text-260nw-2442897457.jpg',
          heroTag: 'promo_image_hero',
          cta: PromoCTA(text: 'Close and Watch', action: PromoAction.close),
          rules: const PromoRules(),
        ),
      ],
      config: const PromoCarouselConfig(enableHeroAnimations: true),
      onAction: _handleAction,
    );
  }

  List<PromoSlide> _getStandardSlides() {
    return [
      PromoSlide(
        id: 'welcome',
        title: 'Welcome to Our App!',
        subtitle: 'Discover amazing features',
        visualType: PromoVisualType.featureHighlight,
        cta: PromoCTA(
          text: 'Get Started',
          action: PromoAction.navigate,
          target: '/home',
        ),
        rules: const PromoRules(showOnce: true, showAfterDate: null),
      ),
      PromoSlide(
        id: 'feature_2',
        title: 'Search Made Easy',
        subtitle: 'Find what you need instantly',
        visualType: PromoVisualType.searchBar,
        cta: PromoCTA(
          text: 'Try Search',
          action: PromoAction.openFeature,
          target: 'search',
        ),
        rules: const PromoRules(showOnce: false),
      ),
      PromoSlide(
        id: 'premium_offer',
        title: 'Unlock Premium',
        subtitle: 'Get access to exclusive features',
        visualType: PromoVisualType.featureHighlight,
        cta: PromoCTA(text: 'Upgrade Now', action: PromoAction.openPaywall),
        rules: const PromoRules(showOnce: true, userSegments: ['free_tier']),
      ),
    ];
  }

  Widget _buildZodiacWidget(String sign, String search) {
    return Container(
      height: 200,
      decoration: BoxDecoration(
        color: Colors.grey[900],
        borderRadius: BorderRadius.circular(16),
      ),
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            decoration: BoxDecoration(
              color: Colors.grey[800],
              borderRadius: BorderRadius.circular(24),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.search, color: Colors.grey[400], size: 20),
                const SizedBox(width: 8),
                Text(
                  'your search history is loading...',
                  style: TextStyle(color: Colors.grey[400], fontSize: 14),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.auto_awesome, color: Colors.blue, size: 20),
              const SizedBox(width: 8),
              Flexible(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'The Google searches',
                      style: TextStyle(color: Colors.grey[400], fontSize: 13),
                    ),
                    RichText(
                      text: TextSpan(
                        style: const TextStyle(
                          fontSize: 13,
                          color: Colors.white,
                        ),
                        children: [
                          const TextSpan(text: 'your zodiac sign '),
                          TextSpan(
                            text: sign,
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                        ],
                      ),
                    ),
                    Text(
                      'hopes that it dies with them',
                      style: TextStyle(color: Colors.grey[400], fontSize: 13),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}