Flutter plugin for the REES46 personalization platform — a thin bridge over the native Android and iOS SDKs. Storage, sessions, identity and push delivery happen natively; Dart only routes calls to the right shop instance.
flutter pub add rees46_sdkEverything is exported from a single import:
import 'package:rees46_sdk/rees46_sdk.dart';The native dependencies come with it — com.github.rees46:android-sdk from
JitPack and the REES46 pod from CocoaPods trunk.
The plugin brings its own Gradle settings; the only value your app must agree on
is minSdk 24. Current Flutter versions already default to it, so a freshly
generated project needs no changes — set it only if you hardcoded something lower:
android {
defaultConfig {
minSdk = 24
}
}Your app's Java version does not have to match the plugin's — compileOptions
and jvmTarget only govern the module they are declared in.
If your android/settings.gradle.kts centralizes repositories
(RepositoriesMode.FAIL_ON_PROJECT_REPOS / PREFER_SETTINGS), add JitPack there —
otherwise the native SDK cannot be resolved:
maven(url = "https://jitpack.io")Nothing to add: the plugin's podspec pulls the REES46 pod and declares the
iOS 13.0 minimum itself, pod install runs as part of flutter run, and the
plugin registers its own application delegate — your AppDelegate stays untouched.
Initialize once, as early as possible — typically in main(), before runApp:
import 'package:flutter/widgets.dart';
import 'package:rees46_sdk/rees46_sdk.dart';
late final PersonalizationSdk sdk;
void main() {
WidgetsFlutterBinding.ensureInitialized();
sdk = REES46.initialize(
const REES46Config(shopId: 'YOUR_SHOP_ID'),
);
runApp(const MyApp());
}REES46 is the entry point. REES46.initialize returns the handle
synchronously and starts native initialization in the background; calls issued
right after are queued natively until the session is ready, so the handle is
usable straight away. A broken setup surfaces as a PlatformException on the
first call you make with it (bad_args for an empty shopId, init_failed if
native init threw).
shopId is the only required field:
| Field | Default | Notes |
|---|---|---|
shopId |
— (required) | Your REES46 shop key |
apiDomain |
api.rees46.ru |
API host |
stream |
android / ios |
Traffic stream label; defaults to the current platform |
autoSendPushToken |
true |
Fetches and sends the push token during init |
needReInitialization |
false |
Forces a fresh session / device id |
Push delivery needs platform setup of its own — a Firebase config on Android, the Push Notifications capability on iOS. Without it initialization still succeeds; there is simply no token to send.
Keep one place that owns the handle, so no widget re-initializes:
class REES46Service {
static const _shopId = 'YOUR_SHOP_ID';
static PersonalizationSdk get sdk => REES46.isInitialized(_shopId)
? REES46.getInstance(_shopId)
: REES46.initialize(const REES46Config(shopId: _shopId));
}final sid = await sdk.getSid(); // session id
final did = await sdk.getDid(); // device id issued by REES46A non-empty did means the native SDK completed its handshake with the API.
One app can run several shops at once — regional storefronts, super-app tenants.
Each gets its own native instance with isolated storage, session and did.
// Registered now, initialized on first use.
REES46.registerShops(const [
REES46Config(shopId: 'shop-a'),
REES46Config(shopId: 'shop-b'),
]); // pass eagerInit: true to initialize up front
final shopA = REES46.getInstance('shop-a');Address instances explicitly once more than one is registered: getInstance()
without an id resolves only while exactly one shop is registered, and throws
AmbiguousShopException otherwise (UnknownShopIdException for an id that was
never registered).
Standard events live in the tracking namespace of the handle:
await sdk.tracking.productView('sku-1');Each method returns a Future<void> that completes once the native SDK has the
API's answer; a rejected request throws a PlatformException (track_failed, or
track_event_failed / track_purchase_failed for custom events and orders).
Awaiting is optional — fire-and-forget from UI code is fine.
With several shops the namespace follows the instance:
await REES46.getInstance('shop-b').tracking.productView('sku-1');// Product page opened.
await sdk.tracking.productView('sku-1');
// Category listing opened.
await sdk.tracking.categoryView('boots');
// The user searched. Pass `results` when your app runs the search itself and
// knows the ids it showed.
await sdk.tracking.search('winter boots', results: ['sku-1', 'sku-2']);// One product added.
await sdk.tracking.addToCart(
const TrackingItem(id: 'sku-1', quantity: 2, price: 49.9, fashionSize: '42'),
);
// The cart as it looks after a change — replaces the stored contents.
await sdk.tracking.syncCart(const [
TrackingItem(id: 'sku-1', quantity: 2, price: 49.9),
TrackingItem(id: 'sku-2'), // quantity defaults to 1
]);
// One product removed.
await sdk.tracking.removeFromCart('sku-1');TrackingItem needs only an id; quantity defaults to 1, price and
fashionSize are optional.
await sdk.tracking.addToFavorites('sku-1');
await sdk.tracking.syncFavorites(const ['sku-1', 'sku-2']);
await sdk.tracking.removeFromFavorites('sku-1');An empty list is how an emptied cart or wishlist is reported: items goes on
the wire as [] alongside full_cart / full_wish, which is what the native
SDKs send. Removing a single product is still removeFromCart /
removeFromFavorites.
Only needed when you render stories yourself — the built-in stories view tracks its own views and taps.
await sdk.tracking.storyView(
storyId: '4321',
slideId: '2',
code: 'main_stories',
);
await sdk.tracking.storyClick(
storyId: '4321',
slideId: '2',
code: 'main_stories',
);code is the stories block; omit it and the native SDK uses the block it loaded
last. A tracked story also becomes the source of the events that follow it — the
same rule as setSource.
await sdk.tracking.purchase(
orderId: 'order-1024',
orderPrice: 149.8,
items: const [
PurchaseLineItem(id: 'sku-1', amount: 2, price: 49.9, fashionSize: '42'),
PurchaseLineItem(id: 'sku-2', amount: 1, price: 50.0),
],
);Those three are required; everything else is optional:
await sdk.tracking.purchase(
orderId: 'order-1024',
orderPrice: 149.8,
items: const [PurchaseLineItem(id: 'sku-1', amount: 1, price: 149.8)],
deliveryType: 'courier',
deliveryAddress: 'Tverskaya 1, Moscow',
paymentType: 'card',
isTaxFree: false,
isGiftPackage: true,
promocode: 'WINTER20',
orderCash: 100.0,
orderBonuses: 49.8,
orderDelivery: 5.0,
orderDiscount: 20.0,
channel: 'mobile_app',
custom: const {'store_id': 'msk-01'},
recommendedSource: const {'from': 'dynamic', 'code': 'main_page_block'},
stream: 'flutter',
segment: 'a',
);custom goes on the wire under custom; keys that collide with the order's own
fields (order_id, items, event, shop_id, …) are rejected natively.
await sdk.tracking.custom(
'subscribed_to_newsletter',
category: 'account',
label: 'footer_form',
value: 1,
time: DateTime.now().millisecondsSinceEpoch ~/ 1000, // UNIX seconds
customFields: const {'plan': 'weekly'},
);customFields is the free-form part: its entries are sent at the top level and
duplicated under payload. The SDK's own keys are reserved and rejected —
event, time, category, label, value, source, payload, from,
code, stream, shop_id, did, sid, seance, segment.
Events can carry the campaign the user arrived from — a bulk mobile push, a
chain. Both values come from the link that opened the app, which carries them as
recommended_by and recommended_code:
myshop://product/sku-1?recommended_by=bulk&recommended_code=jkIWdXSRfwVyK
A campaign brings the user in and then colours everything they do next, so store it once when you handle the link:
await sdk.tracking.setSource(
const TrackingSource(type: TrackingSourceType.bulk, code: 'jkIWdXSRfwVyK'),
);
// Carries recommended_by=bulk without being told to.
await sdk.tracking.productView('sku-1');The stored source lives natively per shop, survives restarts, and is kept for 48 hours or until it is replaced.
To attribute a single event instead of everything that follows, pass source per
call — productView, addToCart and addToFavorites accept it:
await sdk.tracking.addToCart(
const TrackingItem(id: 'sku-1', quantity: 1),
source: const TrackingSource(type: TrackingSourceType.chain, code: 'welcome_2'),
);A per-call source wins over the stored one, and an order is attributed by the
recommendedSource of the purchase call itself.
TrackingSourceType |
Wire value | Set by |
|---|---|---|
dynamicBlock |
dynamic |
Dynamic recommender block |
chain |
chain |
Recommendation chain |
bulk |
bulk |
Bulk mailing |
transactional |
transactional |
Transactional mailing |
instantSearch |
instant_search |
Search suggestions |
fullSearch |
full_search |
Full search results page |
stories |
stories |
Stories block |
webPushDigest |
web_push_digest |
Web push digest |
MIT — see LICENSE.