loymax_personal_offers 1.1.1 copy "loymax_personal_offers: ^1.1.1" to clipboard
loymax_personal_offers: ^1.1.1 copied to clipboard

Flutter widgets for embedding Loymax personal offers via a WebView (carousel and full-screen list).

loymax_personal_offers #

Flutter widgets for embedding Loymax personal offers into a host app through a WebView. Ships two top-level widgets β€” a carousel for the home screen and a full-screen list β€” plus the bridge that decodes JS events into strongly-typed Dart objects.

πŸ‡·πŸ‡Ί Русская вСрсия


Features #

  • LoymaxOffersCarousel β€” horizontal carousel block, ~280 px tall, designed to live inside a ListView on the home screen.
  • LoymaxOffersView β€” full-screen list, intended to be wrapped in a Scaffold by the host app.
  • Four-phase state machine (loading / ready / error / empty) with customisable loadingBuilder, errorBuilder and emptyBuilder β€” return SizedBox.shrink() to fully collapse the block in any phase.
  • Optional AnimatedSize transitions between phases (tunable duration / curve, can be turned off).
  • LoymaxOffersController for imperative reload() and phase observation.
  • Cross-platform pull-to-refresh implemented through a JS injection (no platform-specific glue required).
  • Strongly-typed events: LoymaxViewAllTap, LoymaxCardTap, LoymaxActivateTap, LoymaxNoContent.
  • keepAlive flag for embedding inside lazy parents (ListView, TabBarView, PageView).
  • hideTitle toggle on both widgets β€” keep the Loymax page's built-in title or drop it (default).

Quick start #

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

const LoymaxOffersConfig kLoymaxConfig = LoymaxOffersConfig(
  baseUrl: '<LOYMAX_OFFERS_BASE_URL>',
);

class HomePage extends StatefulWidget {
  const HomePage({super.key, required this.personUid});
  final String personUid;

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

class _HomePageState extends State<HomePage> {
  late final LoymaxOffersController _offers = LoymaxOffersController();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: RefreshIndicator(
        onRefresh: () async => _offers.reload(),
        child: ListView(
          children: [
            LoymaxOffersCarousel(
              config: kLoymaxConfig,
              controller: _offers,
              partner: '<partner>',
              personUid: widget.personUid,
              onEvent: (event) {
                if (event is LoymaxViewAllTap || event is LoymaxCardTap) {
                  Navigator.of(context).push(MaterialPageRoute(
                    builder: (_) => OffersPage(personUid: widget.personUid),
                  ));
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

class OffersPage extends StatelessWidget {
  const OffersPage({super.key, required this.personUid});
  final String personUid;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('My offers')),
      body: LoymaxOffersView(
        config: kLoymaxConfig,
        partner: '<partner>',
        personUid: personUid,
        pullToRefreshEnabled: true,
        onEvent: (event) {
          if (event is LoymaxActivateTap) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Activated: ${event.offer.name}')),
            );
          }
        },
      ),
    );
  }
}

Configuration #

const LoymaxOffersConfig kLoymaxConfig = LoymaxOffersConfig(
  baseUrl: '<LOYMAX_OFFERS_BASE_URL>',
  // jsBridgeName: 'LoymaxBridge', // override only if Loymax renamed the channel
);

The full URL is built as {baseUrl}/{partner}/?personUid=…&view=row&no-title. Pass hideTitle: false on the widget to keep the page's built-in title (the no-title flag is then dropped from the query).

Phases and builders #

LoymaxOffersPhase has four values: loading, ready, error, empty. Both LoymaxOffersCarousel and LoymaxOffersView accept:

  • loadingBuilder: (context) => Widget β€” placeholder during loading; return SizedBox.shrink() to hide the block until the WebView is ready.
  • errorBuilder: (context, retry) => Widget β€” error view; call retry() to reload.
  • emptyBuilder: (context) => Widget β€” shown when the page reports no_content (loaded but no offers for this user). If null, the WebView is left visible with Loymax's own empty UI; return SizedBox.shrink() to collapse the block.

The carousel additionally wraps the result in AnimatedSize so that size changes between phases are animated. Pass resizeAnimationDuration: null to disable the wrapper.

See example/lib/demo_gallery.dart for a walkthrough of every builder combination.

Events #

The WebView posts JSON messages through the LoymaxBridge JS channel. LoymaxOfferEvent.tryParse decodes them into:

Event When it fires
LoymaxViewAllTap User tapped "view all" inside the carousel.
LoymaxCardTap User tapped a card (carousel or list).
LoymaxActivateTap User activated an offer. Contains LoymaxOffer.
LoymaxNoContent Page rendered with no offers available. Independently flips the widget into LoymaxOffersPhase.empty.
LoymaxOtherEvent Forward-compat wrapper for any event name the package does not recognise. Carries the raw name and the full decoded payload.

event.source tells you whether the event came from the carousel or the list.

Forward compatibility #

If Loymax adds new bridge events, the package will not throw or drop them β€” they are surfaced as LoymaxOtherEvent so the host app can opt into handling them without a package upgrade:

onEvent: (event) {
  switch (event) {
    case LoymaxActivateTap(:final offer):
      // ...
    case LoymaxOtherEvent(:final name, :final payload):
      if (name == 'some_new_event') {
        // Handle the new event using payload[...] yourself.
      }
    default:
      break;
  }
}

Controller #

LoymaxOffersController mirrors ScrollController / TextEditingController:

late final LoymaxOffersController _offers = LoymaxOffersController();

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

// Force reload (e.g. on auth change or pull-to-refresh):
_offers.reload();

// Observe phase (e.g. to toggle an AppBar spinner):
ListenableBuilder(
  listenable: _offers,
  builder: (_, __) => Icon(_offers.phase == LoymaxOffersPhase.loading
      ? Icons.hourglass_top
      : Icons.refresh),
);

Each controller can be attached to one widget at a time.

Pull-to-refresh #

Two options:

  1. Host scroll view. Wrap the parent ListView with RefreshIndicator and call controller.reload() in onRefresh. Recommended for the carousel embedded on the home screen.
  2. In-WebView (full-screen mode). Set pullToRefreshEnabled: true on LoymaxOffersView. The package injects JS that detects "pull down at the top of the page" and reloads. Customise the indicator via pullToRefreshIndicatorBuilder.

keepAlive #

When the widget lives inside a lazy parent (ListView, TabBarView, PageView), the parent will unmount it on scroll-off and the WebView controller will be recreated on the next mount, restarting the load. Pass keepAlive: true to keep the subtree alive. Default is false.

Example app #

cd example
flutter run

The example contains two screens:

  • Home (carousel) β€” a typical home screen integration.
  • Builder gallery β€” eight side-by-side variations of loadingBuilder / errorBuilder / emptyBuilder and the resize animation knob.

License #

MIT Β© Loymax. See LICENSE.

0
likes
160
points
153
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter widgets for embedding Loymax personal offers via a WebView (carousel and full-screen list).

Repository (GitHub)
View/report issues

Topics

#loymax #webview #personal-offers #loyalty #carousel

License

MIT (license)

Dependencies

flutter, webview_flutter

More

Packages that depend on loymax_personal_offers