Playbooks
Moments Prefetch Integration O...
Flutter β Moments Prefetch Integration Guide
22 min
overview this guide explains how to integrate the momentscience moments solution into your flutter application to present personalized offers during the checkout experience this guide covers two integration modes, both designed to preload and display offers in a webview sdk prefetch mode api prefetch mode both options enable a high performing native experience while keeping the integration lightweight and flexible integration modes sdk prefetch mode the sdk automatically preloads and caches offers using a 0Γ0 webview placed in advance of checkout offers are displayed instantly when the user reaches the checkout step easiest integration path no need to make manual api calls api prefetch mode your app uses a native https request to fetch offers from the moments api and passes the response into the sdk during checkout full control over when and how offers are requested ability to customize the request payload (e g , user id, cart value) in both modes, the offers are rendered inside a webview on the checkout screen offers are preloaded ahead of time, so user experience remains responsive, even when no offers are available if no offers are returned, your app can skip rendering the offer section to see a full implementation, check out the momentscience flutter demo on github which includes examples for offer fetching, event handling, and webview integration requirements to integrate the momentscience sdk into your flutter app, ensure the following prerequisites are met a valid momentscience sdk id , which you can obtain by following these steps environment flutter 3 0 0+ dart 2 17 0+ ios minimum deployment target ios 12 android minimum sdk version api level 21 integration steps step 1 add dependencies add html assets add the required html templates to your project these files are used by the sdk to render the offer experience in a webview create a directory assets/html/ place the following files in the assets/html/ directory checkout html https //github com/adspostx/examples/blob/main/flutter/mssdkdemoapp flutter/mssdk demo app/assets/html/checkout html prefetch html https //github com/adspostx/examples/blob/main/flutter/mssdkdemoapp flutter/mssdk demo app/assets/html/prefetch html register the asset path in your pubspec yaml pubspec yaml flutter assets \ assets/html/ install dependencies update your pubspec yaml file with the required packages dependencies http ^1 1 0 # for making api requests device info plus ^9 1 0 # for retrieving user agent information flutter inappwebview ^6 1 5 # for rendering the offers in webview url launcher ^6 1 10 # for launching urls externally flutter dotenv ^5 0 2 # for managing environment variables (optional) after adding the dependencies, install them by running the following command from your project root flutter pub get add internet permission to allow offer content to load, add the following permission in android/app/src/main/androidmanifest xml inside \<manifest> but before \<application> \<uses permission android\ name="android permission internet" /> this is required for the sdk to load offer content in both integration modes step 2 prefetch offers (sdk or api) you can prefetch offers using one of two modes sdk prefetch uses a hidden 0Γ0 webview to preload offers api prefetch uses a native api call to fetch offers, which are then injected into the sdk display template in both modes, you can skip showing the checkout screen if no offers are found option 1 sdk prefetch this method uses a hidden 0Γ0 webview to preload the moments sdk in the background when available offers are detected, the sdk triggers an ads found event in your app this is the most lightweight integration method and is ideal when you want to minimize native code involvement set up the webview place a hidden 0Γ0 webview on any screen before checkout make sure to enable the following webview settings javascript dom storage database support register javascript event handler the web sdk communicates with your app through javascript events you must handle these events in your app to react to sdk signals use addjavascripthandler to listen for web sdk events, and implement a handler like addjavascripthandler inappwebview( onwebviewcreated (controller) { webviewcontroller = controller; controller addjavascripthandler( handlername 'adpxcallback', callback (args) { if (args length >= 2) { string event = args\[0]; string payload = args\[1]; prefetchservice handlewebsdkevent(event, payload); } return null; }, ); }, ) handlewebsdkevent void handlewebsdkevent(string event, string payload) { debugprint('websdk event received $event'); if (event == 'ads found') { int offercount = getoffercountfromwebsdkpayload(payload); debugprint('offers found $offercount'); // notify the app/ui with the number of offers found websdkcallback? call(offercount); } } helper to extract the offer count int getoffercountfromwebsdkpayload(string payload) { try { final payloadobj = jsondecode(payload); final response = payloadobj\['response']; final data = response\['data']; final offers = data\['offers']; if (offers is list) return offers length; } catch (e) { debugprint('error parsing websdk payload $e'); } return 0; } load webview with the prefetch html template use the existing prefetch html template from your local assets inject your sdk id, sdk cdn url, and adpxuser configuration (payload) dynamically, then load the resulting html into the webview prefetchwithwebsdk // prefetchwithwebsdk() replaces placeholders in prefetch html // with actual values, such as sdkid, payload, and config // it then loads the final html into the webview, // which initializes the sdk in prefetch mode future\<void> prefetchwithwebsdk(string sdkid, inappwebviewcontroller? webviewcontroller) async { if (webviewcontroller == null) throw exception("webview controller is not initialized"); final payload = await createpayload(); final htmltemplate = await rootbundle loadstring('assets/html/prefetch html'); // insert user config as window\ adpxuser string adpxuserconfigscript = ''; if (payload isnotempty) { adpxuserconfigscript = 'window\ adpxuser = {\n'; payload foreach((key, value) { adpxuserconfigscript += ' $key "$value",\n'; }); adpxuserconfigscript = adpxuserconfigscript replacefirst(regexp(r',\n$'), '\n') + '};\n'; } // inject into template string finalhtml = htmltemplate replaceall('%%sdk id%%', sdkid) replaceall('%%sdk cdn url%%', appconfig web launcherscripturl) replacefirst('window\ adpxuser = {};', adpxuserconfigscript); await webviewcontroller loaddata( data finalhtml, mimetype 'text/html', encoding 'utf 8', baseurl weburi('https //myapp local'), // avoid using about\ blank to prevent webview security errors ); debugprint('websdk prefetch initialized with sdk id $sdkid'); } createpayload future\<map\<string, string>> createpayload() async { return { 'pub user id' <"unique value">, // a unique, non pii identifier for the user 'adpx fp' <"unique value">, // unique user identifier 'ua' await deviceutils getuseragent(), //user agent string 'themeid' 'demo', // pass valid themeid 'placement' 'checkout', //indicates where the offer unit is shown (e g , checkout screen) }; } handle ads found event once the webview receives the ads found event, extract the offer count if offercount == 0 , you can skip rendering the offer ui entirely see prefetch service dart in the demo app for working implementation option 2 prefetch with api this method allows your app to prefetch offers directly from the moments api and pass them into the web sdk via script injection for complete details on moments api, refer to the moments api documentation https //docs adspostx com/moments api#rqin send a post request to the moments api moments api endpoint post https //api adspostx com/native/v4/offers json build the request parameters include the following api key as a query parameter a user payload in the request body custom user agent in the request headers validate optional parameters (if used) loyaltyboost must be " 0 ", " 1 ", or " 2 " creative must be " 0 " or " 1 " prepare and send the request use the following example code to construct and send the request fetchoffers function future\<map\<string, dynamic>> fetchoffers({ required string sdkid, bool isdevelopment = false, map\<string, string>? payload, string? loyaltyboost, string? creative, string? campaignid, }) async { if (loyaltyboost != null && !\['0', '1', '2'] contains(loyaltyboost)) { throw networkexception(networkerror invalidparameter, 'loyaltyboost must be 0, 1, or 2'); } if (creative != null && !\['0', '1'] contains(creative)) { throw networkexception(networkerror invalidparameter, 'creative must be 0 or 1'); } try { final uri = uri parse(appconfig api baseurl); final queryparams = { 'api key' sdkid, if (loyaltyboost != null) 'loyaltyboost' loyaltyboost, if (creative != null) 'creative' creative, if (campaignid != null) 'campaignid' campaignid, }; final url = uri( scheme uri scheme, host uri host, path uri path, queryparameters queryparams, ); final requestbody = \<string, dynamic>{}; if (isdevelopment) requestbody\['dev'] = '1'; if (payload != null) requestbody addall(payload); final string useragent = payload?\['ua'] ?? await deviceutils getuseragent(); final jsonbody = jsonencode(requestbody); final response = await client post( url, headers { 'content type' 'application/json', 'user agent' useragent, }, body jsonbody, ); if (response statuscode >= 200 && response statuscode < 300) { return jsondecode(response body) as map\<string, dynamic>; } else { throw networkexception( networkerror servererror, 'server returned error ${response statuscode}', ); } } catch (e) { debugprint('network error $e'); throw networkexception( networkerror connectionerror, 'failed to connect ${e tostring()}', ); } } createpayload future\<map\<string, string>> createpayload() async { return { 'pub user id' "\<unique value>", // simple unique id 'adpx fp' "\<unique value>", // unique value 'ua' await deviceutils getuseragent(), //always set a valid user agent 'themeid' 'demo', // pass valid themeid here 'placement' 'checkout', }; save the api response locally store the json response from the api to use it when initializing the sdk see prefetch service dart , network service dart and device utils dart in the demo app for a complete working implementation of this flow step 3 show offers using fullscreen webview after confirming that offers are available, display them on your checkout screen using a fullscreen webview pass the necessary parameters depending on the prefetch method navigate to the checkout screen use navigator push() to launch a checkoutscreen and pass the required values navigator push( context, materialpageroute( builder (context) => checkoutscreen( sdkid sdkidcontroller text trim(), prefetchmethod prefetchmethod, // you may not need this parameter as you are going to use only one of the approach apiresponse apiresponse, // only required for api prefetch offercount offercount, // optional, require only if you want to show no of offers in ui payload payload, ), ), ); load the offer experience inside the checkoutscreen , load the offer experience into the webview using checkout html follow these steps read checkout html from assets replace placeholders like %%sdk id%% , %%config%% , and %%adpxuser%% with actual values (from the payload or api response) load the final html into the webview ensure javascript, dom storage, and database settings are enabled in the webview use getautoloadconfig() to determine whether auto loading should be enabled based on your prefetch method choose logic based on prefetch mode use different logic depending on whether you're using prefetch with sdk cached offers are automatically shown by the sdk prefetch with api you need to pass the saved apiresponse and inject it via javascript loadcontentintowebview future\<void> loadcontentintowebview({ required inappwebviewcontroller controller, required string sdkid, required bool isprefetchapi, map\<string, dynamic>? apiresponse, required int offercount, map\<string, string>? payload, }) async { try { // reset the externally opened urls when loading new content resetexternallyopenedurls(); // get sdk cdn url from appconfig final string sdkcdnurl = appconfig web launcherscripturl; // load html template from assets final string htmltemplate = await rootbundle loadstring('assets/html/checkout html'); // create adpxuser script with payload key value pairs string adpxuserscript = createadpxuserscript(payload); // different config based on prefetch method string autoloadconfig = getautoloadconfig(isprefetchapi); // prepare response handling code only for api prefetch method string responsehandling = createresponsehandlingscript(isprefetchapi, apiresponse); // replace placeholders with actual values string html = htmltemplate replaceall('%%sdk id%%', sdkid) replaceall('%%sdk cdn url%%', sdkcdnurl) replaceall('%%offers count%%', offercount tostring()) replaceall('%%autoload config%%', autoloadconfig) replaceall('%%response handling%%', responsehandling) replaceall('window\ adpxuser = {};', adpxuserscript); // load html content directly with a proper baseurl await controller loaddata( data html, mimetype 'text/html', encoding 'utf 8', baseurl weburi( 'https //myapp local', ), // required to avoid security errors; 'about\ blank' or skipping this causes issues ); debugprint('webview initialized with sdk id $sdkid'); } catch (e) { debugprint('error loading html content $e'); rethrow; } } createadpxuserscript string createadpxuserscript(map\<string, string>? payload) { string adpxuserscript = 'window\ adpxuser = {'; if (payload != null && payload isnotempty) { payload foreach((key, value) { adpxuserscript += '\n $key "$value",'; }); // remove trailing comma adpxuserscript = adpxuserscript substring(0, adpxuserscript length 1); } adpxuserscript += '\n};'; return adpxuserscript; } getautoloadconfig string getautoloadconfig(bool isprefetchapi) { if (isprefetchapi) { // for api prefetch disable auto features since we'll provide the response return 'autoload false, prefetch false'; } else { // for websdk prefetch enable auto features to use cached data return 'autoload true, prefetch true'; } } createresponsehandlingscript string createresponsehandlingscript( bool isprefetchapi, map\<string, dynamic>? apiresponse, ) { if (isprefetchapi && apiresponse != null) { // prepare the json string for the api response final apiresponsejson = jsonencode(apiresponse); return ''' // add the api response to adpx if (window\ adpx && window\ adpx setapiresponse) { const apiresponse = $apiresponsejson; window\ adpx setapiresponse(apiresponse) then(() => { console log('api response set to adpx, now reloading '); window\ adpx reload(); }); } '''; } return ''; } see checkout service dart and checkout screen dart in the demo app for working implementation step 4 open clicks in an external browser to ensure that offer clickouts open in the deviceβs default browser (rather than inside the webview), listen for sdk callback events and handle the url externally add a javascript handler in the webview use addjavascripthandler to capture sdk events such as url clicked addjavascripthandler controller addjavascripthandler( handlername 'adpxcallback', callback (args) { if (args length >= 2) { string event = args\[0]; dynamic payload = args\[1]; debugprint('webview callback $event, $payload'); checkoutservice handleadevent(event, payload); } return null; }, ); processurlevent future\<void> handleadevent(string event, dynamic payload) async { debugprint('processing ad event $event'); // handle ad taken event that requires opening an external url if (event == 'url clicked') { await processurlevent(payload); } } future\<void> processurlevent(dynamic payload) async { try { // parse payload if it's a string map\<string, dynamic> payloadmap; if (payload is string) { try { payloadmap = map\<string, dynamic> from(json decode(payload)); } catch (e) { debugprint('error parsing payload $e'); return; } } else if (payload is map) { payloadmap = map\<string, dynamic> from(payload); } else { debugprint('unexpected payload type ${payload runtimetype}'); return; } // extract target url and open in external browser if (payloadmap containskey('target url')) { final targeturl = payloadmap\['target url']; if (targeturl != null && targeturl is string) { final normalizedurl = normalizeurl(targeturl); externallyopenedurls add(normalizedurl); await openexternalurl(normalizedurl); } } } catch (e) { debugprint('error processing url event $e'); } } handle the url clicked event in your service open external urls manually and prevent the webview from navigating to the same url again handleurlnavigation future\<navigationactionpolicy> handleurlnavigation(navigationaction navigationaction) async { final uri = navigationaction request url; if (uri != null) { await future delayed(const duration(seconds 1)); final url = uri tostring(); final normalizedurl = normalizeurl(url); debugprint('intercepted url $normalizedurl'); if ( externallyopenedurls contains(normalizedurl)) { debugprint('canceling navigation for externally opened url $normalizedurl'); return navigationactionpolicy cancel; } return navigationactionpolicy allow; } return navigationactionpolicy cancel; } see checkout screen dart in the demo app for working implementation conclusion congratulations! you've now completed the integration of the momentscience sdk into your flutter app whether you're using sdk prefetch mode or api prefetch mode , your app is now ready to deliver personalized offers directly within the user journey, enhancing monetization while maintaining full control over offer timing and display π’ if you're running into any issues while going through the integration process, feel free to contact us at help\@m omentscience com