appdna_sdk 1.0.21
appdna_sdk: ^1.0.21 copied to clipboard
AppDNA SDK for Flutter — analytics, experiments, paywalls, surveys, and more.
1.0.21 #
Wraps iOS 1.0.83 / Android 1.0.55.
AppDNA.reportPayingUser({productId, priceCents, currency})— tell AppDNA that the current user is in a paid state for a subscription the SDK never sold (billing run by your own server, a web checkout, a seat sold by a salesperson). Emitspaying_user_reportedwithsource: "host". Every argument is optional; the price is analytics only, because the paying-user meter counts distinct users rather than money. Safe to call on every launch — repeating it in a month costs nothing and misses nothing. Callidentify()first: the SDK attaches the identity it already holds, and there is deliberately no user-id argument, so an app can only report its own users.- Fixed:
setInitDelegatecould lose a real degradation (both platforms). When the SDK already had a pending init error and your Dart code started listening, the bridge swallowed oneonInitDegradedso the pending error was not delivered twice — but it swallowed whichever arrived FIRST. A degradation raised just after you started listening was dropped, and the older, stale error was delivered in its place. The duplicate is now identified by its type and message, so only the actual repeat is dropped.
1.0.20 #
-
flushInterval,batchSizeandconfigTTLreach the native SDK only when you set them. The plugin used to fill in 30 / 20 / 3600 when they were omitted, so native treated every Flutter app as having set them. Native now resolves each one as: your option > the value the server returns with the bootstrap request (if positive) > its default (30 s, no cap, 3600 s).batchSizenow works on both platforms: it caps the network-sized batch (100 / 50 / 20) — the flush threshold and the most one upload sends; a value below 1 is ignored (logged), as if not set. -
Experiments: Traffic Allocation and targeting are applied (native SDKs).
getVariantreturnsnull,isInVariantfalse, and no exposure is recorded for a user outside the experiment's traffic allocation or targeting rules (countries, minimum app version, new users only, user trait conditions); servable surfaces show the live entity to them. Before, every user on a targeted platform got a variant. -
shutdown()uploads the queued events. On both platforms it now makes one last attempt to upload the queued events; whatever it cannot send stays on the device and is sent after the nextconfigure()(Android also hands it to a background upload once the attempt has finished). Before, on iOS the attempt could be dropped before it ran, and Android only scheduled the background upload — which, when it ran during another upload, ended without sending anything. Aftershutdown()and a newconfigure(), events the old session's last upload delivered are not sent again (both platforms). -
A large queue of unsent events no longer slows uploads or
configure(). With up to 10,000 events queued, each upload used to read the whole queue (twice on iOS) andconfigure()aftershutdown()waited for it; now an upload reads only the events it sends, on both platforms. -
A failed bootstrap no longer lasts the whole session (native SDKs). When the bootstrap fails (for example, the app starts offline) the SDK becomes ready on cached config, reports the failure through
onInitDegraded(the native Android SDK now reports it as iOS does), and retries the bootstrap when the network comes back, on foreground — every trigger (foreground or network regained) waits a random 0–1 s — and after a backoff of up to 5 minutes (at most 10 retries). A 401 or 403 (the server refused the API key) ends the retries; a 429'sRetry-Afterholds the next one back. A retry that succeeds fetches remote config and starts the Firestore listeners;onReadydoes not fire again. -
⚠️ If you already pass
batchSize, it is now a hard cap. It had no effect before this release; withbatchSize: 10, every upload now sends at most 10 events. Remove it, or raise it, unless that is what you want. Passing an option's default (flushInterval30,batchSize100,configTTL3600) is the same as not passing it, on both platforms. -
onInitDegradedreaches every listening engine. With several Flutter engines, only the last one to callsetInitDelegatereceived it, and any engine's cancel stopped it for all; one native delegate now fans out to every listener, and a listener that joins late is replayed the pending degradation. On iOS the errortypenow matches Android (BootstrapFailed,SubsystemFailed,FirebaseConfigMissing;UnsupportedBlockTypeon iOS only) instead ofAppDNAInitError. -
Event queue (native SDKs). iOS: a launch that cannot read the queue file yet (a background launch before the first unlock) leaves it as it is — it used to index it as empty, and the next flush could truncate the unread events, uncounted; an older SDK's queue file is migrated in one atomic write; a failed compaction counts nothing, a successful one counts its drops once; a file that cannot be opened for appending is never overwritten. Both: the 5 MB quota measures the queued events' bytes the same way and evicts the oldest 10 % until they fit; the in-app queue reloads its window once it drains, so a backlog over 1,000 events is sent (and removes stored events another upload already delivered, which could stop the reload); the OS background upload drops and counts a permanently rejected batch and ends its run, the next run sending the rest (iOS kept sending it, blocking every later run; Android went on with up to 50 more batches); a 401 / 403 (invalid or revoked API key) pauses uploads at once — in-app and background — until the next foreground or
AppDNA.flush()(the in-app queue paused only after 5 failed cycles, dropping a batch on each). iOS: an event queued while an older SDK's queue file could not be migrated yet starts its own line (it joined the file's last line, and the next read lost every event in it); the 10,000-event limit holds after every event queued (it was applied every 500), and the background upload sends up to 50 batches per run (it sent one), as on Android. Android: the event database upgrades in one chunked pass, tolerates two upgrades or two creations at once, and a later downgrade to 1.0.54 or newer (a downgrade to 1.0.53 or older cannot open the database); queuing an event no longer reads the whole table (running totals); an eviction is counted once, after it commits. -
Do not roll back to plugin 1.0.19 or earlier after shipping this version (Android). Its native Android SDK writes event database schema version 4, which Android SDK 1.0.53 and earlier (plugin 1.0.19 and earlier) cannot open: after such a rollback no event is recorded or sent until the app updates again to plugin 1.0.20 or later (or is reinstalled, or its data is cleared).
-
Offline cold start (Android) and cached surveys (both). On Android, paywalls, onboarding flows, surveys, in-app messages and experiments cached by a previous session load again on an offline cold start (each cache was skipped); on both platforms cached surveys reach the survey manager at start-up.
-
Experiment targeting answers identically on both platforms (native SDKs). "New users only" treats a reinstall, or a restore from a backup or device transfer, as a new install on both (iOS no longer reads a backed-up install date — see below); the device country is the first two-letter country code among the locale's and the preferred languages' regions (Android no longer reads the SIM's or the network's country, which iOS has no equivalent of); a malformed
traffic_allocationallocates no one and a malformedstarted_at_msno longer drops the experiment (iOS); astarted_at_msoutside the 64-bit range is absent (Android saturated it); a malformed entry ofvariantsis dropped on its own (iOS dropped every variant, Android kept it with a placeholder id or weight 0); a true/false trait is never a number, and integer traits of every width compare as numbers; a trait condition without a readable trait or operator excludes the user. -
"New users only": the install date on iOS. The SDK's own install marker, in its backup-excluded directory, written by the first
configure()of an install: an app update keeps it; a reinstall, or a restore from a backup or device transfer, starts a new one (the restored Documents directory date and preferences are not read). The first launch with this version on an install where an older AppDNA SDK already ran takes the app container's creation date once. An app that adds the AppDNA SDK in an update dates its existing users from that update on iOS (Android:PackageInfo.firstInstallTime, the original install). -
Location traits on iOS. The
country,region,cityandtimezonetraits from the IP address stay throughidentify(userId, traits), a change of user andreset()(your own trait of the same name wins). -
Error
typein a minified Android build.onInitDegradedandgetLastInitErrorreport an explicit type per init error (BootstrapFailed, …) rather than the class's runtime name, and the native SDK's rules keep the names of its exception classes. -
Bootstrap retries (native SDKs). A 429 at
configure()holds the first retry back for itsRetry-After; a trigger during a retry in flight no longer starts the next one at once. -
Network state (native SDKs). iOS: the network monitor's state is thread-safe. Android: losing Wi-Fi while cellular is up no longer reads as offline; a VPN counts as connected on every path; only the default network sets the connection type (a change on a background network did).
-
setInitDelegateworks on iOS. The iOS plugin registered the init event channel with a handler that never emitted, soonInitDegradedreached Flutter apps on Android only. It now forwards the native iOS delegate, with the same{message, type}map as Android.lastInitError()answers on both platforms. -
A push tap after
shutdown()is no longer lost (Android). A drain of waiting taps posted while the SDK was ready could run aftershutdown()and hand a tap to the shut-down SDK, which dropped it. Now it hands nothing over and waits for the nextconfigure(). -
Event uploads during an outage (Android). After 5 consecutive upload failures the queue pauses; every
track()that filled a batch used to clear the pause and start a full upload cycle. Now only the app coming to the foreground andAppDNA.flush()clear it, as on iOS. On both platforms the pause now also holds for the OS background upload (WorkManager / BGProcessingTask): backgrounding a paused app schedules none, and one that runs while paused uploads nothing. -
The iOS bootstrap is never answered from the HTTP cache. It was cached for 24 hours, so a later launch could start from a day-old answer — a runtime lock, a new map key or the device's location took up to a day to arrive. Every bootstrap now reaches the server; with no network it fails (the SDK runs on its cached configuration), as on Android.
-
shutdown()during start-up. A native bootstrap that answers afterAppDNA.shutdown(), or after a newerconfigure(), is now ignored on both platforms. Before, it could make a shut-down SDK ready again (firingonReady), and on Android leave it refusing every laterconfigure(); aftershutdown()and a newconfigure(), it could make the SDK ready before the new configure's bootstrap answered. -
Push action buttons and custom sound from the Console. They are displayed by the native SDKs; see the iOS and Android push guides. A button tap reaches
onPushTapped(notification, actionId)with the button's id, and a text-reply button's text arrives innotification['data']['reply_text']. -
iOS billing (native SDK).
AppDNABillingDelegate.onEntitlementsChangednow fires on iOS too, with theonEntitlementsChangedstream, including on renewal, expiry and refund, and only on a real change;expiresAtis the real expiry (StoreKit's, or the server's for a cross-platform entry) andstatusisactive/expiredfrom the realisActive(a billing grace period or billing retry staysactive). iOS purchases are verified by the AppDNA server in the background understoreKit2, andtransaction.environmentreportsproduction/sandbox/xcodeon iOS. -
iOS sign-out reports entitlements.
AppDNA.reset()now also firesonEntitlementsChanged(delegate and stream) when the set changes: it reports the device's StoreKit entitlements, which belong to the Apple ID, without the signed-out user's entitlements from other platforms — not an empty list, and nothing when that is already the last-reported state. This applies understoreKit2and under RevenueCat or Adapty: the published plugin links neither provider SDK, so the iOS SDK reads StoreKit itself. Only RevenueCat or Adapty linked into a source build of the iOS SDK skips the check. With no billing provider, nothing fires. -
Interactive maps with no host code. A map block with interactivity on now pans and zooms from the bundled native map — before, without
registerMapViewit silently drew a still image (#671). Mapbox / Google provider choice, fullscreen and top/bottom placement, editable theme colours. Existing flows with interactivity on become interactive on upgrade without being republished. -
Host data reaches every onboarding field.
onBeforeStepRender'sdataContextnow resolves in every text and image field, and a Select can build its options — including the stored value — from a host list. Unresolved{{…}}text renders empty instead of the literal token. -
Device time zone on every event. Events now carry
device.timezone, the device's IANA zone id, read by the wrapped native SDK on each event (nothing to change in Dart). The AppDNA server keeps the latest one on the identified user's profile, so pushes sent in the user's time zone, push quiet hours and journey waits use it without atimezonetrait; a trait passed toidentifystill takes precedence. -
Config and feature-flag callbacks fire together.
AppDNA.remoteConfig.onChangedandAppDNA.features.onChangedboth fire on every config refresh the SDK applies, whether or not the values that callback is about changed: both natives signal oneconfigUpdatedfor the whole config document. Compare the values you read if you only want to act on a real change. -
iOS purchase result.
AppDNA.billing.purchase()returns the product's entitlement with its realexpiresAtandstatus(read after the purchase); it was a placeholder withexpiresAt: null. -
iOS billing fixes in the wrapped SDK. A subscription in its billing grace period, or a server entitlement in
grace_period/billing_retry, is reported active although itsexpiresAthas passed; with RevenueCat linked into AppDNA (source builds), a purchase the user cancels returns{status: "cancelled"}(it was aPURCHASE_ERROR). -
Push. A Dismiss button never opens the app (iOS); a text-reply button's typed text reaches
onPushTappedon Android 7–11. Android: a push tap that was waiting forconfigure()when you calledshutdown()is dropped, as on iOS; it used to be delivered to the nextconfigure(), possibly for another user. -
iOS entitlement checks on a slow network. A server reply that arrives as the 2.5-second wait runs out is no longer reported twice, and while one reply is outstanding later checks for the same user wait on it instead of each sending another request. Android countdown: a
target_datetimemore than about 68 years away counts the real time, as on iOS (it was capped at 24855 days). On iOS, the Notification Service Extension links the newAppDNANotificationExtensionpod (see the iOS push guide). -
diagnose()reportsvetoTimeoutas given (0.5, not1on Android or0on iOS). -
Push payload types. The
notificationmap anAppDNAPushDelegatereceives has real Dart types:notification['actions'] as List<Map<String, dynamic>>?no longer throws, andactionsis absent (not an empty list) when the push has no buttons, as on React Native. -
"Show more". A button with action "Refresh this step" calls
onElementInteractionwith actionrefresh; return{'dataContext': {...}}to replace keys of the step'shook_data(nullremoves a key). Arefreshreply now gets 8 seconds — the bridge waits that long even whenvetoTimeoutis shorter, and the SDK drops a later reply even when it is larger (other interactions keepvetoTimeout, default 5 s) — anddataContextcrosses the bridge with itsnullmembers intact. -
🔴 Four presentation calls stopped throwing the native answer away.
AppDNA.presentOnboarding,AppDNA.onboarding.present,AppDNA.screen.showandAppDNA.screen.showFlowreturnedFuture<void>, soawait AppDNA.presentOnboarding('typo_id')completed successfully with no onboarding on screen and a host could not tell that from a flow that ran. All four now returnFuture<bool>—falsewhen the id is not in the published config, when the SDK is not configured yet, or when there was no view controller / foreground Activity to present from. Both natives always returned this value and React Native always delivered it; Flutter was the only surface that did not. This fix is in the Flutter plugin only (the Dart facade and its Kotlin / Swift bridge); it needed no change to the wrapped native SDKs (the billing, push and onboarding items below do change the wrapped iOS 1.0.82 / Android 1.0.54). -
Source-compatible. Existing code that awaits and ignores the result keeps compiling; only the declared type widened.
-
🔴 The plugin's iOS side now COMPILES.
ElementInteractionResult(...)was called with its arguments out of declaration order, which Swift rejects — so since 1.0.17 a Flutter host building for iOS failed with "Argument 'fieldConfigPatches' must precede argument 'fieldOptions'". Nothing caught it because nothing compiled this file:flutter analyze/flutter testare Dart and CI compiled only the Android half. CI now compiles the iOS half too.
Billing ownership, sign-in timeout, push forwarding, consumables, location #
Needs the AppDNA server from the same release. Wraps iOS 1.0.82 / Android 1.0.54.
Billing ownership (both platforms)
billingProvidernow decides who owns a store transaction. OnlystoreKit2(the default) lets the SDK buy through the store itself and finish (iOS) or verify + acknowledge (Android). UnderrevenueCat,adaptyornonethe SDK never finishes, acknowledges or consumes a transaction; before, arevenueCathost on iOS had every StoreKit update finished by the SDK, and on Android its paywall bought through Play.- A paywall tap under
revenueCat(not linked into the native SDK — every published channel),adapty(Android, and iOS unlinked) ornonefails loudly: onepurchase_failed(error_type: providerNotAvailable), thenonPaywallPurchaseFailedwitherrorType: 'providerNotAvailable'and the tapped plan'sproductId, then the paywall's failure route. NoonPaywallPurchaseStarted. On iOS a tap undernoneused to do nothing at all. Start the purchase with your provider from that callback. onPaywallPurchaseFailednow receives the realerrorTypeandproductId(it received'unknown'/nullon both platforms, whatever the failure). A failedbilling.purchasekeeps itsPURCHASE_ERRORcode and now carriesdetails['errorType']((e as PlatformException).details?['errorType']).- Restore can fail.
billing.restorePurchases()throwsPlatformException('RESTORE_ERROR')withdetails['errorType'](detailswasnullon both platforms):providerNotAvailableunderrevenueCat/adapty/none(restore through your provider — on iOS an unlinkedrevenueCatused to run a StoreKit restore, and an unlinkedadaptyreturned[]and firedonRestoreCompleted([]); on Android it returned[]undernone, and underrevenueCat/adaptyran AppDNA's own Play restore and returned the cached entitlements); on AndroidstoreKit2,networkError/serverError. Entitlements stay unchanged. - Under
revenueCatthe device no longer emitssubscription_renewed/subscription_canceled/subscription_renewal_failed; the RevenueCat webhook is the single source.adaptykeeps them. - iOS: an
adaptyprovider with an empty key (AppDNABillingProvider.adapty('')) is refused, logged as a warning, and falls back to the defaultstoreKit2. It used to configure Adapty with an empty key. Android already fell back tostoreKit2, silently; it now logs the refusal too. purchase/restorePurchasescalled beforeconfigurecompletes fail with "AppDNA SDK not configured yet — call configure() first" (errorTypeunknown); on Android a paywall tap made afterconfigurebut before billing has initialised reports the same (notproviderNotAvailable). A paywall restore tap before billing is ready, or aftershutdown(), reports the sameunknownerror on both platforms, withoutonPaywallRestoreStarted(on AndroidonPaywallRestoreStartedused to fire first, then a "Billing bridge not configured" failure with no error type). On Android a paywall restore tap underrevenueCat/adapty/noneis now refused withproviderNotAvailable, withoutonPaywallRestoreStarted(iOS never fired it); before, Started fired, thenrevenueCat/adaptyran a Play restore through AppDNA andnonefailed with "Billing bridge not configured" and no error type.shutdown()during a paywall purchase or restore no longer reports it as failed.- Android purchase errors: re-buying an owned item can fail — with a message ending in
item_already_owned(error typeunknown) when an owned consumable cannot be consumed and bought again in the same tap, and withverificationFailedwhen the owned purchase's verification fails.productNotFoundnow reaches you as the purchase error type, and verification failures reportverificationFailed(wasunknown) —networkErrorwhen the verify does not answer within 30 s. A purchase started while the Play connection is failing fails within 30 s withserverError. - A refused purchase or restore under
revenueCat/adaptycarries one message on both platforms and on every path (direct call and paywall tap): "RevenueCat: purchases are made by RevenueCat in your app" ("Adapty: …" for Adapty). - The paywall's purchase button no longer stays disabled with its spinner: it is enabled again after
every failed, cancelled or pending purchase (both platforms), after a successful purchase when it
has no
on_successaction or one this SDK version does not know (the paywall stays up for your app to close), and on Android whenshutdown()cancels the purchase. - Android
storeKit2purchases are now verified and acknowledged. The SDK's/billing/verifycall was refused by the server, so a Play purchase was never acknowledged and Play refunded it after 3 days. It is now verified and acknowledged (or consumed). - iOS event parity: the paywall
purchase_restore_failedcarrieserror_type; a paywall restore with no billing bridge emitspurchase_restore_failed{error_type: providerNotAvailable}; a directbilling.purchaseemitspurchase_started/purchase_failed, as on Android; a failed directrestorePurchases()trackspurchase_restore_failed(nopaywall_id). - Native API note (Android): source-compatible; binary-incompatible only for precompiled callers of
PurchaseResult.Failed.copy(it gainederrorType). - The SDK sends
billing_owneron its Android verify / restore calls; an app with a connected RevenueCat or Adapty integration that still runs an older Android SDK onstoreKit2gets its verifications refused until it updates.
Late purchases and consumables (no Dart API change)
onPurchaseCompletedmay arrive later, through a delivery queue: on Android for a purchase whose verification failed or that was made outside the app — e.g. a resubscribe from the Play Store, a promo-code redemption, a PENDING purchase that completed while the app was killed, or a plan change (reported once at the next return to the foreground — app start included —identify, restore orrefreshEntitlementCache(), aspurchase_completed+subscription_startedfor a subscription, with an emptypaywall_id); on iOS for interrupted and Ask-to-Buy purchases. The queue also holds an iOS Ask-to-Buy approval that arrives with nopurchase()call waiting for it, and a purchase reported after itspurchase()call was cancelled (e.g. the paywall closed). On iOS a re-buy of an owned item fires noonPurchaseCompleted(billing.purchase()still returnsstatus: 'purchased'). The queue is drained when your billing listener is attached, and then, while one is attached, whenever the SDK reports a purchase, afteridentifyand at app start; on Android also on each return to the foreground, after a restore and afterrefreshEntitlementCache(). Any Flutter billing listener drains it (the plugin cannot see whether you overrideonPurchaseCompleted), so implementonPurchaseCompletedand grant idempotently bytransactionId— delivery is at least once. The queue keeps 100 entries for 30 days; past either, a purchase needs a manual grant.- Known: after a reinstall, a purchase acknowledged before it is not reported again; a late-reported Android plan change looks like a new subscription.
- Android consumables are consumed immediately, so they can be bought again at once;
the
transactionmap passed toonPurchaseCompletedhas no quantity. - Re-buying an owned item emits
purchase_restored(reason: "item_already_owned", no price) instead of a secondpurchase_completed; restore emits nopurchase_restoredfor a purchase it just reported. On Android anITEM_ALREADY_OWNEDreturned synchronously by Play is handled like the listener result (it waslaunch_billing_flow_failed). - On
storeKit2, a trial purchase reportsis_trial: trueand price 0, and iOS purchases report the charged price (RevenueCat / Adapty omitis_trial). A late iOS purchase made while another user was signed in is delivered when that user signs in; the SDK keeps one device-wide owner map for purchase tokens. - Android:
TransactionInfo.transactionIdfalls back to the purchase token (not the product id) when Play gives no order id; a user id that is not a canonical 8-4-4-4-12 UUID (e.g.1-2-3-4-5) is now hashed into theappAccountToken, as on iOS; a lifetime purchase no longer disappears after an entitlement update (older cached entries without a type are read as lifetime when they are active Play entries without an expiry); server-side refunds and renewals now reach the device;reset()andidentifywith a different user clear the cached entitlements (the firstidentifyafter an anonymous session keeps them). - Android now also detects a renewal by the Play order id's
..Nsuffix, so newsubscription_renewedevents appear, including on a trial conversion. Known: a same-product base-plan change counts as a renewal, and a 7-day trial conversion found this way is timed when the device notices it — mostly in the d30 trial-to-paid window rather than d7. - New event properties:
emitted_by, lifecycletransaction_id/original_transaction_id/cancel_semantics,original_transaction_idonpurchase_completedandsubscription_started,is_consumable;delivery_idonpush_delivered/push_tapped;purchase_failed.reasonnow carries the server's code for a verification failure.AppDNA.trackdrops a host-passedemitted_by/_appdna_origin, also on calls made beforeconfigure.
Streams after shutdown() → configure()
billing.onEntitlementsChangedand the web-entitlement stream keep emitting afterAppDNA.shutdown()thenAppDNA.configure()(both platforms). They went silent for the rest of the process: the nativeshutdown()drops every entitlement listener and the plugin never registered again. Each change is still emitted once. A web-entitlement listen made beforeconfigure()is now attached byconfigure()on Android (it was dropped).- Android: the remote-config and feature-flag change streams now fire on every config refresh,
including after
shutdown()→configure()and when listened to before the SDK is ready (they could stay silent for the whole session). - Android: the in-app message stream's delegate and
shouldShowMessageveto are applied again by everyconfigure(), so they keep working aftershutdown()→configure()and when listened to beforeconfigure()(they were set on the native message manager of that moment only). - iOS:
billing.purchase()returns{status: 'cancelled'}only for a real user cancellation (typed error), no longer for any error whose message contains "cancel"; other failures throwPURCHASE_ERRORwith theirerrorType.
Onboarding
vetoTimeoutis honoured on every Flutter hook (it was ignored — 5 s everywhere), anddiagnose()reports the real number of timed-out hooks. A value ≤ 0 means the default (5 s).- Sign-in actions in
onBeforeStepAdvancewait at least 120 s (max(vetoTimeout, 120 s)), so a Google / Apple sign-in with an account picker or 2FA no longer ends in "Sign-in isn't available right now" after 5 s. Make the sign-in hook idempotent: a reply after 120 s is dropped and the user retries. - iOS: an
onBeforeStepAdvanceanswer of{'type': 'skipToWithData', 'stepId': …}now skips tostepId, as on Android; it advanced to the next step instead.{'type': 'skipTo', 'stepId': …, 'data': {…}}remains the canonical form. - The SDK's own interactive map (new in this release — see the interactive-maps entry above) draws the route polyline, otherwise straight segments between the stops. With auto-fit on (the default) the camera fits the route and its stops, and a single point uses zoom 15; with auto-fit off it centres on the authored centre at the authored zoom.
AppDNA.deepLinks.getLocationDatano longer crashes on iOS after a typed answer, and returnsformattedAddress/rawQuerywith null coordinates for text typed without selecting a suggestion (Android returnednull); a selection carries city, state, country, coordinates and timezone. Both location writers on both platforms (the form-step Location field and the Location content block) now store the same shape: the iOS form-step field stores typed text (it stored nothing), a blank value is left out rather than stored as'', a failed time-zone lookup (on device or AppDNA's) stores no zone instead of'UTC', and a selection storestimezoneOffsetandrawQuery(the typed search text).- An
onBeforeStepAdvancereply{'type': 'skipTo'}(orskipToWithData) without astepId, or with a blank one, is no longer a skip: on a sign-in step the bridge blocks it as no answer (it advanced the user past the sign-in step), elsewhere it proceeds (with itsdata, if any). - iOS: a
social_logintap on a step with an input field whose id isactionorprovidernow still reportsaction: 'social_login'and the button's provider (as Android does). AppDNA.setSessionDatawith a value that is not valid JSON (for exampledouble.nan) no longer throws on Android; on both platforms that value is not saved to disk and a warning naming it is logged. Every other saved value is kept.- 🔴 Dart source break:
LocationData's fields other thanformattedAddressare now nullable (city,state,stateCode,country,countryCode,latitude,longitude,timezone,timezoneOffset,rawQuery), andLocationData.fromMapno longer invents''/0.0/'UTC'/0for a missing value. Code that reads them as non-null must handlenull. - Android: location autocomplete no longer stores
0,0for a suggestion that comes back without coordinates (the suggestion is kept, withoutlatitude/longitude); the event queue no longer spins the IO thread pool whenflushIntervalis very large.
Push
- Android: the SDK's messaging service handles only AppDNA-marked pushes (
appdna: "1");onPushReceivedno longer fires for your own messages, and a data-only message is never shown as a blank notification. Turn the service off with the resource boolappdna_messaging_service_enabled=false. - Tap routing follows one ladder on both platforms: the tapped button, then the push's
action, then flataction_type/action_value, thenscreen_id, thendeep_link— a push with both an action and ascreen_idnow follows the action (Android used to preferscreen_id). iOS additionally falls back to the first button's action when the push has none of these. AppDNAPush.handlePushTap()(Android) returnsfalseand does nothing for an intent without the AppDNA marker — except a tap on a notification that an Android SDK before 1.0.54 displayed (still in the tray when the app updates), which predates the marker: an intent whose extras are exactly that SDK's tap keys is still handled as an AppDNA tap.- Android: a tap on a notification that reaches the running app through
onNewIntent(body, button and text-reply taps on notifications the SDK displayed, and a tap that restores the app after its process was killed) is tracked, routed and passed toonPushTappedby the plugin itself, once the SDK is configured. Before, the plugin read only the activity's launch intent, so these taps did nothing.handlePushTap()now answers for the newest intent (the last one throughonNewIntent, else the launch intent) — only that one, so an older launch tap no longer answerstruefor a newer intent that is not a tap; it returnstruefor a tap the plugin already handled, without tracking or routing it again. It answers at once: beforeconfigure, aftershutdown()or after aconfigurethat threw, it used to wait for the SDK (for ever, if it never became ready); a tap not handled yet is now tracked and routed once the SDK is ready (one asked for beforeconfigureused to be lost). - Android: a tap that starts a new activity while the Flutter engine is still running (a cached engine,
or the activity was closed with Back) is handed to the SDK when the plugin attaches to that activity.
It was handled only if the app called
configureagain orhandlePushTap(). - Android: before
configure, intents that are not AppDNA taps are no longer held for the SDK, and the taps that wait for it are held once for the whole app (at most 64).handlePushTap()answersfalsefor such an intent at once, and keeps answeringtruefor a tap even if the native SDK fails while handling it. - Android: a push delegate set before
configurenow receivesonPushReceived/onPushTapped; it was dropped by the native SDK. - Android 8.0 / 8.1: a push with a channel group no longer crashes the app (the native SDK called an API 28 method there). Dismissing a presentation with another queued behind it no longer crashes below Android 15.
- Android: a push action (tapped button or body action) with a blank value routes nowhere instead of
falling through to
screen_id/deep_link. - New
AppDNA.push.isAppDNAMessage(data),handleMessage(data)andhandleTap(data, actionId:)for apps that own Firebase Messaging (firebase_messaging). Each is a no-op returningfalsefor a push without the marker, and tracks each push once even if the SDK also saw it.handleMessagetracks delivery and never displays anything. - iOS: the SDK installs its notification handler at launch, chaining any existing
UNUserNotificationCenterdelegate (includingFlutterAppDelegate/ FlutterFire), so AppDNA pushes are tracked and routed — cold-start taps included — with no forwarding code. Info.plist keys:AppDNADisableNotificationProxy(opt out) andAppDNAForegroundPresentation. With FlutterFire as the outer notification delegate, a push arriving in the foreground keeps FlutterFire's presentation and is not tracked as delivered unlessAppDNAForegroundPresentationis set or the delegate AppDNA wrapped implementswillPresent; taps are still tracked, because FlutterFire forwards them to the delegate it wrapped. With no notification delegate and no push library, an AppDNA push arriving in the foreground is now shown (AppDNA's foreground options), tracked as delivered and passed toonPushReceived— before, it was not shown. When the SDK ends up as the OUTER delegate and your delegate implementswillPresent(orAppDNAForegroundPresentationis set), AppDNA's foreground options apply to AppDNA pushes and your delegate is not called for them; if neither holds, iOS does not ask, and an AppDNA push arriving in the foreground is not shown and not tracked as delivered. Other pushes are forwarded to your delegate when it implements the method (otherwise completed with no presentation, the iOS default); the exception is theAppDNAForegroundPresentationtrade-off (with the key set, and AppDNA innermost under FlutterFire with no delegate of its own to forward to, your own FCM pushes get no foreground presentation). - Android: the tap that launched the app is handled at
configure. The plugin hands the launch intent to the native SDK when you callconfigure, as the React Native module does, so the tap is tracked, routed and passed toonPushTappedwithout a call toAppDNAPush.handlePushTap(). Calling it as well is safe: it returnstruefor a tap already handled and tracks and routes nothing again. The plugin hands native a copy of each tap intent — fromconfigure, fromonNewIntentand fromhandlePushTap()— so the activity's intent keeps itsappdna,push_idanddelivery_idextras, as on React Native. It hands each intent over once: callingconfigureagain (aftershutdown()) does not re-run the launch intent, however many taps came after it, and a tap on a notification an earlier SDK version posted is routed once.
Build
- The plugin ships a
consumer-rules.pro, so an R8-minified Android release build keeps the classes the plugin needs. - iOS: the SDK no longer references the Contacts, EventKit, App Tracking Transparency or
Photos-library APIs unless your flow uses those permission steps. Every app still needs
NSLocationWhenInUseUsageDescription. If App Store Connect still asks for a key, add it and tell us.
1.0.17 #
- Wraps iOS 1.0.79 / Android 1.0.51. No Dart API change — every fix and addition below is in the wrapped native SDKs, so upgrading is a version bump only.
- Onboarding fixes: the stray grey "Skip" no longer draws on every Android screen;
horizontal_alignis honoured on a width-constrained block (a 75%-wide sound button or button authoredcenterwas stuck left); content pinned behind the bottom button zone is reachable again, because the scroll reserve is measured instead of a hardcoded 80dp; a summary card with one unresolved token keeps the card instead of disappearing; the "or" between the social-login divider segments takes the authored colour. - Onboarding additions: a Multi-buttons group that lays 1–3 real buttons per row with a centred or stretched last row and its own background; and a button action that refreshes a step in place, letting the host swap a select's options without advancing the flow.
- Paywalls: a standalone Back button (style, position, colour, size, delay, text) independent of the close affordance, plus a Restore background colour and corner radius and a corner radius on each extra CTA button.
1.0.16 #
- Wraps iOS 1.0.78 / Android 1.0.50. No Dart API change — every fix and addition below is in the wrapped native SDKs, so upgrading is a version bump only.
- Onboarding: a CTA with
action: "skip"now advances on Android (it was a dead button); the social-login divider draws once and honours its authored position; the warning banner gained a subtitle, text alignment, its own border and corner radius, per-role font sizes and a font family; the device mockup keeps real phone proportions instead of stretching vertically, andimage_fitapplies inside the frame. - Paywalls: legal text renders inline
[label](url)links in the authored Link Color — which previously never reached the device at all; the sticky-footer subtitle takes a size and colour instead of being pinned at 10pt; the CTA section gained a Restore gap, up to six extra buttons with configurable actions, and actions on the CTA and Restore link themselves; a paywall can show a back chevron that returns the user to the previous screen.
1.0.15 #
- Wraps iOS 1.0.77 / Android 1.0.49. Navigation rules that branch on an answer given on an earlier screen now match; previously they silently fell through to the next screen in order. No Dart API change — the fix is entirely in the wrapped native SDKs.
1.0.6 #
- Full feature parity with the native SDK's expanded surface (wraps iOS 1.0.68 /
Android 1.0.40). The Flutter API now covers the complete capability set: host
approval callbacks for in-app messages, server-driven screen actions, deep links
and promo codes, plus onboarding step and permission hooks; an inline
AppDNAScreenSlotwidget for embedding server-driven screens; brand-accent, runtime-lock, bundle-version and notification-icon reads; session-data get/set/clear; and SDK lifecycle and init delegates.diagnose()now returns the health report as a string and reports the Flutter SDK version. All rendering, business logic, networking and storage remain in the native SDK.
1.0.5 #
appdna_feature_parity: 1.0.65— parity marker advanced to wrap iOS 1.0.65- Android 1.0.37, experiment-aware presentation. This is
a purely-native change: the native surface managers decide treatment-vs-
active inside
present(), so a Flutter app embedding the native SDKs renders experiment treatments automatically with no Dart change. The publishable Flutterversionstays at 1.0.5; the thin-wrapper Dart catch-up (if any) is owned. Flutter remains a thin wrapper per ADR-001.
- Android 1.0.37, experiment-aware presentation. This is
a purely-native change: the native surface managers decide treatment-vs-
active inside
appdna_feature_parity: 1.0.64— Flutter now wraps iOS 1.0.64 + Android 1.0.36, hard SDK suspension. Adds the codegen'dAppDNALifecycleDelegateinterface (onSdkRuntimeLocked/onSdkRuntimeUnlocked) so hosts can react to backend-driven SDK lock state changes. Lock state arrives on the/sdk/bootstrapresponse's new optionalruntime_lockobject when the tenant has been per-key- suspended (day 20+ of billing overdue, via the sweep) or cancelled. Flutter remains a thin wrapper per ADR-001: enforcement lives in the native layer (iOS 1.0.64 / Android 1.0.36).
1.0.4 #
appdna_feature_parity: 1.0.63— Flutter now wraps iOS 1.0.63 + Android 1.0.35, the cross-account-entitlement-leak follow-up. The 1.0.3 migration-tolerant policy (granting untagged historical purchases to whoever was currently identified) is now scoped to the device's first identified user, so an SDK-paywall-during-onboarding purchase made beforeidentify(...)was called can only be claimed by the legitimate first user — a later user-switch on the same device is denied. Device QA reproduced the original leak; this release closes it. No Dart code changes — hosts pick up the fix automatically by upgrading the Flutter package.
1.0.3 #
appdna_feature_parity: 1.0.62— Flutter now wraps iOS 1.0.62 + Android 1.0.34, the cross-account-entitlement-leak hotfix. Both halves of the fix (write-side per-userappAccountToken/obfuscatedAccountIdbinding viaAppAccountTokenResolver; read-side filter on every device-level entitlement read viaEntitlementOwnerFilter) ship in the bundled native iOS + Android — no Dart code changes, hosts pick up the fix automatically by upgrading the Flutter package.
1.0.2 #
- codegen artifacts for
AppDNAEnvironmentDTO + delegate interfaces shipped underlib/generated/(autogenerated; do not hand- edit).appdna_feature_parity: 1.0.61— Flutter now wraps iOS 1.0.61 (entitlement-aware paywall triggers + restore routing) plus the Android catch-up. No Dart code changes from the parity bump — the new behavior ships in the bundled native iOS + Android. - Test runner:
shared_fixtures_test.dartrunner self-check moved inside atest()block so Flutter 3.41+ doesn't throwOutsideTestExceptionat file-load time.
1.0.1 #
- Fix build errors and add smoke tests
- Add repository field to pubspec.yaml
- Add MIT LICENSE file
1.0.0 #
- Initial release
- Core SDK: configure, identify, track, reset
- Push notifications module
- Billing module (StoreKit 2 / Google Play Billing)
- Onboarding flows
- Paywall presentation
- Offline-first architecture
- Config bundle support